diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db5021..9dac7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 +Upgraded android sdk to 33. +Added permission check on enableBluetooth function. + ## 1.0.0 Stable release including the changes noted in the beta releases. This release also updates Android dependencies. diff --git a/analysis_options.yaml b/analysis_options.yaml index 17d6dbf..5a1ed56 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ -include: package:lint/analysis_options_package.yaml \ No newline at end of file +include: package:lint/analysis_options_package.yaml + +linter: + rules: + unnecessary_import: false \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 90062d0..c61f625 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } diff --git a/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralManager.kt b/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralManager.kt index a850088..77c1249 100644 --- a/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralManager.kt +++ b/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralManager.kt @@ -6,10 +6,12 @@ package dev.steenbakker.flutter_ble_peripheral +import android.Manifest import android.bluetooth.* import android.bluetooth.le.* import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat @@ -22,11 +24,15 @@ import io.flutter.plugin.common.MethodChannel class FlutterBlePeripheralManager(context: Context) { + companion object { + const val REQUEST_ENABLE_BT = 4 + const val REQUEST_PERMISSION_BT = 8 + } + var mBluetoothManager: BluetoothManager? var mBluetoothLeAdvertiser: BluetoothLeAdvertiser? = null var pendingResultForActivityResult: MethodChannel.Result? = null - val requestEnableBt = 4 //TODO // private lateinit var mBluetoothGattServer: BluetoothGattServer @@ -39,28 +45,76 @@ class FlutterBlePeripheralManager(context: Context) { mBluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager } + // Permissions for Bluetooth API > 31 + @RequiresApi(Build.VERSION_CODES.S) + private fun hasBluetoothAdvertisePermission(context: Context): Boolean { + return (context.checkSelfPermission( + Manifest.permission.BLUETOOTH_ADVERTISE + ) + == PackageManager.PERMISSION_GRANTED) + } + + @RequiresApi(Build.VERSION_CODES.S) + private fun hasBluetoothConnectPermission(context: Context): Boolean { + return (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) + == PackageManager.PERMISSION_GRANTED) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun hasLocationFinePermission(context: Context): Boolean { + return (context.checkSelfPermission( + Manifest.permission.ACCESS_FINE_LOCATION + ) + == PackageManager.PERMISSION_GRANTED) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun hasLocationCoarsePermission(context: Context): Boolean { + return (context.checkSelfPermission( + Manifest.permission.ACCESS_COARSE_LOCATION + ) + == PackageManager.PERMISSION_GRANTED) + } + /** * Enables bluetooth with a dialog or without. */ - fun enableBluetooth(call: MethodCall, result: MethodChannel.Result, activityBinding: ActivityPluginBinding) { + fun checkAndEnableBluetooth(call: MethodCall, result: MethodChannel.Result, activityBinding: ActivityPluginBinding) { if (mBluetoothManager!!.adapter.isEnabled) { result.success(true) } else { - if (call.arguments as Boolean) { - pendingResultForActivityResult = result - val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - ActivityCompat.startActivityForResult( - activityBinding.activity, - intent, - requestEnableBt, - null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && (!hasBluetoothAdvertisePermission(activityBinding.activity) || !hasBluetoothConnectPermission(activityBinding.activity))) { + ActivityCompat.requestPermissions(activityBinding.activity, + arrayOf(Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_ADVERTISE), + REQUEST_PERMISSION_BT + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.S && (!hasLocationCoarsePermission(activityBinding.activity) || !hasLocationFinePermission(activityBinding.activity))) { + ActivityCompat.requestPermissions(activityBinding.activity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), + REQUEST_PERMISSION_BT ) } else { - mBluetoothManager!!.adapter.enable() + enableBluetooth(call, result, activityBinding) } } } + @Suppress("deprecation") + fun enableBluetooth(call: MethodCall, result: MethodChannel.Result, activityBinding: ActivityPluginBinding) { + if (call.arguments as Boolean) { + pendingResultForActivityResult = result + val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + ActivityCompat.startActivityForResult( + activityBinding.activity, + intent, + REQUEST_ENABLE_BT, + null + ) + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU){ + mBluetoothManager!!.adapter.enable() + } + } + /** * Start advertising using the startAdvertising() method. */ @@ -81,7 +135,6 @@ class FlutterBlePeripheralManager(context: Context) { @RequiresApi(Build.VERSION_CODES.O) fun startSet(advertiseData: AdvertiseData, advertiseSettingsSet: AdvertisingSetParameters, peripheralResponse: AdvertiseData?, periodicResponse: AdvertiseData?, periodicResponseSettings: PeriodicAdvertisingParameters?, maxExtendedAdvertisingEvents: Int = 0, duration: Int = 0, mAdvertiseSetCallback: PeripheralAdvertisingSetCallback) { - mBluetoothLeAdvertiser!!.startAdvertisingSet( advertiseSettingsSet, advertiseData, diff --git a/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralPlugin.kt b/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralPlugin.kt index eea9294..3305c95 100644 --- a/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/dev/steenbakker/flutter_ble_peripheral/FlutterBlePeripheralPlugin.kt @@ -18,8 +18,6 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.ParcelUuid -import androidx.annotation.NonNull -import androidx.annotation.RequiresApi import dev.steenbakker.flutter_ble_peripheral.callbacks.PeripheralAdvertisingCallback import dev.steenbakker.flutter_ble_peripheral.callbacks.PeripheralAdvertisingSetCallback import dev.steenbakker.flutter_ble_peripheral.exceptions.PeripheralException @@ -32,21 +30,22 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry import java.util.* -class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { +class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.RequestPermissionsResultListener { private var methodChannel: MethodChannel? = null private val tag: String = "flutter_ble_peripheral" private var flutterBlePeripheralManager: FlutterBlePeripheralManager? = null private lateinit var stateChangedHandler: StateChangedHandler -// private lateinit var mtuChangedHandler: MtuChangedHandler -// private val dataReceivedHandler = DataReceivedHandler() private var context: Context? = null - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + private var activityBinding: ActivityPluginBinding? = null + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext methodChannel = MethodChannel( flutterPluginBinding.binaryMessenger, @@ -54,14 +53,13 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle ) methodChannel?.setMethodCallHandler(this) + stateChangedHandler = StateChangedHandler(flutterPluginBinding) stateChangedHandler.publishPeripheralState(PeripheralState.poweredOff) flutterBlePeripheralManager = FlutterBlePeripheralManager(flutterPluginBinding.applicationContext) -// mtuChangedHandler = MtuChangedHandler(flutterPluginBinding, flutterBlePeripheralManager!!) -// dataReceivedHandler.register(flutterPluginBinding, flutterBlePeripheralManager) } - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodChannel?.setMethodCallHandler(null) methodChannel = null flutterBlePeripheralManager = null @@ -69,7 +67,7 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle } - private fun checkBluetooth() { + private fun checkBluetoothState() { if (flutterBlePeripheralManager!!.mBluetoothManager == null) throw PeripheralException(PeripheralState.unsupported) // Can't check whether ble is turned off or not supported, see https://stackoverflow.com/questions/32092902/why-ismultipleadvertisementsupported-returns-false-when-getbluetoothleadverti @@ -78,37 +76,16 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle ?: throw PeripheralException(PeripheralState.poweredOff) } - private var activityBinding: ActivityPluginBinding? = null - -// private fun handlePeripheralException(e: PeripheralException, result: MethodChannel.Result?) { -// when (e.state) { -// PeripheralState.unsupported -> { -// stateChangedHandler.publishPeripheralState(PeripheralState.unsupported) -// Log.e(tag, "This device does not support bluetooth LE") -// result?.error("Not Supported", "This device does not support bluetooth LE", e.state.name) -// } -// PeripheralState.poweredOff -> { -// stateChangedHandler.publishPeripheralState(PeripheralState.poweredOff) -// Log.e(tag, "Bluetooth may be turned off") -// result?.error("Not powered", "Bluetooth may be turned off", e.state.name) -// } -// else -> { -// stateChangedHandler.publishPeripheralState(e.state) -// Log.e(tag, e.state.name) -// result?.error(e.state.name, null, null) -// } -// } -// } - private fun enableBluetooth(call: MethodCall, result: MethodChannel.Result) { if (activityBinding != null) { - flutterBlePeripheralManager!!.enableBluetooth(call, result, activityBinding!!) + this.call = call + this.pendingResultForActivityResult = result + flutterBlePeripheralManager!!.checkAndEnableBluetooth(call, result, activityBinding!!) } else { result.error("No activity", "FlutterBlePeripheral is not correctly initialized", "null") } } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (flutterBlePeripheralManager == null || context == null) { result.error("Not initialized", "FlutterBlePeripheral is not correctly initialized", "null") } @@ -116,7 +93,7 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle if (call.method == "enableBluetooth") { enableBluetooth(call, result) } else { - checkBluetooth() + checkBluetoothState() try { when (call.method) { @@ -153,7 +130,7 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle @Suppress("UNCHECKED_CAST") private fun startPeripheral(call: MethodCall, result: MethodChannel.Result) { - hasPermissions(context!!) +// hasPermissions(context!!) if (call.arguments !is Map<*, *>) { throw IllegalArgumentException("Arguments are not a map! " + call.arguments) @@ -281,12 +258,6 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle flutterBlePeripheralManager!!.start(advertiseData.build(), advertiseSettings.build(), advertiseResponseData?.build(), advertisingCallback!!) } - -// -// Handler(Looper.getMainLooper()).post { -// Log.i(tag, "Start advertise: $advertiseData") -// result.success(null) -// } } private var advertisingSetCallback: PeripheralAdvertisingSetCallback? = null @@ -338,78 +309,37 @@ class FlutterBlePeripheralPlugin : FlutterPlugin, MethodChannel.MethodCallHandle // } // } - private fun hasPermissions(context: Context): Boolean { - // Required for API > 31 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (!hasBluetoothAdvertisePermission(context)) { - throw PermissionNotFoundException("BLUETOOTH_ADVERTISE") - } -// if (!hasBluetoothConnectPermission(context)) { -// throw PermissionNotFoundException("BLUETOOTH_CONNECT") -// } -// if (!hasBluetoothScanPermission(context)) { -// throw PermissionNotFoundException("BLUETOOTH_SCAN") -// } - - // Required for API > 28 - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - if (!hasLocationFinePermission(context)) { - throw PermissionNotFoundException("ACCESS_FINE_LOCATION") - } + private var pendingResultForActivityResult: MethodChannel.Result? = null + private var call: MethodCall? = null + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + if (requestCode == FlutterBlePeripheralManager.REQUEST_PERMISSION_BT) { + for (i in permissions.indices) { + val permission = permissions[i] + val grantResult = grantResults[i] + if (permission == Manifest.permission.BLUETOOTH_CONNECT || permission == Manifest.permission.BLUETOOTH_ADVERTISE || permission == Manifest.permission.ACCESS_FINE_LOCATION || permission == Manifest.permission.ACCESS_COARSE_LOCATION) { + if (grantResult == PackageManager.PERMISSION_GRANTED) { + if (call != null && pendingResultForActivityResult != null && activityBinding != null) { + flutterBlePeripheralManager?.enableBluetooth(call!!, pendingResultForActivityResult!!, activityBinding!! ) + return true + } - // Required for API < 28 - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ - if (!hasLocationCoarsePermission(context)) { - throw PermissionNotFoundException("ACCESS_COARSE_LOCATION") + } + } } } - return true - - } - - // Permissions for Bluetooth API > 31 - @RequiresApi(Build.VERSION_CODES.S) - private fun hasBluetoothAdvertisePermission(context: Context): Boolean { - return (context.checkSelfPermission( - Manifest.permission.BLUETOOTH_ADVERTISE - ) - == PackageManager.PERMISSION_GRANTED) - } - - @RequiresApi(Build.VERSION_CODES.S) - private fun hasBluetoothConnectPermission(context: Context): Boolean { - return (context.checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) - == PackageManager.PERMISSION_GRANTED) - } - - @RequiresApi(Build.VERSION_CODES.S) - private fun hasBluetoothScanPermission(context: Context): Boolean { - return (context.checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) - == PackageManager.PERMISSION_GRANTED) + return false } - @RequiresApi(Build.VERSION_CODES.M) - private fun hasLocationFinePermission(context: Context): Boolean { - return (context.checkSelfPermission( - Manifest.permission.ACCESS_FINE_LOCATION - ) - == PackageManager.PERMISSION_GRANTED) - } - - @RequiresApi(Build.VERSION_CODES.M) - private fun hasLocationCoarsePermission(context: Context): Boolean { - return (context.checkSelfPermission( - Manifest.permission.ACCESS_COARSE_LOCATION - ) - == PackageManager.PERMISSION_GRANTED) - } - - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { + binding.addRequestPermissionsResultListener(this) binding.addActivityResultListener { requestCode, resultCode, _ -> when (requestCode) { - flutterBlePeripheralManager?.requestEnableBt -> { + FlutterBlePeripheralManager.REQUEST_ENABLE_BT -> { // @TODO - used underlying value of `Activity.RESULT_CANCELED` since we tend to use `androidx` in which I were not able to find the constant. if (flutterBlePeripheralManager?.pendingResultForActivityResult != null) { flutterBlePeripheralManager!!.pendingResultForActivityResult!!.success(resultCode == Activity.RESULT_OK) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 26128b9..96fc8a8 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,5 @@ -include: package:lint/analysis_options.yaml \ No newline at end of file +include: package:lint/analysis_options.yaml + +linter: + rules: + unnecessary_import: false \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e8516b5..487d1b9 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -36,7 +36,7 @@ android { defaultConfig { applicationId "dev.steenbakker.flutter_ble_peripheral_example" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 7159197..b57ff09 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Nov 25 10:37:01 CET 2021 +#Thu Oct 20 14:53:42 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/lib/src/flutter_ble_peripheral.dart b/lib/src/flutter_ble_peripheral.dart index 9493fdf..c661b6f 100644 --- a/lib/src/flutter_ble_peripheral.dart +++ b/lib/src/flutter_ble_peripheral.dart @@ -28,8 +28,8 @@ class FlutterBlePeripheral { FlutterBlePeripheral._internal(); /// Method Channel used to communicate state with - final MethodChannel _methodChannel = - const MethodChannel('dev.steenbakker.flutter_ble_peripheral/ble_state'); + static const MethodChannel _methodChannel = + MethodChannel('dev.steenbakker.flutter_ble_peripheral/ble_state'); /// Event Channel for MTU state final EventChannel _mtuChangedEventChannel = const EventChannel( @@ -148,6 +148,9 @@ class FlutterBlePeripheral { } /// Stop advertising + /// + /// [askUser] ONLY AVAILABLE ON ANDROID SDK < 33 + /// If set to false, it will enable bluetooth without asking user. Future enableBluetooth({bool askUser = true}) async { return await _methodChannel.invokeMethod( 'enableBluetooth', diff --git a/pubspec.yaml b/pubspec.yaml index bffeb97..63cc8f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_ble_peripheral description: This plugin enables a device to be set into peripheral mode, and advertise custom services and characteristics. -version: 1.0.0 +version: 1.1.0 homepage: https://github.com/juliansteenbakker/flutter_ble_peripheral environment: