Skip to content

Commit 5d7f930

Browse files
GuillaumeHugotGuillaume HugotVincentMasselis
authored
[FEAT] Scan & Connect: Handle new permission system on Android 12 (#12)
* [FEAT] overall : handle new permission system on Android 12 * [FEAT] MainActivity : ask permission separately � Conflicts: � dev-app/src/main/java/com/vincentmasselis/devapp/MainActivity.kt * Added comment * gradle cleanup * Updated permission management * Updated readme * Updated readme * Updated gradle version and doc * Updated maven release developer list * Updated javadoc of `rxScan` Co-authored-by: Guillaume Hugot <guillaume@equisense.fr> Co-authored-by: Vincent Masselis <vincent.masselis@gmail.com>
1 parent ba63d3f commit 5d7f930

File tree

17 files changed

+248
-128
lines changed

17 files changed

+248
-128
lines changed

README.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,21 @@ Made with love at the [Equisense](http://equisense.com) HQ. This library is used
1010

1111
Looking for BLE with Coroutines instead of RxJava ? Take a look at the [LouisCAD's implementation](https://github.com/Beepiz/BleGattCoroutines).
1212

13-
## ⚠️ Android 10 permissions changes ⚠️
14-
Starting from Android API 29, the coarse location permission is not required anymore, instead of this, you have to use the FINE permission location to scan over the bluetooth low energy. Before upgrading `targetSdkVersion` to 29 in your app, check your `requestPermission` calls according to this new permission.
13+
## ⚠️ Android 12 permissions changes ⚠️
14+
Starting from Android API 31, the fine location permission is not required anymore, instead of this, you have to use the BLUETOOTH_CONNECT and BLUETOOTH_SCAN permissions when dealing the bluetooth low energy framework. Before upgrading `targetSdkVersion` to 31 in your app, check your `requestPermission` calls according to this new permission.
1515

16-
Because of this change, RxBluetoothKotlin was updated to fire the `NeedLocationPermission` exception at the right moment when the fine location permission is missing starting from the release `1.2.2`.
17-
If you're targeting Android API 28 and less, the last supported release is 1.2.1, if you're targeting API 29 or more, you should use the last version of RxBluetoothKotlin.
16+
Because of this change, RxBluetoothKotlin was updated to fire the `NeedBluetoothScanPermission` and `NeedBluetoothConnectPermission` exceptions at the right moment if they're missing at the runtime. Theses exceptions are fired since the release `3.2.0`.
17+
18+
[Learn more](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions)
1819

1920
## ⚠️ Important notice about Maven Central release ⚠️
2021
**RxBluetoothKotlin is released on Maven Central** since the version `3.1.0` you don't have to worry about this library when jCenter will shutdown ! Unfortunately, according to the Maven Central policies, I must update my package to match with the host domain I own. I only own `masselis.com`, so the package name RxBluetothKotlin were renamed from `com.vincentmasselis.rxbluetoothkotlin` to `com.masselis.rxbluetoothkotlin`, as consequence, <ins>you have to renamed EVERY import from rxbluetoothkotlin to the new package name</ins>.
2122

2223
## TL/DR
2324

2425
```groovy
26+
// Check the github release section to find the latest available version
2527
implementation 'com.masselis.rxbluetoothkotlin:rxbluetoothkotlin-core:<<latest_version>>'
26-
// Add this to use the scanner
27-
implementation 'no.nordicsemi.android.support.v18:scanner:1.4.3'
28-
// RxBluetoothKotlin doesn't provides the RxJava dependecy, you have to add it yourself:
29-
implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
3028
```
3129

3230
### Scan
@@ -69,15 +67,20 @@ rxBluetoothGatt.disconnect().subscribe()
6967

7068
## Requirements
7169
* Min target API 18
72-
* A manifest with `<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />` and `<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />`
73-
* The user permission to access to the fine Location `requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_CODE_FINE_LOCATION)`
70+
* When <ins>scanning</ins> with RxBluetoothKotlin, you have to grant theses runtime permissions:
71+
- From Android 6 to 9 inclusive: `ACCESS_COARSE_LOCATION`
72+
- From Android 10 to 11 inclusive: `ACCESS_FINE_LOCATION`
73+
- From Android 12: `BLUETOOTH_SCAN`
74+
* When <ins>connecting</ins> with RxBluetoothKotlin, you have to grant this runtime permission:
75+
- From Android 12: `BLUETOOTH_CONNECT`
7476
* A turned on bluetooth chip `(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter.isEnabled`
77+
* You can add to your manifest `<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />`
7578

7679
## Logging
77-
When scanning with `rxScan()` or connecting with `connectRxGatt()`, you can set the `logger` parameter. By setting it, RxBluetoothKotlin will produce a log for every bluetooth input, output, starting scan, error thrown, etc.. I recommend to set it for debugging purproses and/or if you're not familiar with the Android BLE API. It could helps you a lot to understand what's going on between your app and the Bluetooth Low Energy device.
80+
When scanning with `rxScan()` or connecting with `connectRxGatt()`, you can set the `logger` parameter. By setting it, RxBluetoothKotlin will produce a log for every bluetooth input, output, starting scan, error thrown, etc.. I recommend to set it for debugging purposes and/or if you're not familiar with the Android BLE API. It could helps you a lot to understand what's going on between your app and the Bluetooth Low Energy device.
7881

7982
## Error management
80-
Interact with Bluetooth Low Energy devices on Android is **hard**. The BLE specs uses unfamiliars concepts, the BLE API from Android could fails at any moment and some exceptions are silent. Because of this, a basic method like `write(BluetoothGattCharacteristic)` could fails for 5 differents reasons. It becomes harder if you are chaining this call with other calls, this sum up to a thrown exception when it's impossible to known which call fails and why.
83+
Interact with Bluetooth Low Energy devices on Android is **hard**. The BLE specs uses unfamiliar concepts, the BLE API from Android could fails at any moment and some exceptions are silent. Because of this, a basic method like `write(BluetoothGattCharacteristic)` could fails for 5 different reasons. It becomes harder if you are chaining this call with other calls, this sum up to a thrown exception when it's impossible to known which call fails and why.
8184

8285
For this reason, every public method from this library is documented with every exceptions which could be fired and most of the exception are unique. For example: `write(BluetoothGattCharacteristic)` could fire:
8386
* Unique `CharacteristicWriteDeviceDisconnected` if the device disconnects while writing

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
buildscript {
44
ext.kotlin_version = '1.5.31'
5+
ext.compile_sdk_version = 31
6+
ext.target_sdk_version = 31
7+
ext.build_tool_version = "31.0.0"
58
repositories {
69
google()
710
mavenCentral()
8-
jcenter()
911
}
1012
dependencies {
1113
classpath 'com.android.tools.build:gradle:7.0.3'

demo-app/build.gradle

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ apply plugin: 'com.android.application'
22
apply plugin: 'kotlin-android'
33

44
android {
5-
compileSdkVersion 31
5+
compileSdkVersion compile_sdk_version
6+
buildToolsVersion build_tool_version
7+
68
defaultConfig {
79
applicationId "com.masselis.demoapp"
810
minSdkVersion 18
9-
targetSdkVersion 31
11+
targetSdkVersion target_sdk_version
12+
1013
versionCode 1
1114
versionName "1.0"
1215
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -31,13 +34,10 @@ android {
3134

3235
dependencies {
3336
implementation 'androidx.appcompat:appcompat:1.3.1'
34-
implementation 'androidx.core:core-ktx:1.6.0'
37+
implementation 'androidx.core:core-ktx:1.7.0'
3538
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
3639

37-
implementation 'com.vincentmasselis.rxbluetoothkotlin:rxbluetoothkotlin-core:3.0.0'
38-
implementation 'io.reactivex.rxjava3:rxjava:3.1.2'
39-
implementation 'no.nordicsemi.android.support.v18:scanner:1.4.3'
40-
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
40+
implementation project(":rxbluetoothkotlin-core")
4141

4242
// Helpers
4343
implementation 'androidx.recyclerview:recyclerview:1.2.1'

demo-app/src/main/java/com/vincentmasselis/demoapp/DeviceActivity.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package com.vincentmasselis.demoapp
22

3+
import android.Manifest.permission.BLUETOOTH_CONNECT
34
import android.annotation.SuppressLint
45
import android.bluetooth.BluetoothDevice
56
import android.content.Context
67
import android.content.Intent
8+
import android.os.Build
79
import android.os.Bundle
810
import android.view.View
911
import android.widget.Toast
12+
import androidx.activity.result.contract.ActivityResultContracts
13+
import androidx.annotation.RequiresApi
1014
import androidx.appcompat.app.AlertDialog
1115
import androidx.appcompat.app.AppCompatActivity
1216
import com.jakewharton.rxbinding4.view.clicks
17+
import com.masselis.rxbluetoothkotlin.*
1318
import com.vincentmasselis.demoapp.databinding.ActivityDeviceBinding
14-
import com.vincentmasselis.rxbluetoothkotlin.*
1519
import com.vincentmasselis.rxuikotlin.disposeOnState
1620
import com.vincentmasselis.rxuikotlin.utils.ActivityState
1721
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -24,9 +28,15 @@ class DeviceActivity : AppCompatActivity() {
2428

2529
private val device by lazy { intent.getParcelableExtra<BluetoothDevice>(DEVICE_EXTRA)!! }
2630

31+
private val permissionLauncher =
32+
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
33+
if (isGranted) states.onNext(States.Connecting)
34+
else finish()
35+
}
2736
private val states = BehaviorSubject.createDefault<States>(States.Connecting)
2837
private lateinit var binding: ActivityDeviceBinding
2938

39+
@RequiresApi(Build.VERSION_CODES.S)
3040
@SuppressLint("SetTextI18n")
3141
override fun onCreate(savedInstanceState: Bundle?) {
3242
super.onCreate(savedInstanceState)
@@ -53,10 +63,17 @@ class DeviceActivity : AppCompatActivity() {
5363
states
5464
.filter { it is States.Connecting }
5565
.switchMapSingle { device.connectRxGatt() }
66+
.onErrorComplete {
67+
if (it is NeedBluetoothConnectPermission) {
68+
permissionLauncher.launch(BLUETOOTH_CONNECT)
69+
true
70+
} else
71+
false
72+
}
5673
.switchMapMaybe { gatt -> gatt.whenConnectionIsReady().map { gatt } }
74+
.observeOn(AndroidSchedulers.mainThread())
5775
.doOnSubscribe { binding.connectingGroup.visibility = View.VISIBLE }
5876
.doFinally { binding.connectingGroup.visibility = View.INVISIBLE }
59-
.observeOn(AndroidSchedulers.mainThread())
6077
.subscribe(
6178
{
6279
Toast.makeText(this, "Connected !", Toast.LENGTH_SHORT).show()

demo-app/src/main/java/com/vincentmasselis/demoapp/ScanActivity.kt

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package com.vincentmasselis.demoapp
22

3-
import android.Manifest
3+
import android.Manifest.permission.*
44
import android.app.Activity
55
import android.bluetooth.BluetoothManager
66
import android.content.Intent
7-
import android.content.pm.PackageManager
7+
import android.content.pm.PackageManager.PERMISSION_GRANTED
88
import android.os.Build
99
import android.os.Bundle
1010
import android.provider.Settings
1111
import android.view.View
12+
import androidx.activity.result.contract.ActivityResultContracts
1213
import androidx.appcompat.app.AlertDialog
1314
import androidx.appcompat.app.AppCompatActivity
15+
import androidx.core.content.ContextCompat
1416
import androidx.recyclerview.widget.LinearLayoutManager
1517
import com.jakewharton.rxbinding4.view.clicks
18+
import com.masselis.rxbluetoothkotlin.*
1619
import com.vincentmasselis.demoapp.databinding.ActivityScanBinding
17-
import com.vincentmasselis.rxbluetoothkotlin.*
1820
import com.vincentmasselis.rxuikotlin.disposeOnState
1921
import com.vincentmasselis.rxuikotlin.utils.ActivityState
2022
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -26,10 +28,25 @@ class ScanActivity : AppCompatActivity() {
2628

2729
private var currentState = BehaviorSubject.createDefault<States>(States.NotScanning)
2830
private lateinit var binding: ActivityScanBinding
31+
private val permissionLauncher =
32+
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
33+
if (isGranted) startScan()
34+
}
35+
36+
private val forResultLauncher =
37+
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
38+
if (result.resultCode == Activity.RESULT_OK) startScan()
39+
}
2940

3041
override fun onCreate(savedInstanceState: Bundle?) {
3142
super.onCreate(savedInstanceState)
32-
setContentView(R.layout.activity_scan)
43+
binding = ActivityScanBinding.inflate(layoutInflater)
44+
setContentView(binding.root)
45+
46+
// To display the bluetooth device name I need this permission
47+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
48+
ContextCompat.checkSelfPermission(this, BLUETOOTH_CONNECT) != PERMISSION_GRANTED
49+
) permissionLauncher.launch(BLUETOOTH_CONNECT)
3350

3451
currentState
3552
.distinctUntilChanged()
@@ -76,25 +93,6 @@ class ScanActivity : AppCompatActivity() {
7693
super.onDestroy()
7794
}
7895

79-
80-
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
81-
super.onActivityResult(requestCode, resultCode, data)
82-
when (requestCode) {
83-
REQUEST_CODE_ENABLE_LOCATION -> if (resultCode == Activity.RESULT_OK) startScan()
84-
}
85-
}
86-
87-
override fun onRequestPermissionsResult(
88-
requestCode: Int,
89-
permissions: Array<out String>,
90-
grantResults: IntArray
91-
) {
92-
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
93-
when (requestCode) {
94-
PERMISSION_CODE_FINE_LOCATION -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) startScan()
95-
}
96-
}
97-
9896
private var scanDisp: Disposable? = null
9997
private fun startScan() {
10098
currentState.onNext(States.StartingScan)
@@ -107,19 +105,18 @@ class ScanActivity : AppCompatActivity() {
107105
}, {
108106
currentState.onNext(States.NotScanning)
109107
when (it) {
110-
is DeviceDoesNotSupportBluetooth -> AlertDialog.Builder(this)
111-
.setMessage("The current device doesn't support bluetooth le").show()
108+
is DeviceDoesNotSupportBluetooth -> AlertDialog
109+
.Builder(this)
110+
.setMessage("The current device doesn't support bluetooth le")
111+
.show()
112+
is BluetoothIsTurnedOff ->
113+
AlertDialog.Builder(this).setMessage("Bluetooth is turned off").show()
112114
is NeedLocationPermission -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
113-
requestPermissions(
114-
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
115-
PERMISSION_CODE_FINE_LOCATION
116-
)
117-
is BluetoothIsTurnedOff -> AlertDialog.Builder(this)
118-
.setMessage("Bluetooth is turned off").show()
119-
is LocationServiceDisabled -> startActivityForResult(
120-
Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS),
121-
REQUEST_CODE_ENABLE_LOCATION
122-
)
115+
permissionLauncher.launch(ACCESS_FINE_LOCATION)
116+
is NeedBluetoothScanPermission -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
117+
permissionLauncher.launch(BLUETOOTH_SCAN)
118+
is LocationServiceDisabled ->
119+
forResultLauncher.launch(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
123120
else -> AlertDialog.Builder(this).setMessage("Error occurred: $it").show()
124121
}
125122
})
@@ -131,9 +128,4 @@ class ScanActivity : AppCompatActivity() {
131128
object StartingScan : States()
132129
object Scanning : States()
133130
}
134-
135-
companion object {
136-
private const val PERMISSION_CODE_FINE_LOCATION = 1
137-
private const val REQUEST_CODE_ENABLE_LOCATION = 2
138-
}
139131
}

dev-app/build.gradle

Whitespace-only changes.

dev-app/src/main/AndroidManifest.xml

Whitespace-only changes.

dev-app/src/main/java/com/vincentmasselis/devapp/MainActivity.kt

Whitespace-only changes.

rxbluetoothkotlin-core/build.gradle

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ plugins {
55
}
66

77
android {
8-
compileSdkVersion 30
9-
buildToolsVersion "30.0.3"
8+
compileSdkVersion compile_sdk_version
9+
buildToolsVersion build_tool_version
1010

1111
defaultConfig {
1212
minSdkVersion 18
13-
targetSdkVersion 30
13+
targetSdkVersion target_sdk_version
1414

1515
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1616
}
@@ -33,11 +33,11 @@ android {
3333
}
3434

3535
dependencies {
36-
api 'androidx.core:core-ktx:1.6.0'
37-
api 'androidx.annotation:annotation:1.3.0'
36+
implementation 'androidx.core:core-ktx:1.7.0'
37+
implementation 'androidx.annotation:annotation:1.3.0'
3838
api 'io.reactivex.rxjava3:rxjava:3.1.2'
3939
api 'io.reactivex.rxjava3:rxandroid:3.0.0'
40-
api 'no.nordicsemi.android.support.v18:scanner:1.4.3'
40+
api 'no.nordicsemi.android.support.v18:scanner:1.6.0'
4141

4242
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
4343
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -55,7 +55,7 @@ dokkaHtml.configure {
5555

5656
ext {
5757
PUBLISH_GROUP_ID = 'com.masselis.rxbluetoothkotlin'
58-
PUBLISH_VERSION = '3.1.0'
58+
PUBLISH_VERSION = '3.2.0'
5959
PUBLISH_ARTIFACT_ID = 'rxbluetoothkotlin-core'
6060
}
6161

rxbluetoothkotlin-core/src/androidTest/kotlin/com/masselis/rxbluetoothkotlin/utils.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.masselis.rxbluetoothkotlin
22

3-
import android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
4-
import android.Manifest.permission.ACCESS_FINE_LOCATION
3+
import android.Manifest.permission.*
54
import android.bluetooth.BluetoothAdapter
65
import android.bluetooth.BluetoothManager
76
import android.content.Context
87
import android.content.IntentFilter
98
import android.os.Build
9+
import android.os.Build.VERSION.SDK_INT
1010
import android.util.Log
1111
import com.masselis.rxbluetoothkotlin.internal.appContext
1212
import com.masselis.rxbluetoothkotlin.internal.observe
@@ -99,6 +99,14 @@ internal val NOTIFY_CHAR: UUID = UUID
9999
internal val READ_CHAR: UUID = UUID
100100
.fromString("00002A2B-0000-1000-8000-00805F9B34FB")
101101

102-
internal val PERMISSIONS = mutableListOf(ACCESS_FINE_LOCATION)
103-
.apply { if (Build.VERSION.SDK_INT >= 29) add(ACCESS_BACKGROUND_LOCATION) }
104-
.toTypedArray()
102+
internal val PERMISSIONS =
103+
when (SDK_INT) {
104+
in Build.VERSION_CODES.M until Build.VERSION_CODES.Q ->
105+
arrayOf(ACCESS_COARSE_LOCATION)
106+
in Build.VERSION_CODES.Q until Build.VERSION_CODES.S ->
107+
arrayOf(ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION)
108+
in Build.VERSION_CODES.S..Int.MAX_VALUE ->
109+
arrayOf(BLUETOOTH_SCAN)
110+
else ->
111+
emptyArray()
112+
}

0 commit comments

Comments
 (0)