diff --git a/android/build.gradle b/android/build.gradle index 082a0fea..5efe38d2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,7 +18,7 @@ rootProject.allprojects { } apply plugin: 'com.android.library' android { - compileSdkVersion 30 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 3b5ff2f0..70f0eb8d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,12 @@ - - - + + + + + + + diff --git a/android/src/main/java/io/github/edufolly/flutterbluetoothserial/FlutterBluetoothSerialPlugin.java b/android/src/main/java/io/github/edufolly/flutterbluetoothserial/FlutterBluetoothSerialPlugin.java index 0b9686b4..00dc300b 100644 --- a/android/src/main/java/io/github/edufolly/flutterbluetoothserial/FlutterBluetoothSerialPlugin.java +++ b/android/src/main/java/io/github/edufolly/flutterbluetoothserial/FlutterBluetoothSerialPlugin.java @@ -6,6 +6,7 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothClass; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -16,6 +17,7 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import android.os.Build; import android.util.Log; import android.util.SparseArray; import android.os.AsyncTask; @@ -268,7 +270,7 @@ public void onReceive(Context context, Intent intent) { switch (action) { case BluetoothDevice.ACTION_FOUND: final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - //final BluetoothClass deviceClass = intent.getParcelableExtra(BluetoothDevice.EXTRA_CLASS); // @TODO . !BluetoothClass! + // final BluetoothClass deviceClass = intent.getParcelableExtra(BluetoothDevice.EXTRA_CLASS); // @TODO . !BluetoothClass! //final String extraName = intent.getStringExtra(BluetoothDevice.EXTRA_NAME); // @TODO ? !EXTRA_NAME! final int deviceRSSI = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); @@ -276,12 +278,13 @@ public void onReceive(Context context, Intent intent) { discoveryResult.put("address", device.getAddress()); discoveryResult.put("name", device.getName()); discoveryResult.put("type", device.getType()); - //discoveryResult.put("class", deviceClass); // @TODO . it isn't my priority for now !BluetoothClass! + // discoveryResult.put("class", deviceClass); // @TODO . it isn't my priority for now !BluetoothClass! discoveryResult.put("isConnected", checkIsDeviceConnected(device)); discoveryResult.put("bondState", device.getBondState()); discoveryResult.put("rssi", deviceRSSI); + discoveryResult.put("deviceClass", device.getBluetoothClass().getDeviceClass()); - Log.d(TAG, "Discovered " + device.getAddress()); + Log.d(TAG, "Discovered " + device.getAddress() + " (deviceClass: " + device.getBluetoothClass().getDeviceClass() + ")"); if (discoverySink != null) { discoverySink.success(discoveryResult); } @@ -444,15 +447,35 @@ private interface EnsurePermissionsCallback { EnsurePermissionsCallback pendingPermissionsEnsureCallbacks = null; private void ensurePermissions(EnsurePermissionsCallback callbacks) { - if ( + boolean permissionGranted = ( ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED - || ContextCompat.checkSelfPermission(activity, + == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { + == PackageManager.PERMISSION_GRANTED); + + String[] requestString = new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION}; + + Log.e(TAG,"request permission"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Log.e(TAG,"request scan permission"); + permissionGranted = ( + ContextCompat.checkSelfPermission(activity, + Manifest.permission.BLUETOOTH_SCAN) + == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(activity, + Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED); + requestString = new String[]{Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT}; + } + + if (!permissionGranted) { ActivityCompat.requestPermissions(activity, - new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION}, + requestString, REQUEST_COARSE_LOCATION_PERMISSIONS); pendingPermissionsEnsureCallbacks = callbacks; @@ -481,7 +504,6 @@ static private boolean checkIsDeviceConnected(BluetoothDevice device) { } } - /// Helper wrapper class for `BluetoothConnection` private class BluetoothConnectionWrapper extends BluetoothConnection { private final int id; @@ -549,6 +571,7 @@ protected void onDisconnected(boolean byRemote) { private class FlutterBluetoothSerialMethodCallHandler implements MethodCallHandler { /// Provides access to the plugin methods + @SuppressLint("MissingPermission") @Override public void onMethodCall(MethodCall call, Result result) { if (bluetoothAdapter == null) { @@ -580,9 +603,16 @@ public void onMethodCall(MethodCall call, Result result) { case "requestEnable": if (!bluetoothAdapter.isEnabled()) { - pendingResultForActivityResult = result; - Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - ActivityCompat.startActivityForResult(activity, intent, REQUEST_ENABLE_BLUETOOTH, null); + ensurePermissions(granted -> { + if (!granted) { + result.error("no_permissions", "Enabling bluetooth requires bluetooth permission", null); + return; + } + + pendingResultForActivityResult = result; + Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + ActivityCompat.startActivityForResult(activity, intent, REQUEST_ENABLE_BLUETOOTH, null); + }); } else { result.success(true); } @@ -590,8 +620,15 @@ public void onMethodCall(MethodCall call, Result result) { case "requestDisable": if (bluetoothAdapter.isEnabled()) { + ensurePermissions(granted -> { + if (!granted) { + result.error("no_permissions", "Enabling bluetooth requires bluetooth permission", null); + return; + } + bluetoothAdapter.disable(); result.success(true); + }); } else { result.success(false); } @@ -606,7 +643,11 @@ public void onMethodCall(MethodCall call, Result result) { break; case "getAddress": { - String address = bluetoothAdapter.getAddress(); + String address = "02:00:00:00:00:00"; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + address = bluetoothAdapter.getAddress(); + } if (address.equals("02:00:00:00:00:00")) { Log.w(TAG, "Local Bluetooth MAC address is hidden by system, trying other options..."); @@ -907,6 +948,8 @@ public void onReceive(Context context, Intent intent) { entry.put("type", device.getType()); entry.put("isConnected", checkIsDeviceConnected(device)); entry.put("bondState", BluetoothDevice.BOND_BONDED); + entry.put("deviceClass", device.getBluetoothClass().getDeviceClass()); + Log.d(TAG, "Discovered " + device.getAddress() + " (deviceClass: " + device.getBluetoothClass().getDeviceClass() + ")"); list.add(entry); } @@ -919,6 +962,7 @@ public void onReceive(Context context, Intent intent) { break; case "startDiscovery": + Log.d(TAG,"Starting discovery 22"); ensurePermissions(granted -> { if (!granted) { result.error("no_permissions", "discovering other devices requires location access permission", null); @@ -1067,4 +1111,4 @@ public void onReceive(Context context, Intent intent) { } } -} +} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4b7eba10..944c38e3 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 33 lintOptions { disable 'InvalidPackage' } @@ -23,7 +23,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.github.edufolly.flutterbluetoothserialexample" minSdkVersion 19 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index bae56f3b..c0a23034 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" + android:exported="true" android:windowSoftInputMode="adjustResize"> diff --git a/example/android/build.gradle b/example/android/build.gradle index c9e3db0a..cab8f601 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 53ae0ae4..d2964be7 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,9 @@ -android.enableJetifier=true +org.gradle.jvmargs=-Xmx1536M \ +--add-exports=java.base/sun.nio.ch=ALL-UNNAMED \ +--add-opens=java.base/java.lang=ALL-UNNAMED \ +--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \ +--add-opens=java.base/java.io=ALL-UNNAMED \ +--add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M +android.enableJetifier=true +android.enableR8=true \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index c732f9e0..7c8bab40 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/lib/BluetoothDeviceListEntry.dart b/example/lib/BluetoothDeviceListEntry.dart index a94afc07..9cb19004 100644 --- a/example/lib/BluetoothDeviceListEntry.dart +++ b/example/lib/BluetoothDeviceListEntry.dart @@ -12,8 +12,7 @@ class BluetoothDeviceListEntry extends ListTile { onTap: onTap, onLongPress: onLongPress, enabled: enabled, - leading: - Icon(Icons.devices), // @TODO . !BluetoothClass! class aware icon + leading: Icon(getIcon(device.deviceClass)), // @TODO . !BluetoothClass! class aware icon title: Text(device.name ?? ""), subtitle: Text(device.address.toString()), trailing: Row( @@ -44,6 +43,29 @@ class BluetoothDeviceListEntry extends ListTile { ), ); + static IconData getIcon(BluetoothDeviceClass deviceClass) { + switch (deviceClass) { + case BluetoothDeviceClass.PERIPHERAL_KEYBOARD: + return Icons.keyboard; + case BluetoothDeviceClass.AUDIO_VIDEO_WEARABLE_HEADSET: + return Icons.headphones; + case BluetoothDeviceClass.AUDIO_VIDEO_HEADPHONES: + return Icons.headphones; + case BluetoothDeviceClass.AUDIO_VIDEO_HANDSFREE: + return Icons.headphones; + case BluetoothDeviceClass.AUDIO_VIDEO_LOUDSPEAKER: + return Icons.speaker; + case BluetoothDeviceClass.PHONE_SMART: + return Icons.smartphone; + case BluetoothDeviceClass.COMPUTER_DESKTOP: + return Icons.computer; + case BluetoothDeviceClass.COMPUTER_LAPTOP: + return Icons.laptop; + default: + return Icons.devices; + } + } + static TextStyle _computeTextStyle(int rssi) { /**/ if (rssi >= -35) return TextStyle(color: Colors.greenAccent[700]); diff --git a/example/lib/SelectBondedDevicePage.dart b/example/lib/SelectBondedDevicePage.dart index 7acb033c..5ccbcf6d 100644 --- a/example/lib/SelectBondedDevicePage.dart +++ b/example/lib/SelectBondedDevicePage.dart @@ -17,7 +17,6 @@ class SelectBondedDevicePage extends StatefulWidget { } enum _DeviceAvailability { - no, maybe, yes, } @@ -27,7 +26,7 @@ class _DeviceWithAvailability { _DeviceAvailability availability; int? rssi; - _DeviceWithAvailability(this.device, this.availability, [this.rssi]); + _DeviceWithAvailability(this.device, this.availability); } class _SelectBondedDevicePage extends State { diff --git a/example/lib/helpers/LineChart.dart b/example/lib/helpers/LineChart.dart index a81bf0dd..8ca30fde 100644 --- a/example/lib/helpers/LineChart.dart +++ b/example/lib/helpers/LineChart.dart @@ -162,9 +162,9 @@ class LineChart extends StatelessWidget { values: values, valuesLabels: valuesLabels, horizontalLabelsTextStyle: - horizontalLabelsTextStyle ?? Theme.of(context).textTheme.caption, + horizontalLabelsTextStyle ?? Theme.of(context).textTheme.bodySmall, verticalLabelsTextStyle: - verticalLabelsTextStyle ?? Theme.of(context).textTheme.caption, + verticalLabelsTextStyle ?? Theme.of(context).textTheme.bodySmall, horizontalLinesPaint: horizontalLinesPaint, verticalLinesPaint: verticalLinesPaint, additionalMinimalHorizontalLabelsInterval: @@ -510,7 +510,7 @@ class _LineChartPainter extends CustomPainter { Iterator argument = arguments.iterator; while (value.moveNext()) { argument.moveNext(); - if (value.current == null || value.current == double.nan) continue; + if (value.current == null) continue; if (argument.current < argumentsOffset) continue; final double xOffset = padding.left + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9c0ab0ce..9074c8d4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,8 +1,6 @@ name: flutter_bluetooth_serial_example version: 0.4.0 description: Demonstrates how to use the `flutter_bluetooth_serial` plugin. -authors: - - Patryk Ludwikowski homepage: https://github.com/edufolly/flutter_bluetooth_serial/tree/master/example/ environment: diff --git a/lib/BluetoothConnection.dart b/lib/BluetoothConnection.dart index 5ffffb25..82a77281 100644 --- a/lib/BluetoothConnection.dart +++ b/lib/BluetoothConnection.dart @@ -17,6 +17,7 @@ class BluetoothConnection { // /// This ID identifies real full `BluetoothConenction` object on platform side code. + // ignore: unused_field final int? _id; final EventChannel _readChannel; diff --git a/lib/BluetoothDevice.dart b/lib/BluetoothDevice.dart index 5c6e527d..339b7333 100644 --- a/lib/BluetoothDevice.dart +++ b/lib/BluetoothDevice.dart @@ -12,7 +12,7 @@ class BluetoothDevice { final BluetoothDeviceType type; /// Class of the device. - //final BluetoothClass bluetoothClass // @TODO . !BluetoothClass! + //final category category // @TODO . !category! /// Describes is device connected. final bool isConnected; @@ -20,6 +20,9 @@ class BluetoothDevice { /// Bonding state of the device. final BluetoothBondState bondState; + /// Broadcasted friendly clas of the device. + final BluetoothDeviceClass deviceClass; + /// Tells whether the device is bonded (ready to secure connect). @Deprecated('Use `isBonded` instead') bool get bonded => bondState.isBonded; @@ -34,6 +37,7 @@ class BluetoothDevice { this.type = BluetoothDeviceType.unknown, this.isConnected = false, this.bondState = BluetoothBondState.unknown, + this.deviceClass = BluetoothDeviceClass.UNCATEGORIZED, }); /// Creates `BluetoothDevice` from map. @@ -41,16 +45,18 @@ class BluetoothDevice { /// Internally used to receive the object from platform code. factory BluetoothDevice.fromMap(Map map) { return BluetoothDevice( - name: map["name"], - address: map["address"]!, - type: map["type"] != null - ? BluetoothDeviceType.fromUnderlyingValue(map["type"]) - : BluetoothDeviceType.unknown, - isConnected: map["isConnected"] ?? false, - bondState: map["bondState"] != null - ? BluetoothBondState.fromUnderlyingValue(map["bondState"]) - : BluetoothBondState.unknown, - ); + name: map["name"], + address: map["address"]!, + type: map["type"] != null + ? BluetoothDeviceType.fromUnderlyingValue(map["type"]) + : BluetoothDeviceType.unknown, + isConnected: map["isConnected"] ?? false, + bondState: map["bondState"] != null + ? BluetoothBondState.fromUnderlyingValue(map["bondState"]) + : BluetoothBondState.unknown, + deviceClass: map["deviceClass"] != null + ? (map['deviceClass'] as int).getBluetoothDeviceClassFromValue + : BluetoothDeviceClass.UNCATEGORIZED); } /// Creates map from `BluetoothDevice`. @@ -60,6 +66,7 @@ class BluetoothDevice { "type": this.type.toUnderlyingValue(), "isConnected": this.isConnected, "bondState": this.bondState.toUnderlyingValue(), + 'deviceClass': this.deviceClass.value, }; /// Compares for equality of this and other `BluetoothDevice`. diff --git a/lib/BluetoothDeviceClass.dart b/lib/BluetoothDeviceClass.dart new file mode 100644 index 00000000..c6a57e91 --- /dev/null +++ b/lib/BluetoothDeviceClass.dart @@ -0,0 +1,99 @@ +part of flutter_bluetooth_serial; + +enum BluetoothDeviceClass { + // Audio and Video Devices + AUDIO_VIDEO_CAMCORDER(1076), + AUDIO_VIDEO_CAR_AUDIO(1056), + AUDIO_VIDEO_HANDSFREE(1032), + AUDIO_VIDEO_HEADPHONES(1048), + AUDIO_VIDEO_HIFI_AUDIO(1064), + AUDIO_VIDEO_LOUDSPEAKER(1044), + AUDIO_VIDEO_MICROPHONE(1040), + AUDIO_VIDEO_PORTABLE_AUDIO(1052), + AUDIO_VIDEO_SET_TOP_BOX(1060), + AUDIO_VIDEO_UNCATEGORIZED(1024), + AUDIO_VIDEO_VCR(1068), + AUDIO_VIDEO_VIDEO_CAMERA(1072), + AUDIO_VIDEO_VIDEO_CONFERENCING(1088), + AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER(1084), + AUDIO_VIDEO_VIDEO_GAMING_TOY(1096), + AUDIO_VIDEO_VIDEO_MONITOR(1080), + AUDIO_VIDEO_WEARABLE_HEADSET(1028), + + // Computer Devices + COMPUTER_DESKTOP(260), + COMPUTER_HANDHELD_PC_PDA(272), + COMPUTER_LAPTOP(268), + COMPUTER_PALM_SIZE_PC_PDA(276), + COMPUTER_SERVER(264), + COMPUTER_UNCATEGORIZED(256), + COMPUTER_WEARABLE(280), + + // Health Devices + HEALTH_BLOOD_PRESSURE(2308), + HEALTH_DATA_DISPLAY(2332), + HEALTH_GLUCOSE(2320), + HEALTH_PULSE_OXIMETER(2324), + HEALTH_PULSE_RATE(2328), + HEALTH_THERMOMETER(2312), + HEALTH_UNCATEGORIZED(2304), + HEALTH_WEIGHING(2316), + + // Peripheral Devices + PERIPHERAL_KEYBOARD(1344), + PERIPHERAL_KEYBOARD_POINTING(1472), + PERIPHERAL_NON_KEYBOARD_NON_POINTING(1280), + PERIPHERAL_POINTING(1408), + + // Phone Devices + PHONE_CELLULAR(516), + PHONE_CORDLESS(520), + PHONE_ISDN(532), + PHONE_MODEM_OR_GATEWAY(528), + PHONE_SMART(524), + PHONE_UNCATEGORIZED(512), + + // Toy Devices + TOY_CONTROLLER(2064), + TOY_DOLL_ACTION_FIGURE(2060), + TOY_GAME(2068), + TOY_ROBOT(2052), + TOY_UNCATEGORIZED(2048), + TOY_VEHICLE(2056), + + // Wearable Devices + WEARABLE_GLASSES(1812), + WEARABLE_HELMET(1808), + WEARABLE_JACKET(1804), + WEARABLE_PAGER(1800), + WEARABLE_UNCATEGORIZED(1792), + WEARABLE_WRIST_WATCH(1796), + + // Major Class Devices + IMAGING(1536), + MISC(0), + UNCATEGORIZED(7936), + NETWORKING(768); + + const BluetoothDeviceClass(this.value); + + final int value; +} + +extension BluetoothDeviceClassEnum on int { + BluetoothDeviceClass get getBluetoothDeviceClassFromValue { + for (BluetoothDeviceClass enumValue in BluetoothDeviceClass.values) { + if (enumValue.value == this) { + return enumValue; + } + } + // Handle the case where the integer value doesn't match any enum value. + // Default value or handle as needed. + return BluetoothDeviceClass.UNCATEGORIZED; + } +} + +extension BluetoothDeviceClassName on BluetoothDeviceClass { + // Get readable name of the bluetooth class + String get name => this.toString().replaceAll('BluetoothDeviceClass.', ''); +} diff --git a/lib/flutter_bluetooth_serial.dart b/lib/flutter_bluetooth_serial.dart index d3eefdc5..1c84cab3 100644 --- a/lib/flutter_bluetooth_serial.dart +++ b/lib/flutter_bluetooth_serial.dart @@ -1,7 +1,6 @@ library flutter_bluetooth_serial; import 'dart:async'; -import 'dart:typed_data'; import 'dart:convert'; import 'package:flutter/services.dart'; @@ -14,3 +13,4 @@ part './BluetoothPairingRequest.dart'; part './BluetoothDiscoveryResult.dart'; part './BluetoothConnection.dart'; part './FlutterBluetoothSerial.dart'; +part './BluetoothDeviceClass.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 5d0a7195..2790fd80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_bluetooth_serial -version: 0.4.0 +version: 0.4.1 description: Flutter basic implementation for Classical Bluetooth (only RFCOMM for now). homepage: https://github.com/edufolly/flutter_bluetooth_serial repository: https://github.com/edufolly/flutter_bluetooth_serial issue_tracker: https://github.com/edufolly/flutter_bluetooth_serial/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' flutter: ">=1.17.0" dependencies: