Skip to content
This repository has been archived by the owner on Feb 15, 2024. It is now read-only.

Commit

Permalink
Supporting Android 12 Bluetooth permissions.
Browse files Browse the repository at this point in the history
  • Loading branch information
espresso3389 authored and feelsoftware committed Apr 20, 2022
1 parent 4f0e2c3 commit c839714
Show file tree
Hide file tree
Showing 8 changed files with 496 additions and 448 deletions.
20 changes: 10 additions & 10 deletions .metadata
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: f53b32eb2317ba09137969999d130c24a6314997
channel: master

project_type: plugin
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: f53b32eb2317ba09137969999d130c24a6314997
channel: master
project_type: plugin
130 changes: 73 additions & 57 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,58 +1,74 @@
group 'com.pauldemarco.flutter_blue'
version '1.0'

buildscript {
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15'
}
}

rootProject.allprojects {
repositories {
google()
jcenter()
}
}

apply plugin: 'com.android.library'
apply plugin: 'com.google.protobuf'

android {
compileSdkVersion 30

defaultConfig {
minSdkVersion 19
}
sourceSets {
main {
proto {
srcDir '../protos'
}
}
}
}

protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.11.4'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}

dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.11.4'
group 'com.pauldemarco.flutter_blue'
version '1.0'

buildscript {
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.15'
}
}

rootProject.allprojects {
repositories {
google()
jcenter()
}
}

apply plugin: 'com.android.library'
apply plugin: 'com.google.protobuf'

android {
compileSdkVersion 31

defaultConfig {
minSdkVersion 19
targetSdkVersion 31
// Use Java 8 language features and APIs
// https://developer.android.com/studio/write/java8-support
multiDexEnabled true
}

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'
}
}
}
}

protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.11.4'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}

dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.google.protobuf:protobuf-javalite:3.11.4'
}
21 changes: 15 additions & 6 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pauldemarco.flutter_blue">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
</manifest>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.pauldemarco.flutter_blue">
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions
-->
<!-- Include "neverForLocation" only if you can strongly assert that
your app never derives physical location from Bluetooth scan results. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCa
private Application application;
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<String, BluetoothDeviceCache> mDevices = new HashMap<>();
private LogLevel logLevel = LogLevel.EMERGENCY;

private interface OperationOnPermission {
public void op(boolean granted, String permission);
}

private int lastEventId = 1452;
private Map<Integer, OperationOnPermission> operationsOnPermission = new HashMap<Integer, OperationOnPermission>();

// Pending call and result for startScan, in the case where permissions are needed
private MethodCall pendingCall;
private Result pendingResult;
private ArrayList<String> macDeviceScanned = new ArrayList<>();
private boolean allowDuplicates = false;

Expand Down Expand Up @@ -238,19 +241,13 @@ public void onMethodCall(MethodCall call, Result result) {

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);
ensurePermissionBeforeAction(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.ACCESS_FINE_LOCATION, (granted, permission) -> {
if (granted)
startScan(call, result);
else
result.error(
"no_permissions", String.format("flutter_blue plugin requires %s for scanning", permission), null);
});
break;
}

Expand All @@ -263,55 +260,69 @@ public void onMethodCall(MethodCall call, Result result) {

case "getConnectedDevices":
{
List<BluetoothDevice> 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());
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<BluetoothDevice> 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);
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;
}
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 device is already connected, return error
if(mDevices.containsKey(deviceId) && isConnected) {
result.error("already_connected", "connection with device already exists", null);
return;
}
// If device is already connected, return error
if(mDevices.containsKey(deviceId) && isConnected) {
result.error("already_connected", "connection with device already exists", null);
return;
}

// If device was connected to previously but is now disconnected, attempt a reconnect
if(mDevices.containsKey(deviceId) && !isConnected) {
if(mDevices.get(deviceId).gatt.connect()){
result.success(null);
} else {
result.error("reconnect_error", "error when reconnecting to device", null);
// If device was connected to previously but is now disconnected, attempt a reconnect
if(mDevices.containsKey(deviceId) && !isConnected) {
if(mDevices.get(deviceId).gatt.connect()){
result.success(null);
} else {
result.error("reconnect_error", "error when reconnecting to device", null);
}
return;
}
return;
}

// 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);
// 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);
});
break;
}

Expand Down Expand Up @@ -635,18 +646,31 @@ public void onMethodCall(MethodCall call, Result result) {
}
}

void ensurePermissionBeforeAction(String permissionA12, String permission, OperationOnPermission operation) {
permission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? permissionA12 : permission;
if (permission != null && ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
operationsOnPermission.put(lastEventId, (granted, perm) -> {
operationsOnPermission.remove(lastEventId);
operation.op(granted, perm);
});
ActivityCompat.requestPermissions(
activityBinding.getActivity(),
new String[] {
permission
},
lastEventId);
lastEventId++;
} else {
operation.op(true, permission);
}
}

@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;
Expand Down
4 changes: 2 additions & 2 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 30
compileSdkVersion 31

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.pauldemarco.flutter_blue_example"
minSdkVersion 19
targetSdkVersion 30
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Expand Down

0 comments on commit c839714

Please sign in to comment.