Skip to content

Commit

Permalink
feat(android): add support for Android 12 permissions (#274)
Browse files Browse the repository at this point in the history
* feat(android): add support for Android 12 permissions

* chore(android): fmt Kotlin files

* docs: update Android documentation
  • Loading branch information
pwespi authored Feb 27, 2022
1 parent c27748b commit 9d38682
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 25 deletions.
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Below is an index of all the methods available.

<docgen-index>

- [`initialize()`](#initialize)
- [`initialize(...)`](#initialize)
- [`isEnabled()`](#isenabled)
- [`enable()`](#enable)
- [`disable()`](#disable)
Expand Down Expand Up @@ -121,6 +121,32 @@ If the app needs to use Bluetooth while it is in the background, you also have t

On Android, no further steps are required to use the plugin (if you are using Capacitor 2, see [here](https://github.com/capacitor-community/bluetooth-le/blob/0.x/README.md#android)).

#### (Optional) Android 12 Bluetooth permissions

If your app targets Android 12 (API level 31) or higher and your app doesn't use Bluetooth scan results to derive physical location information, you can strongly assert that your app doesn't derive physical location. This allows the app to scan for Bluetooth devices without asking for location permissions. See the [Android documentation](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare-android12-or-higher).

The following steps are required to scan for Bluetooth devices without location permission on Android 12 devices:

- In `android/variables.gradle`, increase `compileSdkVersion` and `targetSdkVersion` to 31 (this can have other consequences on your app, so make sure you know what you're doing).
- Make sure you have JDK 11+ (it is recommended to use JDK that comes with Android Studio).
- In `android/app/src/main/AndroidManifest.xml`, add `android:exported="true"` to your activity (setting [`android:exported`](https://developer.android.com/guide/topics/manifest/activity-element#exported) is required in apps targeting Android 12 and higher).
- In `android/app/src/main/AndroidManifest.xml`, update the permissions:
```diff
<!-- Permissions -->
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
+ android:usesPermissionFlags="neverForLocation"
+ tools:targetApi="s" />
```
- Set the `androidNeverForLocation` flag to `true` when initializing the `BleClient`.
```ts
import { BleClient } from '@capacitor-community/bluetooth-le';
await BleClient.initialize({ androidNeverForLocation: true });
```

> [_Note_: If you include neverForLocation in your android:usesPermissionFlags, some BLE beacons are filtered from the scan results.](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#assert-never-for-location)
## Configuration

You can configure the strings that are displayed in the device selection dialog on iOS and Android when using `requestDevice()`:
Expand Down Expand Up @@ -330,16 +356,20 @@ _Note_: web support depends on the browser, see [implementation status](https://
<docgen-api>
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->

### initialize()
### initialize(...)

```typescript
initialize() => Promise<void>
initialize(options?: InitializeOptions | undefined) => Promise<void>
```

Initialize Bluetooth Low Energy (BLE). If it fails, BLE might be unavailable on this device.
On **Android** it will ask for the location permission. On **iOS** it will ask for the Bluetooth permission.
For an example, see [usage](#usage).

| Param | Type |
| ------------- | --------------------------------------------------------------- |
| **`options`** | <code><a href="#initializeoptions">InitializeOptions</a></code> |

---

### isEnabled()
Expand Down Expand Up @@ -774,6 +804,12 @@ Stop listening to the changes of the value of a characteristic. For an example,

### Interfaces

#### InitializeOptions

| Prop | Type | Description | Default |
| ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------ |
| **`androidNeverForLocation`** | <code>boolean</code> | If your app doesn't use Bluetooth scan results to derive physical location information, you can strongly assert that your app doesn't derive physical location. (Android only) Requires adding 'neverForLocation' to AndroidManifest.xml https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#assert-never-for-location | <code>false</code> |

#### DisplayStrings

| Prop | Type | Default | Since |
Expand Down
31 changes: 22 additions & 9 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.capacitorjs.community.plugins.bluetoothle">
<!-- Bluetooth -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<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.BLUETOOTH_SCAN"
tools:targetApi="s" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT"
tools:targetApi="s" />

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.capacitorjs.community.plugins.bluetoothle">
<!-- Bluetooth -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
</manifest>
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
</manifest>

Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.ParcelUuid
import android.provider.Settings.*
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.location.LocationManagerCompat
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
Expand All @@ -30,14 +32,34 @@ import kotlin.collections.ArrayList
Permission(
strings = [
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
], alias = "location"
], alias = "ACCESS_COARSE_LOCATION"
),
Permission(
strings = [
Manifest.permission.ACCESS_FINE_LOCATION,
], alias = "ACCESS_FINE_LOCATION"
),
Permission(
strings = [
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
], alias = "bluetooth"
], alias = "BLUETOOTH"
),
Permission(
strings = [
Manifest.permission.BLUETOOTH_ADMIN,
], alias = "BLUETOOTH_ADMIN"
),
Permission(
strings = [
// Manifest.permission.BLUETOOTH_SCAN
"android.permission.BLUETOOTH_SCAN",
], alias = "BLUETOOTH_SCAN"
),
Permission(
strings = [
// Manifest.permission.BLUETOOTH_ADMIN
"android.permission.BLUETOOTH_CONNECT",
], alias = "BLUETOOTH_CONNECT"
),
]
)
Expand All @@ -56,23 +78,47 @@ class BluetoothLe : Plugin() {
private var deviceMap = HashMap<String, Device>()
private var deviceScanner: DeviceScanner? = null
private var displayStrings: DisplayStrings? = null
private var aliases: Array<String> = arrayOf<String>()

override fun load() {
displayStrings = getDisplayStrings()
}

@PluginMethod()
fun initialize(call: PluginCall) {
if (getPermissionState("location") != PermissionState.GRANTED) {
requestAllPermissions(call, "initializeCallback");
// Build.VERSION_CODES.S = 31
if (Build.VERSION.SDK_INT >= 31) {
val neverForLocation = call.getBoolean("androidNeverForLocation", false) as Boolean
aliases = if (neverForLocation) {
arrayOf<String>(
"BLUETOOTH_SCAN",
"BLUETOOTH_CONNECT",
)
} else {
arrayOf<String>(
"BLUETOOTH_SCAN",
"BLUETOOTH_CONNECT",
"ACCESS_FINE_LOCATION",
)
}
} else {
runInitialization(call);
aliases = arrayOf<String>(
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"BLUETOOTH",
"BLUETOOTH_ADMIN",
)
}
requestPermissionForAliases(aliases, call, "checkPermission");
}

@PermissionCallback()
private fun initializeCallback(call: PluginCall) {
if (getPermissionState("location") == PermissionState.GRANTED) {
private fun checkPermission(call: PluginCall) {
val granted: List<Boolean> = aliases.map { alias ->
getPermissionState(alias) == PermissionState.GRANTED
}
// all have to be true
if (granted.all { it }) {
runInitialization(call);
} else {
call.reject("Permission denied.")
Expand Down
5 changes: 5 additions & 0 deletions src/bleClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ describe('BleClient', () => {
expect(BluetoothLe.initialize).toHaveBeenCalledTimes(1);
});

it('should run initialize with options', async () => {
await BleClient.initialize({ androidNeverForLocation: true });
expect(BluetoothLe.initialize).toHaveBeenCalledTimes(1);
});

it('should run isEnabled', async () => {
(BluetoothLe.isEnabled as jest.Mock).mockReturnValue({ value: true });
const result = await BleClient.isEnabled();
Expand Down
7 changes: 4 additions & 3 deletions src/bleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
BleDevice,
BleService,
Data,
InitializeOptions,
ReadResult,
RequestBleDeviceOptions,
ScanResult,
Expand All @@ -23,7 +24,7 @@ export interface BleClientInterface {
* On **Android** it will ask for the location permission. On **iOS** it will ask for the Bluetooth permission.
* For an example, see [usage](#usage).
*/
initialize(): Promise<void>;
initialize(options?: InitializeOptions): Promise<void>;

/**
* Reports whether Bluetooth is enabled on this device.
Expand Down Expand Up @@ -282,9 +283,9 @@ class BleClientClass implements BleClientInterface {
this.queue = getQueue(false);
}

async initialize(): Promise<void> {
async initialize(options?: InitializeOptions): Promise<void> {
await this.queue(async () => {
await BluetoothLe.initialize();
await BluetoothLe.initialize(options);
});
}

Expand Down
14 changes: 13 additions & 1 deletion src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import type { PluginListenerHandle } from '@capacitor/core';

import type { DisplayStrings } from './config';

export interface InitializeOptions {
/**
* If your app doesn't use Bluetooth scan results to derive physical
* location information, you can strongly assert that your app
* doesn't derive physical location. (Android only)
* Requires adding 'neverForLocation' to AndroidManifest.xml
* https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#assert-never-for-location
* @default false
*/
androidNeverForLocation?: boolean;
}

export interface RequestBleDeviceOptions {
/**
* Filter devices by service UUIDs.
Expand Down Expand Up @@ -231,7 +243,7 @@ export interface ScanResult {
}

export interface BluetoothLePlugin {
initialize(): Promise<void>;
initialize(options?: InitializeOptions): Promise<void>;
isEnabled(): Promise<BooleanResult>;
enable(): Promise<void>;
disable(): Promise<void>;
Expand Down

0 comments on commit 9d38682

Please sign in to comment.