diff --git a/README.md b/README.md index b4f611a30..0c4c164a9 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,27 @@ FlutterBlue is a bluetooth plugin for [Flutter](http://www.flutter.io), a new mobile SDK to help developers build modern apps for iOS and Android. +## Setup + +## Android + +This app contains *most* but not all of the permissions you'll need to get up and running with bluetooth. For android you'll need to consider whether or not your scan will derive location. If you it does not, you must add this line to your manifest: + +```xml + +``` + +In the case you do derive physical location, you'll need to add this: + +```xml + +``` + +At runtime, you'll also need to request the appropriate permissions for bluetooth and location. You can do this with the [permission_handler](https://pub.dev/packages/permission_handler) package. + +If you haven't taken any of the previous steps, you'll likely get an error or recieve no scan results. + ## Alpha version This library is actively developed alongside production apps, and the API will evolve as we continue our way to version 1.0. diff --git a/android/build.gradle b/android/build.gradle index 056f3dbba..98f9c7e47 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,35 +1,22 @@ -def PLUGIN = "flutter_blue"; -def ANDROIDX_WARNING = "flutterPluginsAndroidXWarning"; -gradle.buildFinished { buildResult -> - if (buildResult.failure && !rootProject.ext.has(ANDROIDX_WARNING)) { - println ' *********************************************************' - println 'WARNING: This version of ' + PLUGIN + ' will break your Android build if it or its dependencies aren\'t compatible with AndroidX.' - println ' See https://goo.gl/CP92wY for more information on the problem and how to fix it.' - println ' This warning prints for all Android build failures. The real root cause of the error may be unrelated.' - println ' *********************************************************' - rootProject.ext.set(ANDROIDX_WARNING, true); - } -} - group 'com.pauldemarco.flutter_blue' -version '1.0-SNAPSHOT' +version '1.0' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.13' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -37,38 +24,35 @@ apply plugin: 'com.android.library' apply plugin: 'com.google.protobuf' android { - compileSdkVersion 28 + compileSdkVersion 31 defaultConfig { minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - lintOptions { - disable 'InvalidPackage' + + compileOptions { + // Use Java 8 language features and APIs + // https://developer.android.com/studio/write/java8-support + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + // Sets Java compatibility to Java 8 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } + sourceSets { main { proto { srcDir '../protos' } - java { - srcDir '../protos' - } } } } protobuf { - // Configure the protoc executable protoc { - // Download from repositories - artifact = 'com.google.protobuf:protoc:3.13.0' - } - plugins { - javalite { - // The codegen for lite comes as a separate artifact - artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' - } + artifact = 'com.google.protobuf:protoc:3.17.3' } generateProtoTasks { all().each { task -> @@ -82,6 +66,6 @@ protobuf { } dependencies { - implementation 'com.google.protobuf:protobuf-javalite:3.11.0' - implementation 'androidx.core:core:1.2.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation 'com.google.protobuf:protobuf-javalite:3.17.3' } \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a48fda615..32554b406 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,16 @@ - - - - + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java index ed7b1f6da..f26f29b67 100644 --- a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java +++ b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java @@ -60,9 +60,9 @@ import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener; - /** FlutterBluePlugin */ -public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, RequestPermissionsResultListener { +public class FlutterBluePlugin + implements FlutterPlugin, ActivityAware, MethodCallHandler, RequestPermissionsResultListener { private static final String TAG = "FlutterBluePlugin"; private final Object initializationLock = new Object(); private Context context; @@ -77,12 +77,19 @@ public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCa private ActivityPluginBinding activityBinding; private Activity activity; - private static final int REQUEST_FINE_LOCATION_PERMISSIONS = 1452; static final private UUID CCCD_ID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private final Map mDevices = new HashMap<>(); private LogLevel logLevel = LogLevel.EMERGENCY; - // Pending call and result for startScan, in the case where permissions are needed + private interface OperationOnPermission { + public void op(boolean granted, String permission); + } + + private int lastEventId = 1452; + private Map operationsOnPermission = new HashMap(); + + // Pending call and result for startScan, in the case where permissions are + // needed private MethodCall pendingCall; private Result pendingResult; private final ArrayList macDeviceScanned = new ArrayList<>(); @@ -98,7 +105,8 @@ public static void registerWith(Registrar registrar) { } } - public FlutterBluePlugin() {} + public FlutterBluePlugin() { + } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @@ -175,28 +183,25 @@ private void tearDown() { mBluetoothManager = null; } - @Override public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { - if(mBluetoothAdapter == null && !"isAvailable".equals(call.method)) { + if (mBluetoothAdapter == null && !"isAvailable".equals(call.method)) { result.error("bluetooth_unavailable", "the device does not have bluetooth", null); return; } switch (call.method) { - case "setLogLevel": - { - int logLevelIndex = (int)call.arguments; + case "setLogLevel": { + int logLevelIndex = (int) call.arguments; logLevel = LogLevel.values()[logLevelIndex]; result.success(null); break; } - case "state": - { + case "state": { Protos.BluetoothState.Builder p = Protos.BluetoothState.newBuilder(); try { - switch(mBluetoothAdapter.getState()) { + switch (mBluetoothAdapter.getState()) { case BluetoothAdapter.STATE_OFF: p.setState(Protos.BluetoothState.State.OFF); break; @@ -220,104 +225,120 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "isAvailable": - { + case "isAvailable": { result.success(mBluetoothAdapter != null); break; } - case "isOn": - { + case "isOn": { result.success(mBluetoothAdapter.isEnabled()); break; } - case "startScan": - { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions( - activityBinding.getActivity(), - new String[] { - Manifest.permission.ACCESS_FINE_LOCATION - }, - REQUEST_FINE_LOCATION_PERMISSIONS); - pendingCall = call; - pendingResult = result; - break; - } - startScan(call, result); + case "startScan": { + ensurePermissionsBeforeAction( + new String[] { Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT }, + Manifest.permission.ACCESS_FINE_LOCATION, (granted, permission) -> { + if (granted) { + // print to console that it started + Log.i(TAG, "startScan"); + startScan(call, result); + } else { + result.error( + "no_permissions", + String.format("flutter_blue plugin requires %s for scanning", permission), + null); + } + }); break; } - case "stopScan": - { + case "stopScan": { stopScan(); result.success(null); break; } - case "getConnectedDevices": - { - List devices = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); - Protos.ConnectedDevicesResponse.Builder p = Protos.ConnectedDevicesResponse.newBuilder(); - for(BluetoothDevice d : devices) { - p.addDevices(ProtoMaker.from(d)); - } - result.success(p.build().toByteArray()); + case "getConnectedDevices": { + ensurePermissionBeforeAction(Manifest.permission.BLUETOOTH_CONNECT, null, (granted, permission) -> { + if (!granted) { + result.error( + "no_permissions", + String.format("flutter_blue plugin requires %s for obtaining connected devices", + permission), + null); + return; + } + List devices = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT); + Protos.ConnectedDevicesResponse.Builder p = Protos.ConnectedDevicesResponse.newBuilder(); + for (BluetoothDevice d : devices) { + p.addDevices(ProtoMaker.from(d)); + } + result.success(p.build().toByteArray()); + log(LogLevel.EMERGENCY, "mDevices size: " + mDevices.size()); + }); break; } - case "connect": - { - byte[] data = call.arguments(); - Protos.ConnectRequest options; - try { - options = Protos.ConnectRequest.newBuilder().mergeFrom(data).build(); - } catch (InvalidProtocolBufferException e) { - result.error("RuntimeException", e.getMessage(), e); - break; - } - String deviceId = options.getRemoteId(); - BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); - boolean isConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device); + case "connect": { + ensurePermissionBeforeAction(Manifest.permission.BLUETOOTH_CONNECT, null, (granted, permission) -> { + if (!granted) { + result.error( + "no_permissions", + String.format("flutter_blue plugin requires %s for new connection", permission), null); + return; + } - if(mDevices.containsKey(deviceId)) { - if (isConnected) { - result.success(null); - } else { - BluetoothDeviceCache deviceToConnect = mDevices.get(deviceId); - if(deviceToConnect != null && deviceToConnect.gatt != null && deviceToConnect.gatt.connect()){ + byte[] data = call.arguments(); + Protos.ConnectRequest options; + try { + options = Protos.ConnectRequest.newBuilder().mergeFrom(data).build(); + } catch (InvalidProtocolBufferException e) { + result.error("RuntimeException", e.getMessage(), e); + return; + } + String deviceId = options.getRemoteId(); + BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); + boolean isConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT).contains(device); + + if (mDevices.containsKey(deviceId)) { + if (isConnected) { result.success(null); } else { - result.error("reconnect_error", "error when reconnecting to device", null); + BluetoothDeviceCache deviceToConnect = mDevices.get(deviceId); + if (deviceToConnect != null && deviceToConnect.gatt != null + && deviceToConnect.gatt.connect()) { + result.success(null); + } else { + result.error("reconnect_error", "error when reconnecting to device", null); + } } - } - return; - } else { - // New request, connect and add gattServer to Map - BluetoothGatt gattServer; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback, BluetoothDevice.TRANSPORT_LE); + return; } else { - gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback); + // New request, connect and add gattServer to Map + BluetoothGatt gattServer; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback, + BluetoothDevice.TRANSPORT_LE); + } else { + gattServer = device.connectGatt(context, options.getAndroidAutoConnect(), mGattCallback); + } + mDevices.put(deviceId, new BluetoothDeviceCache(gattServer)); + result.success(null); } - mDevices.put(deviceId, new BluetoothDeviceCache(gattServer)); - result.success(null); - } + }); break; } - case "disconnect": - { - String deviceId = (String)call.arguments; + case "disconnect": { + String deviceId = (String) call.arguments; BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); int state = mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT); BluetoothDeviceCache cache = mDevices.remove(deviceId); - if(cache != null) { + if (cache != null) { BluetoothGatt gattServer = cache.gatt; gattServer.disconnect(); - if(state == BluetoothProfile.STATE_DISCONNECTED) { + if (state == BluetoothProfile.STATE_DISCONNECTED) { gattServer.close(); } } @@ -325,54 +346,50 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "deviceState": - { - String deviceId = (String)call.arguments; + case "deviceState": { + String deviceId = (String) call.arguments; BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceId); int state = mBluetoothManager.getConnectionState(device, BluetoothProfile.GATT); try { result.success(ProtoMaker.from(device, state).toByteArray()); - } catch(Exception e) { + } catch (Exception e) { result.error("device_state_error", e.getMessage(), e); } break; } - case "discoverServices": - { - String deviceId = (String)call.arguments; + case "discoverServices": { + String deviceId = (String) call.arguments; try { BluetoothGatt gatt = locateGatt(deviceId); - if(gatt.discoverServices()) { + if (gatt.discoverServices()) { result.success(null); } else { result.error("discover_services_error", "unknown reason", null); } - } catch(Exception e) { + } catch (Exception e) { result.error("discover_services_error", e.getMessage(), e); } break; } - case "services": - { - String deviceId = (String)call.arguments; + case "services": { + String deviceId = (String) call.arguments; try { BluetoothGatt gatt = locateGatt(deviceId); Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder(); p.setRemoteId(deviceId); - for(BluetoothGattService s : gatt.getServices()){ + for (BluetoothGattService s : gatt.getServices()) { p.addServices(ProtoMaker.from(gatt.getDevice(), s, gatt)); } result.success(p.build().toByteArray()); - } catch(Exception e) { + } catch (Exception e) { result.error("get_services_error", e.getMessage(), e); } break; } - case "readCharacteristic": - { + case "readCharacteristic": { byte[] data = call.arguments(); Protos.ReadCharacteristicRequest request; try { @@ -386,22 +403,24 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { BluetoothGattCharacteristic characteristic; try { gattServer = locateGatt(request.getRemoteId()); - characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); - } catch(Exception e) { + characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), + request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); + } catch (Exception e) { result.error("read_characteristic_error", e.getMessage(), null); return; } - if(gattServer.readCharacteristic(characteristic)) { + if (gattServer.readCharacteristic(characteristic)) { result.success(null); } else { - result.error("read_characteristic_error", "unknown reason, may occur if readCharacteristic was called before last read finished.", null); + result.error("read_characteristic_error", + "unknown reason, may occur if readCharacteristic was called before last read finished.", + null); } break; } - case "readDescriptor": - { + case "readDescriptor": { byte[] data = call.arguments(); Protos.ReadDescriptorRequest request; try { @@ -416,23 +435,24 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { BluetoothGattDescriptor descriptor; try { gattServer = locateGatt(request.getRemoteId()); - characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); + characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), + request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); descriptor = locateDescriptor(characteristic, request.getDescriptorUuid()); - } catch(Exception e) { + } catch (Exception e) { result.error("read_descriptor_error", e.getMessage(), null); return; } - if(gattServer.readDescriptor(descriptor)) { + if (gattServer.readDescriptor(descriptor)) { result.success(null); } else { - result.error("read_descriptor_error", "unknown reason, may occur if readDescriptor was called before last read finished.", null); + result.error("read_descriptor_error", + "unknown reason, may occur if readDescriptor was called before last read finished.", null); } break; } - case "writeCharacteristic": - { + case "writeCharacteristic": { byte[] data = call.arguments(); Protos.WriteCharacteristicRequest request; try { @@ -446,25 +466,26 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { BluetoothGattCharacteristic characteristic; try { gattServer = locateGatt(request.getRemoteId()); - characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); - } catch(Exception e) { + characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), + request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); + } catch (Exception e) { result.error("write_characteristic_error", e.getMessage(), null); return; } // Set characteristic to new value - if(!characteristic.setValue(request.getValue().toByteArray())){ + if (!characteristic.setValue(request.getValue().toByteArray())) { result.error("write_characteristic_error", "could not set the local value of characteristic", null); } // Apply the correct write type - if(request.getWriteType() == Protos.WriteCharacteristicRequest.WriteType.WITHOUT_RESPONSE) { + if (request.getWriteType() == Protos.WriteCharacteristicRequest.WriteType.WITHOUT_RESPONSE) { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); } else { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); } - if(!gattServer.writeCharacteristic(characteristic)){ + if (!gattServer.writeCharacteristic(characteristic)) { result.error("write_characteristic_error", "writeCharacteristic failed", null); return; } @@ -473,8 +494,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "writeDescriptor": - { + case "writeDescriptor": { byte[] data = call.arguments(); Protos.WriteDescriptorRequest request; try { @@ -489,19 +509,20 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { BluetoothGattDescriptor descriptor; try { gattServer = locateGatt(request.getRemoteId()); - characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); + characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), + request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); descriptor = locateDescriptor(characteristic, request.getDescriptorUuid()); - } catch(Exception e) { + } catch (Exception e) { result.error("write_descriptor_error", e.getMessage(), null); return; } // Set descriptor to new value - if(!descriptor.setValue(request.getValue().toByteArray())){ + if (!descriptor.setValue(request.getValue().toByteArray())) { result.error("write_descriptor_error", "could not set the local value for descriptor", null); } - if(!gattServer.writeDescriptor(descriptor)){ + if (!gattServer.writeDescriptor(descriptor)) { result.error("write_descriptor_error", "writeCharacteristic failed", null); return; } @@ -510,8 +531,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "setNotification": - { + case "setNotification": { byte[] data = call.arguments(); Protos.SetNotificationRequest request; try { @@ -526,46 +546,52 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { BluetoothGattDescriptor cccDescriptor; try { gattServer = locateGatt(request.getRemoteId()); - characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); + characteristic = locateCharacteristic(gattServer, request.getServiceUuid(), + request.getSecondaryServiceUuid(), request.getCharacteristicUuid()); cccDescriptor = characteristic.getDescriptor(CCCD_ID); - if(cccDescriptor == null) { - throw new Exception("could not locate CCCD descriptor for characteristic: " +characteristic.getUuid().toString()); + if (cccDescriptor == null) { + throw new Exception("could not locate CCCD descriptor for characteristic: " + + characteristic.getUuid().toString()); } - } catch(Exception e) { + } catch (Exception e) { result.error("set_notification_error", e.getMessage(), null); return; } byte[] value = null; - if(request.getEnable()) { - boolean canNotify = (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0; - boolean canIndicate = (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0; - if(!canIndicate && !canNotify) { + if (request.getEnable()) { + boolean canNotify = (characteristic.getProperties() + & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0; + boolean canIndicate = (characteristic.getProperties() + & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0; + if (!canIndicate && !canNotify) { result.error("set_notification_error", "the characteristic cannot notify or indicate", null); return; } - if(canIndicate) { + if (canIndicate) { value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE; } - if(canNotify) { + if (canNotify) { value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE; } } else { value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; } - if(!gattServer.setCharacteristicNotification(characteristic, request.getEnable())){ - result.error("set_notification_error", "could not set characteristic notifications to :" + request.getEnable(), null); + if (!gattServer.setCharacteristicNotification(characteristic, request.getEnable())) { + result.error("set_notification_error", + "could not set characteristic notifications to :" + request.getEnable(), null); return; } - if(!cccDescriptor.setValue(value)) { - result.error("set_notification_error", "error when setting the descriptor value to: " + Arrays.toString(value), null); + if (!cccDescriptor.setValue(value)) { + result.error("set_notification_error", + "error when setting the descriptor value to: " + Arrays.toString(value), null); return; } - if(!gattServer.writeDescriptor(cccDescriptor)) { + if (!gattServer.writeDescriptor(cccDescriptor)) { result.error("set_notification_error", "error when writing the descriptor", null); return; } @@ -574,11 +600,10 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "mtu": - { - String deviceId = (String)call.arguments; + case "mtu": { + String deviceId = (String) call.arguments; BluetoothDeviceCache cache = mDevices.get(deviceId); - if(cache != null) { + if (cache != null) { Protos.MtuSizeResponse.Builder p = Protos.MtuSizeResponse.newBuilder(); p.setRemoteId(deviceId); p.setMtu(cache.mtu); @@ -589,8 +614,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { break; } - case "requestMtu": - { + case "requestMtu": { byte[] data = call.arguments(); Protos.MtuSizeRequest request; try { @@ -604,42 +628,70 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { try { gatt = locateGatt(request.getRemoteId()); int mtu = request.getMtu(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if(gatt.requestMtu(mtu)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (gatt.requestMtu(mtu)) { result.success(null); } else { result.error("requestMtu", "gatt.requestMtu returned false", null); } } else { - result.error("requestMtu", "Only supported on devices >= API 21 (Lollipop). This device == " + Build.VERSION.SDK_INT, null); + result.error("requestMtu", "Only supported on devices >= API 21 (Lollipop). This device == " + + Build.VERSION.SDK_INT, null); } - } catch(Exception e) { + } catch (Exception e) { result.error("requestMtu", e.getMessage(), e); } break; } - default: - { + default: { result.notImplemented(); break; } } } + private void ensurePermissionBeforeAction(String permissionA12, String permission, + OperationOnPermission operation) { + ensurePermissionsBeforeAction(new String[] { permissionA12 }, permission, operation); + } + + private void ensurePermissionsBeforeAction(String[] permissionsA12, String permission, + OperationOnPermission operation) { + String[] permissions = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? permissionsA12 + : permission != null ? new String[] { permission } : null; + if (!allPermissionsGranted(permissions)) { + operationsOnPermission.put(lastEventId, (granted, perm) -> { + operationsOnPermission.remove(lastEventId); + operation.op(granted, perm); + }); + ActivityCompat.requestPermissions( + activityBinding.getActivity(), + permissions, + lastEventId); + lastEventId++; + } else { + operation.op(true, permission); + } + } + + private boolean allPermissionsGranted(String[] permissions) { + if (permissions == null) + return true; + for (int i = 0; i < permissions.length; i++) { + if (ContextCompat.checkSelfPermission(context, permissions[i]) != PackageManager.PERMISSION_GRANTED) + return false; + } + return true; + } + @Override public boolean onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) { - if (requestCode == REQUEST_FINE_LOCATION_PERMISSIONS) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - startScan(pendingCall, pendingResult); - } else { - pendingResult.error( - "no_permissions", "flutter_blue plugin requires location permissions for scanning", null); - pendingResult = null; - pendingCall = null; - } + OperationOnPermission operation = operationsOnPermission.get(requestCode); + if (operation != null && grantResults.length > 0) { + operation.op(grantResults[0] == PackageManager.PERMISSION_GRANTED, permissions[0]); return true; } return false; @@ -647,41 +699,46 @@ public boolean onRequestPermissionsResult( private BluetoothGatt locateGatt(String remoteId) throws Exception { BluetoothDeviceCache cache = mDevices.get(remoteId); - if(cache == null || cache.gatt == null) { + if (cache == null || cache.gatt == null) { throw new Exception("no instance of BluetoothGatt, have you connected first?"); } else { return cache.gatt; } } - private BluetoothGattCharacteristic locateCharacteristic(BluetoothGatt gattServer, String serviceId, String secondaryServiceId, String characteristicId) throws Exception { + private BluetoothGattCharacteristic locateCharacteristic(BluetoothGatt gattServer, String serviceId, + String secondaryServiceId, String characteristicId) throws Exception { BluetoothGattService primaryService = gattServer.getService(UUID.fromString(serviceId)); - if(primaryService == null) { + if (primaryService == null) { throw new Exception("service (" + serviceId + ") could not be located on the device"); } BluetoothGattService secondaryService = null; - if(secondaryServiceId.length() > 0) { - for(BluetoothGattService s : primaryService.getIncludedServices()){ - if(s.getUuid().equals(UUID.fromString(secondaryServiceId))){ + if (secondaryServiceId.length() > 0) { + for (BluetoothGattService s : primaryService.getIncludedServices()) { + if (s.getUuid().equals(UUID.fromString(secondaryServiceId))) { secondaryService = s; } } - if(secondaryService == null) { - throw new Exception("secondary service (" + secondaryServiceId + ") could not be located on the device"); + if (secondaryService == null) { + throw new Exception( + "secondary service (" + secondaryServiceId + ") could not be located on the device"); } } BluetoothGattService service = (secondaryService != null) ? secondaryService : primaryService; BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(characteristicId)); - if(characteristic == null) { - throw new Exception("characteristic (" + characteristicId + ") could not be located in the service ("+service.getUuid().toString()+")"); + if (characteristic == null) { + throw new Exception("characteristic (" + characteristicId + ") could not be located in the service (" + + service.getUuid().toString() + ")"); } return characteristic; } - private BluetoothGattDescriptor locateDescriptor(BluetoothGattCharacteristic characteristic, String descriptorId) throws Exception { + private BluetoothGattDescriptor locateDescriptor(BluetoothGattCharacteristic characteristic, String descriptorId) + throws Exception { BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(descriptorId)); - if(descriptor == null) { - throw new Exception("descriptor (" + descriptorId + ") could not be located in the characteristic ("+characteristic.getUuid().toString()+")"); + if (descriptor == null) { + throw new Exception("descriptor (" + descriptorId + ") could not be located in the characteristic (" + + characteristic.getUuid().toString() + ")"); } return descriptor; } @@ -699,16 +756,20 @@ public void onReceive(Context context, Intent intent) { BluetoothAdapter.ERROR); switch (state) { case BluetoothAdapter.STATE_OFF: - sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.OFF).build().toByteArray()); + sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.OFF) + .build().toByteArray()); break; case BluetoothAdapter.STATE_TURNING_OFF: - sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.TURNING_OFF).build().toByteArray()); + sink.success(Protos.BluetoothState.newBuilder() + .setState(Protos.BluetoothState.State.TURNING_OFF).build().toByteArray()); break; case BluetoothAdapter.STATE_ON: - sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.ON).build().toByteArray()); + sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.ON) + .build().toByteArray()); break; case BluetoothAdapter.STATE_TURNING_ON: - sink.success(Protos.BluetoothState.newBuilder().setState(Protos.BluetoothState.State.TURNING_ON).build().toByteArray()); + sink.success(Protos.BluetoothState.newBuilder() + .setState(Protos.BluetoothState.State.TURNING_ON).build().toByteArray()); break; } } @@ -759,14 +820,16 @@ private void stopScan() { @TargetApi(21) private ScanCallback getScanCallback21() { - if(scanCallback21 == null){ + if (scanCallback21 == null) { scanCallback21 = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); - if (!allowDuplicates && result != null && result.getDevice() != null && result.getDevice().getAddress() != null) { - if (macDeviceScanned.contains(result.getDevice().getAddress())) return; + if (!allowDuplicates && result != null && result.getDevice() != null + && result.getDevice().getAddress() != null) { + if (macDeviceScanned.contains(result.getDevice().getAddress())) + return; macDeviceScanned.add(result.getDevice().getAddress()); } try { @@ -798,11 +861,12 @@ public void onScanFailed(int errorCode) { @TargetApi(21) private void startScan21(Protos.ScanSettings proto) throws IllegalStateException { BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - if(scanner == null) throw new IllegalStateException("getBluetoothLeScanner() is null. Is the Adapter on?"); + if (scanner == null) + throw new IllegalStateException("getBluetoothLeScanner() is null. Is the Adapter on?"); int scanMode = proto.getAndroidScanMode(); int count = proto.getServiceUuidsCount(); List filters = new ArrayList<>(count); - for(int i = 0; i < count; i++) { + for (int i = 0; i < count; i++) { String uuid = proto.getServiceUuids(i); ScanFilter f = new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(uuid)).build(); filters.add(f); @@ -814,19 +878,21 @@ private void startScan21(Protos.ScanSettings proto) throws IllegalStateException @TargetApi(21) private void stopScan21() { BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); - if(scanner != null) scanner.stopScan(getScanCallback21()); + if (scanner != null) + scanner.stopScan(getScanCallback21()); } private BluetoothAdapter.LeScanCallback scanCallback18; private BluetoothAdapter.LeScanCallback getScanCallback18() { - if(scanCallback18 == null) { + if (scanCallback18 == null) { scanCallback18 = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice bluetoothDevice, int rssi, - byte[] scanRecord) { + byte[] scanRecord) { if (!allowDuplicates && bluetoothDevice != null && bluetoothDevice.getAddress() != null) { - if (macDeviceScanned.contains(bluetoothDevice.getAddress())) return; + if (macDeviceScanned.contains(bluetoothDevice.getAddress())) + return; macDeviceScanned.add(bluetoothDevice.getAddress()); } @@ -841,12 +907,13 @@ public void onLeScan(final BluetoothDevice bluetoothDevice, int rssi, private void startScan18(Protos.ScanSettings proto) throws IllegalStateException { List serviceUuids = proto.getServiceUuidsList(); UUID[] uuids = new UUID[serviceUuids.size()]; - for(int i = 0; i < serviceUuids.size(); i++) { + for (int i = 0; i < serviceUuids.size(); i++) { uuids[i] = UUID.fromString(serviceUuids.get(i)); } boolean success = mBluetoothAdapter.startLeScan(uuids, getScanCallback18()); - if(!success) throw new IllegalStateException("getBluetoothLeScanner() is null. Is the Adapter on?"); + if (!success) + throw new IllegalStateException("getBluetoothLeScanner() is null. Is the Adapter on?"); } private void stopScan18() { @@ -857,9 +924,9 @@ private void stopScan18() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { log(LogLevel.DEBUG, "[onConnectionStateChange] status: " + status + " newState: " + newState); - - if(newState == BluetoothProfile.STATE_DISCONNECTED) { - if(!mDevices.containsKey(gatt.getDevice().getAddress())) { + + if (newState == BluetoothProfile.STATE_DISCONNECTED) { + if (!mDevices.containsKey(gatt.getDevice().getAddress())) { gatt.close(); } } @@ -871,7 +938,7 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) { log(LogLevel.DEBUG, "[onServicesDiscovered] count: " + gatt.getServices().size() + " status: " + status); Protos.DiscoverServicesResult.Builder p = Protos.DiscoverServicesResult.newBuilder(); p.setRemoteId(gatt.getDevice().getAddress()); - for(BluetoothGattService s : gatt.getServices()) { + for (BluetoothGattService s : gatt.getServices()) { p.addServices(ProtoMaker.from(gatt.getDevice(), s, gatt)); } invokeMethodUIThread("DiscoverServicesResult", p.build().toByteArray()); @@ -879,7 +946,8 @@ public void onServicesDiscovered(BluetoothGatt gatt, int status) { @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - log(LogLevel.DEBUG, "[onCharacteristicRead] uuid: " + characteristic.getUuid().toString() + " status: " + status); + log(LogLevel.DEBUG, + "[onCharacteristicRead] uuid: " + characteristic.getUuid().toString() + " status: " + status); Protos.ReadCharacteristicResponse.Builder p = Protos.ReadCharacteristicResponse.newBuilder(); p.setRemoteId(gatt.getDevice().getAddress()); p.setCharacteristic(ProtoMaker.from(gatt.getDevice(), characteristic, gatt)); @@ -888,7 +956,8 @@ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - log(LogLevel.DEBUG, "[onCharacteristicWrite] uuid: " + characteristic.getUuid().toString() + " status: " + status); + log(LogLevel.DEBUG, + "[onCharacteristicWrite] uuid: " + characteristic.getUuid().toString() + " status: " + status); Protos.WriteCharacteristicRequest.Builder request = Protos.WriteCharacteristicRequest.newBuilder(); request.setRemoteId(gatt.getDevice().getAddress()); request.setCharacteristicUuid(characteristic.getUuid().toString()); @@ -916,13 +985,13 @@ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descrip q.setRemoteId(gatt.getDevice().getAddress()); q.setCharacteristicUuid(descriptor.getCharacteristic().getUuid().toString()); q.setDescriptorUuid(descriptor.getUuid().toString()); - if(descriptor.getCharacteristic().getService().getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY) { + if (descriptor.getCharacteristic().getService().getType() == BluetoothGattService.SERVICE_TYPE_PRIMARY) { q.setServiceUuid(descriptor.getCharacteristic().getService().getUuid().toString()); } else { // Reverse search to find service - for(BluetoothGattService s : gatt.getServices()) { - for(BluetoothGattService ss : s.getIncludedServices()) { - if(ss.getUuid().equals(descriptor.getCharacteristic().getService().getUuid())){ + for (BluetoothGattService s : gatt.getServices()) { + for (BluetoothGattService ss : s.getIncludedServices()) { + if (ss.getUuid().equals(descriptor.getCharacteristic().getService().getUuid())) { q.setServiceUuid(s.getUuid().toString()); q.setSecondaryServiceUuid(ss.getUuid().toString()); break; @@ -949,7 +1018,7 @@ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descri p.setSuccess(status == BluetoothGatt.GATT_SUCCESS); invokeMethodUIThread("WriteDescriptorResponse", p.build().toByteArray()); - if(descriptor.getUuid().compareTo(CCCD_ID) == 0) { + if (descriptor.getUuid().compareTo(CCCD_ID) == 0) { // SetNotificationResponse Protos.SetNotificationResponse.Builder q = Protos.SetNotificationResponse.newBuilder(); q.setRemoteId(gatt.getDevice().getAddress()); @@ -971,8 +1040,8 @@ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { log(LogLevel.DEBUG, "[onMtuChanged] mtu: " + mtu + " status: " + status); - if(status == BluetoothGatt.GATT_SUCCESS) { - if(mDevices.containsKey(gatt.getDevice().getAddress())) { + if (status == BluetoothGatt.GATT_SUCCESS) { + if (mDevices.containsKey(gatt.getDevice().getAddress())) { BluetoothDeviceCache cache = mDevices.get(gatt.getDevice().getAddress()); if (cache != null) { cache.mtu = mtu; @@ -986,36 +1055,36 @@ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { } }; - enum LogLevel - { + enum LogLevel { EMERGENCY, ALERT, CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG } private void log(LogLevel level, String message) { - if(level.ordinal() <= logLevel.ordinal()) { + if (level.ordinal() <= logLevel.ordinal()) { Log.d(TAG, message); } } - private void invokeMethodUIThread(final String name, final byte[] byteArray) - { - // this activity should never be null, but there may exist a timing where a method is invoked before the plugin is setup + private void invokeMethodUIThread(final String name, final byte[] byteArray) { + // this activity should never be null, but there may exist a timing where a + // method is invoked before the plugin is setup // if the activity is null this may lead to a future not timing out properly. if (activity != null) { activity.runOnUiThread( - new Runnable() { - @Override - public void run() { - if (channel != null) { - channel.invokeMethod(name, byteArray); + new Runnable() { + @Override + public void run() { + if (channel != null) { + channel.invokeMethod(name, byteArray); + } } - } - }); + }); } - + } - // BluetoothDeviceCache contains any other cached information not stored in Android Bluetooth API + // BluetoothDeviceCache contains any other cached information not stored in + // Android Bluetooth API // but still needed Dart side. static class BluetoothDeviceCache { final BluetoothGatt gatt; diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a0cbfcb58..2802ba0f5 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,34 +1,41 @@ - + + + + + + + + + + - + android:windowSoftInputMode="adjustResize" + android:exported="true"> + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + - - + - + \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 4bb2b5939..c905e7933 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -66,7 +66,7 @@ Future requestBluetoothPermission() async { // Requesting either scan or connect will request both if I'm not mistaken final results = await [ Permission.bluetoothScan, - Permission.bluetoothConnect + Permission.bluetoothConnect, ].request(); return results.values @@ -79,10 +79,17 @@ Future requestBluetoothPermission() async { return Permission.bluetooth.request(); } -class RequestPermissionsScreen extends StatelessWidget { +class RequestPermissionsScreen extends StatefulWidget { const RequestPermissionsScreen({Key key}) : super(key: key); @override + State createState() => + _RequestPermissionsScreenState(); +} + +class _RequestPermissionsScreenState extends State { + @override + Widget build(BuildContext context) { return FutureBuilder( future: bluetoothPermissionStatus(), @@ -118,6 +125,8 @@ class RequestPermissionsScreen extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Permission $status')), ); + } else { + setState(() {}); } }, ),