diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..86e1fc0 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,71 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# Sample workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy docs to Pages + +on: + push: + branches: ["master"] + paths: ["docs/**"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Build job + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./docs + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.1' # Not needed with a .ruby-version file + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 0 # Increment this number if you need to re-download cached gems + working-directory: '${{ github.workspace }}/docs' + - name: Setup Pages + id: pages + uses: actions/configure-pages@v3 + - name: Build with Jekyll + # Outputs to the './_site' directory by default + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production + - name: Upload artifact + # Automatically uploads an artifact from the 'docs/_site' directory by default + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_site/ + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + env: + ACTIONS_STEP_DEBUG: true + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..9f8641b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark and close stale issues + +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + repo-token: ${{ secrets.STALE_TOKEN }} + days-before-stale: 90 + ignore-pr-updates: true + days-before-pr-stale: -1 + days-before-pr-close: -1 + stale-issue-message: 'This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.' + stale-issue-label: 'stale' + exempt-issue-labels: 'bug, enhancement' + exempt-all-assignees: true diff --git a/.gitignore b/.gitignore index a560875..ead46b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,36 @@ +# android .idea - -android/local.properties -android/.project -android/.settings -android/.classpath +.vscode +.gradle +*.iml +*.hprof +/android/local.properties +/android/.project +/android/.settings +/android/.classpath +/android/gradle +/android/gradlew +/android/gradlew.bat gen/ +# JS dependencies & generated files +node_modules/ +npm-debug.log +yarn-error.log +dist/ -.gradle -gradle -gradlew -gradlew.bat - -node_modules +## Build generated +build/ +*.tsbuildinfo # OSX -# .DS_Store # Xcode -# # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## Build generated -build/ DerivedData - *.tgz - *.xcuserstate - xcuserdata - *.iml *.swp diff --git a/.npmignore b/.npmignore index 1f362aa..e217a21 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ +.github +docs example .git *.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5fdb4e4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false, + "bracketSpacing": true, + "arrowParen": "singleline" +} diff --git a/BleManager.js b/BleManager.js deleted file mode 100644 index 8021a85..0000000 --- a/BleManager.js +++ /dev/null @@ -1,411 +0,0 @@ -"use strict"; -var React = require("react-native"); -var bleManager = React.NativeModules.BleManager; - -class BleManager { - constructor() { - this.isPeripheralConnected = this.isPeripheralConnected.bind(this); - } - - read(peripheralId, serviceUUID, characteristicUUID) { - return new Promise((fulfill, reject) => { - bleManager.read( - peripheralId, - serviceUUID, - characteristicUUID, - (error, data) => { - if (error) { - reject(error); - } else { - fulfill(data); - } - } - ); - }); - } - - readRSSI(peripheralId) { - return new Promise((fulfill, reject) => { - bleManager.readRSSI(peripheralId, (error, rssi) => { - if (error) { - reject(error); - } else { - fulfill(rssi); - } - }); - }); - } - - refreshCache(peripheralId) { - return new Promise((fulfill, reject) => { - bleManager.refreshCache(peripheralId, (error, result) => { - if (error) { - reject(error); - } else { - fulfill(result); - } - }); - }); - } - - retrieveServices(peripheralId, services) { - return new Promise((fulfill, reject) => { - bleManager.retrieveServices( - peripheralId, - services, - (error, peripheral) => { - if (error) { - reject(error); - } else { - fulfill(peripheral); - } - } - ); - }); - } - - write(peripheralId, serviceUUID, characteristicUUID, data, maxByteSize) { - if (maxByteSize == null) { - maxByteSize = 20; - } - return new Promise((fulfill, reject) => { - bleManager.write( - peripheralId, - serviceUUID, - characteristicUUID, - data, - maxByteSize, - error => { - if (error) { - reject(error); - } else { - fulfill(); - } - } - ); - }); - } - - writeWithoutResponse( - peripheralId, - serviceUUID, - characteristicUUID, - data, - maxByteSize, - queueSleepTime - ) { - if (maxByteSize == null) { - maxByteSize = 20; - } - if (queueSleepTime == null) { - queueSleepTime = 10; - } - return new Promise((fulfill, reject) => { - bleManager.writeWithoutResponse( - peripheralId, - serviceUUID, - characteristicUUID, - data, - maxByteSize, - queueSleepTime, - error => { - if (error) { - reject(error); - } else { - fulfill(); - } - } - ); - }); - } - - connect(peripheralId) { - return new Promise((fulfill, reject) => { - bleManager.connect(peripheralId, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - createBond(peripheralId,peripheralPin=null) { - return new Promise((fulfill, reject) => { - bleManager.createBond(peripheralId,peripheralPin, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - removeBond(peripheralId) { - return new Promise((fulfill, reject) => { - bleManager.removeBond(peripheralId, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - disconnect(peripheralId, force = true) { - return new Promise((fulfill, reject) => { - bleManager.disconnect(peripheralId, force, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - startNotification(peripheralId, serviceUUID, characteristicUUID) { - return new Promise((fulfill, reject) => { - bleManager.startNotification(peripheralId, serviceUUID, characteristicUUID, (error) => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - stopNotification(peripheralId, serviceUUID, characteristicUUID) { - return new Promise((fulfill, reject) => { - bleManager.stopNotification( - peripheralId, - serviceUUID, - characteristicUUID, - error => { - if (error) { - reject(error); - } else { - fulfill(); - } - } - ); - }); - } - - checkState() { - bleManager.checkState(); - } - - start(options) { - return new Promise((fulfill, reject) => { - if (options == null) { - options = {}; - } - bleManager.start(options, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - setServiceRecoveryData(data) { - return new Promise((fulfill, reject) => { - bleManager.setServiceRecoveryData(data, (error) => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - scan(serviceUUIDs, seconds, allowDuplicates, scanningOptions = {}) { - return new Promise((fulfill, reject) => { - if (allowDuplicates == null) { - allowDuplicates = false; - } - - // (ANDROID) Match as many advertisement per filter as hw could allow - // depends on current capability and availability of the resources in hw. - if (scanningOptions.numberOfMatches == null) { - scanningOptions.numberOfMatches = 3; - } - - // (ANDROID) Defaults to MATCH_MODE_AGGRESSIVE - if (scanningOptions.matchMode == null) { - scanningOptions.matchMode = 1; - } - - // (ANDROID) Defaults to SCAN_MODE_LOW_POWER - if (scanningOptions.scanMode == null) { - scanningOptions.scanMode = 0; - } - - // (ANDROID) Defaults to CALLBACK_TYPE_ALL_MATCHES - // WARN: sometimes, setting a scanSetting instead of leaving it untouched might result in unexpected behaviors. - // https://github.com/dariuszseweryn/RxAndroidBle/issues/462 - if (scanningOptions.callbackType == null) { - scanningOptions.callbackType = 1; - } - - // (ANDROID) Defaults to 0ms (report results immediately). - if (scanningOptions.reportDelay == null) { - scanningOptions.reportDelay = 0; - } - - bleManager.scan( - serviceUUIDs, - seconds, - allowDuplicates, - scanningOptions, - error => { - if (error) { - reject(error); - } else { - fulfill(); - } - } - ); - }); - } - - stopScan() { - return new Promise((fulfill, reject) => { - bleManager.stopScan(error => { - if (error != null) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - enableBluetooth() { - return new Promise((fulfill, reject) => { - bleManager.enableBluetooth(error => { - if (error != null) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - getConnectedPeripherals(serviceUUIDs) { - return new Promise((fulfill, reject) => { - bleManager.getConnectedPeripherals(serviceUUIDs, (error, result) => { - if (error) { - reject(error); - } else { - if (result != null) { - fulfill(result); - } else { - fulfill([]); - } - } - }); - }); - } - - getBondedPeripherals() { - return new Promise((fulfill, reject) => { - bleManager.getBondedPeripherals((error, result) => { - if (error) { - reject(error); - } else { - if (result != null) { - fulfill(result); - } else { - fulfill([]); - } - } - }); - }); - } - - getDiscoveredPeripherals() { - return new Promise((fulfill, reject) => { - bleManager.getDiscoveredPeripherals((error, result) => { - if (error) { - reject(error); - } else { - if (result != null) { - fulfill(result); - } else { - fulfill([]); - } - } - }); - }); - } - - removePeripheral(peripheralId) { - return new Promise((fulfill, reject) => { - bleManager.removePeripheral(peripheralId, error => { - if (error) { - reject(error); - } else { - fulfill(); - } - }); - }); - } - - isPeripheralConnected(peripheralId, serviceUUIDs) { - return this.getConnectedPeripherals(serviceUUIDs).then(result => { - if ( - result.find(p => { - return p.id === peripheralId; - }) - ) { - return true; - } else { - return false; - } - }); - } - - requestConnectionPriority(peripheralId, connectionPriority) { - return new Promise((fulfill, reject) => { - bleManager.requestConnectionPriority( - peripheralId, - connectionPriority, - (error, status) => { - if (error) { - reject(error); - } else { - fulfill(status); - } - } - ); - }); - } - - requestMTU(peripheralId, mtu) { - return new Promise((fulfill, reject) => { - bleManager.requestMTU(peripheralId, mtu, (error, mtu) => { - if (error) { - reject(error); - } else { - fulfill(mtu); - } - }); - }); - } - - setName(name) { - bleManager.setName(name); - } -} - -module.exports = new BleManager(); diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b5d0355 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@innove.it. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index a5ff738..5d6ed32 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,43 @@ # react-native-ble-manager -[![npm version](https://img.shields.io/npm/v/react-native-ble-manager.svg?style=flat)](https://www.npmjs.com/package/react-native-ble-manager) -[![npm downloads](https://img.shields.io/npm/dm/react-native-ble-manager.svg?style=flat)](https://www.npmjs.com/package/react-native-ble-manager) -[![GitHub issues](https://img.shields.io/github/issues/innoveit/react-native-ble-manager.svg?style=flat)](https://github.com/innoveit/react-native-ble-manager/issues) +![GitHub Release](https://img.shields.io/github/v/release/innoveit/react-native-ble-manager?style=for-the-badge) +[![npm version](https://img.shields.io/npm/v/react-native-ble-manager.svg?style=for-the-badge)](https://www.npmjs.com/package/react-native-ble-manager) +[![npm downloads](https://img.shields.io/npm/dm/react-native-ble-manager.svg?style=for-the-badge)](https://www.npmjs.com/package/react-native-ble-manager) +[![GitHub issues](https://img.shields.io/github/issues/innoveit/react-native-ble-manager.svg?style=for-the-badge)](https://github.com/innoveit/react-native-ble-manager/issues) -This is a porting of https://github.com/don/cordova-plugin-ble-central project to React Native. +A React Native Bluetooth Low Energy library. + +Originally inspired by https://github.com/don/cordova-plugin-ble-central. + +## Introduction + +The library is a simple connection with the OS APIs, the BLE stack should be standard but often has different behaviors based on the device used, the operating system and the BLE chip it connects to. Before opening an issue verify that the problem is really the library. ## Requirements -RN 0.60+ +RN 0.76+ only the new architecture is supported -RN 0.40-0.59 supported until 6.7.X -RN 0.30-0.39 supported until 2.4.3 +RN 0.60-0.75 supported until 11.X +RN 0.40-0.59 supported until 6.7.X +RN 0.30-0.39 supported until 2.4.3 ## Supported Platforms -- iOS 8+ -- Android (API 19+) +- iOS 15.1+ +- Android (API 23+) ## Install ```shell npm i --save react-native-ble-manager ``` -The library support the react native autolink feature. - - -##### Android - Update Manifest - -```xml -// file: android/app/src/main/AndroidManifest.xml - - - - - - - - - - - - - - - - - - -... -``` - -If you need communication while the app is not in the foreground you need the "ACCESS_BACKGROUND_LOCATION" permission. - -##### iOS - Update Info.plist +The library support the react native autolink feature. -In iOS >= 13 you need to add the `NSBluetoothAlwaysUsageDescription` string key. +## Documentation -## Note +Read here [the full documentation](https://innoveit.github.io/react-native-ble-manager/) -- Remember to use the `start` method before anything. -- If you have problem with old devices try avoid to connect/read/write to a peripheral during scan. -- Android API >= 23 require the ACCESS_COARSE_LOCATION permission to scan for peripherals. React Native >= 0.33 natively support PermissionsAndroid like in the example. -- Android API >= 29 require the ACCESS_FINE_LOCATION permission to scan for peripherals. - React-Native 0.63.X started targeting Android API 29. -- Before write, read or start notification you need to call `retrieveServices` method -- Because location and bluetooth permissions are runtime permissions, you **must** request these permissions at runtime along with declaring them in your manifest. ## Example @@ -86,755 +48,48 @@ The easiest way to test is simple make your AppRegistry point to our example com import React, { Component } from "react"; import { AppRegistry } from "react-native"; import App from "react-native-ble-manager/example/App"; //<-- simply point to the example js! - +/* +Note: The react-native-ble-manager/example directory is only included when cloning the repo, the above import will not work +if trying to import react-native-ble-manager/example from node_modules +*/ AppRegistry.registerComponent("MyAwesomeApp", () => App); ``` -Or, you can still look into the whole [example](https://github.com/innoveit/react-native-ble-manager/tree/master/example) folder for a standalone project. - -## Methods - -### start(options) - -Init the module. -Returns a `Promise` object. -Don't call this multiple times. - -**Arguments** - -- `options` - `JSON` - -The parameter is optional the configuration keys are: - -- `showAlert` - `Boolean` - [iOS only] Show or hide the alert if the bluetooth is turned off during initialization -- `restoreIdentifierKey` - `String` - [iOS only] Unique key to use for CoreBluetooth state restoration -- `queueIdentifierKey` - `String` - [iOS only] Unique key to use for a queue identifier on which CoreBluetooth events will be dispatched -- `forceLegacy` - `Boolean` - [Android only] Force to use the LegacyScanManager - -**Examples** - -```js -BleManager.start({ showAlert: false }).then(() => { - // Success code - console.log("Module initialized"); -}); -``` - -### scan(serviceUUIDs, seconds, allowDuplicates, scanningOptions) - -Scan for available peripherals. -Returns a `Promise` object. - -**Arguments** - -- `serviceUUIDs` - `Array of String` - the UUIDs of the services to looking for. On Android the filter works only for 5.0 or newer. -- `seconds` - `Integer` - the amount of seconds to scan. -- `allowDuplicates` - `Boolean` - [iOS only] allow duplicates in device scanning -- `scanningOptions` - `JSON` - [Android only] after Android 5.0, user can control specific ble scan behaviors: - - `numberOfMatches` - `Number` - [Android only] corresponding to [`setNumOfMatches`](). Defaults to `ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT`. - - `matchMode` - `Number` - [Android only] corresponding to [`setMatchMode`](). Defaults to `ScanSettings.MATCH_MODE_AGGRESSIVE`. - - `callbackType` - `Number` - [Android only] corresponding to [`setCallbackType`](). Defaults `ScanSettings.CALLBACK_TYPE_ALL_MATCHES`. - - `scanMode` - `Number` - [Android only] corresponding to [`setScanMode`](). Defaults to `ScanSettings.SCAN_MODE_LOW_POWER`. - - `reportDelay` - `Number` - [Android only] corresponding to [`setReportDelay`](). Defaults to `0ms`. - - `phy` - `Number` - [Android only] corresponding to [`setPhy`](https://developer.android.com/reference/android/bluetooth/le/ScanSettings.Builder#setPhy(int)) - - `legacy` - `Boolean` - [Android only] corresponding to [`setLegacy`](https://developer.android.com/reference/android/bluetooth/le/ScanSettings.Builder#setLegacy(boolean)) - -**Examples** - -```js -BleManager.scan([], 5, true).then(() => { - // Success code - console.log("Scan started"); -}); -``` - -### stopScan() - -Stop the scanning. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.stopScan().then(() => { - // Success code - console.log("Scan stopped"); -}); -``` - -### connect(peripheralId) - -Attempts to connect to a peripheral. In many case if you can't connect you have to scan for the peripheral before. -Returns a `Promise` object. - -> In iOS, attempts to connect to a peripheral do not time out (please see [Apple's doc](https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connect)), so you might need to set a timer explicitly if you don't want this behavior. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral to connect. - -**Examples** - -```js -BleManager.connect("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") - .then(() => { - // Success code - console.log("Connected"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### disconnect(peripheralId, force) - -Disconnect from a peripheral. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral to disconnect. -- `force` - `boolean` - [Android only] defaults to true, if true force closes gatt - connection and send the BleManagerDisconnectPeripheral - event immediately to Javascript, else disconnects the - connection and waits for [`disconnected state`](https://developer.android.com/reference/android/bluetooth/BluetoothProfile#STATE_DISCONNECTED) to - [`close the gatt connection`]() - and then sends the BleManagerDisconnectPeripheral to the - Javascript - -**Examples** - -```js -BleManager.disconnect("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") - .then(() => { - // Success code - console.log("Disconnected"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### enableBluetooth() [Android only] - -Create the request to the user to activate the bluetooth. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.enableBluetooth() - .then(() => { - // Success code - console.log("The bluetooth is already enabled or the user confirm"); - }) - .catch((error) => { - // Failure code - console.log("The user refuse to enable bluetooth"); - }); -``` - -### checkState() - -Force the module to check the state of BLE and trigger a BleManagerDidUpdateState event. - -**Examples** - -```js -BleManager.checkState(); -``` - -### startNotification(peripheralId, serviceUUID, characteristicUUID) - -Start the notification on the specified characteristic, you need to call `retrieveServices` method before. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. - -**Examples** - -```js -BleManager.startNotification( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -) - .then(() => { - // Success code - console.log("Notification started"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### startNotificationUseBuffer(peripheralId, serviceUUID, characteristicUUID, buffer) [Android only] - -Start the notification on the specified characteristic, you need to call `retrieveServices` method before. The buffer will collect a number or messages from the server and then emit once the buffer count it reached. Helpful to reducing the number or js bridge crossings when a characteristic is sending a lot of messages. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. -- `buffer` - `Integer` - a number of message to buffer prior to emit for the characteristic. - -**Examples** - -```js -BleManager.startNotification( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - 1234 -) - .then(() => { - // Success code - console.log("Notification started"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### stopNotification(peripheralId, serviceUUID, characteristicUUID) - -Stop the notification on the specified characteristic. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. - -### read(peripheralId, serviceUUID, characteristicUUID) - -Read the current value of the specified characteristic, you need to call `retrieveServices` method before. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. - -**Examples** - -```js -BleManager.read( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -) - .then((readData) => { - // Success code - console.log("Read: " + readData); - - const buffer = Buffer.Buffer.from(readData); //https://github.com/feross/buffer#convert-arraybuffer-to-buffer - const sensorData = buffer.readUInt8(1, true); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### write(peripheralId, serviceUUID, characteristicUUID, data, maxByteSize) - -Write with response to the specified characteristic, you need to call `retrieveServices` method before. -Returns a `Promise` object. +Or, [use the example directly](example) -**Arguments** -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. -- `data` - `Byte array` - the data to write. -- `maxByteSize` - `Integer` - specify the max byte size before splitting message, defaults to 20 bytes if not specified +## Library development -**Data preparation** +- the library is written in typescript and needs to be built before being used for publication or local development, using the provided npm scripts in `package.json`. +- the local `example` project is configured to work with the locally built version of the library. To be able to run it, you need to build at least once the library so that its outputs listed as entrypoint in `package.json` (in the `dist` folder) are properly generated for consumption by the example project: -If your data is not in byte array format you should convert it first. For strings you can use `convert-string` or other npm package in order to achieve that. -Install the package first: +from the root folder: ```shell -npm install convert-string -``` - -Then use it in your application: - -```js -// Import/require in the beginning of the file -import { stringToBytes } from "convert-string"; -// Convert data to byte array before write/writeWithoutResponse -const data = stringToBytes(yourStringData); -``` - -Feel free to use other packages or google how to convert into byte array if your data has other format. - -**Examples** - -```js -BleManager.write( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - data -) - .then(() => { - // Success code - console.log("Write: " + data); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### writeWithoutResponse(peripheralId, serviceUUID, characteristicUUID, data, maxByteSize, queueSleepTime) - -Write without response to the specified characteristic, you need to call `retrieveServices` method before. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUID` - `String` - the UUID of the service. -- `characteristicUUID` - `String` - the UUID of the characteristic. -- `data` - `Byte array` - the data to write. -- `maxByteSize` - `Integer` - (Optional) specify the max byte size -- `queueSleepTime` - `Integer` - (Optional) specify the wait time before each write if the data is greater than maxByteSize - -**Data preparation** - -If your data is not in byte array format check info for the write function above. - -**Example** - -```js -BleManager.writeWithoutResponse( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - data -) - .then(() => { - // Success code - console.log("Writed: " + data); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### readRSSI(peripheralId) - -Read the current value of the RSSI. -Returns a `Promise` object resolving with the updated RSSI value (`number`) if it succeeds. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. - -**Examples** - -```js -BleManager.readRSSI("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") - .then((rssi) => { - // Success code - console.log("Current RSSI: " + rssi); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### requestConnectionPriority(peripheralId, connectionPriority) [Android only API 21+] - -Request a connection parameter update. -Returns a `Promise` object which fulfills with the status of the request. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `connectionPriority` - `Integer` - the connection priority to be requested, as follows: - - 0 - balanced priority connection - - 1 - high priority connection - - 2 - low power priority connection - -**Examples** - -```js -BleManager.requestConnectionPriority("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 1) - .then((status) => { - // Success code - console.log("Requested connection priority"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### requestMTU(peripheralId, mtu) [Android only API 21+] - -Request an MTU size used for a given connection. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `mtu` - `Integer` - the MTU size to be requested in bytes. - -**Examples** - -```js -BleManager.requestMTU("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 512) - .then((mtu) => { - // Success code - console.log("MTU size changed to " + mtu + " bytes"); - }) - .catch((error) => { - // Failure code - console.log(error); - }); -``` - -### retrieveServices(peripheralId[, serviceUUIDs]) - -Retrieve the peripheral's services and characteristics. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. -- `serviceUUIDs` - `String[]` - [iOS only] only retrieve these services. - -**Examples** - -```js -BleManager.retrieveServices("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX").then( - (peripheralInfo) => { - // Success code - console.log("Peripheral info:", peripheralInfo); - } -); -``` - -### refreshCache(peripheralId) [Android only] - -refreshes the peripheral's services and characteristics cache -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. - -**Examples** - -```js -BleManager.refreshCache("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") - .then((peripheralInfo) => { - // Success code - console.log("cache refreshed!"); - }) - .catch((error) => { - console.error(error); - }); -``` - -### getConnectedPeripherals(serviceUUIDs) - -Return the connected peripherals. -Returns a `Promise` object. - -**Arguments** - -- `serviceUUIDs` - `Array of String` - the UUIDs of the services to looking for. - -**Examples** - -```js -BleManager.getConnectedPeripherals([]).then((peripheralsArray) => { - // Success code - console.log("Connected peripherals: " + peripheralsArray.length); -}); -``` - -### createBond(peripheralId,peripheralPin) [Android only] - -Start the bonding (pairing) process with the remote device. If you pass peripheralPin(optional), bonding will be auto(without manual entering pin) -Returns a `Promise` object. The promise is resolved when either `new bond successfully created` or `bond already existed`, otherwise it will be rejected. - -**Examples** - -```js -BleManager.createBond(peripheralId) - .then(() => { - console.log("createBond success or there is already an existing one"); - }) - .catch(() => { - console.log("fail to bond"); - }); -``` - -### removeBond(peripheralId) [Android only] - -Remove a paired device. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.removeBond(peripheralId) - .then(() => { - console.log("removeBond success"); - }) - .catch(() => { - console.log("fail to remove the bond"); - }); -``` - -### getBondedPeripherals() [Android only] - -Return the bonded peripherals. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.getBondedPeripherals([]).then((bondedPeripheralsArray) => { - // Each peripheral in returned array will have id and name properties - console.log("Bonded peripherals: " + bondedPeripheralsArray.length); -}); -``` - -### getDiscoveredPeripherals() - -Return the discovered peripherals after a scan. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.getDiscoveredPeripherals([]).then((peripheralsArray) => { - // Success code - console.log("Discovered peripherals: " + peripheralsArray.length); -}); -``` - -### removePeripheral(peripheralId) [Android only] - -Removes a disconnected peripheral from the cached list. -It is useful if the device is turned off, because it will be re-discovered upon turning on again. -Returns a `Promise` object. - -**Arguments** - -- `peripheralId` - `String` - the id/mac address of the peripheral. - -### isPeripheralConnected(peripheralId, serviceUUIDs) - -Check whether a specific peripheral is connected and return `true` or `false`. -Returns a `Promise` object. - -**Examples** - -```js -BleManager.isPeripheralConnected( - "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - [] -).then((isConnected) => { - if (isConnected) { - console.log("Peripheral is connected!"); - } else { - console.log("Peripheral is NOT connected!"); - } -}); -``` - -### setName(name) [Android only] - -Create the request to set the name of the bluetooth adapter. (https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#setName(java.lang.String)) -Returns a `Promise` object. - -**Examples** - -```js -BleManager.setName("INNOVEIT_CENTRAL") - .then(() => { - // Success code - console.log("Name set successfully"); - }) - .catch((error) => { - // Failure code - console.log("Name could not be set"); - }); -``` - -## Events - -### BleManagerStopScan - -The scanning for peripherals is ended. - -**Arguments** - -- `status` - `Number` - [iOS] the reason for stopping the scan. Error code 10 is used for timeouts, 0 covers everything else. [Android] the reason for stopping the scan (). Error code 10 is used for timeouts - -**Examples** - -```js -bleManagerEmitter.addListener("BleManagerStopScan", (args) => { - // Scanning is stopped -}); -``` - -### BleManagerDidUpdateState - -The BLE change state. - -**Arguments** - -- `state` - `String` - the new BLE state. can be one of `unknown` (iOS only), `resetting` (iOS only), `unsupported`, `unauthorized` (iOS only), `on`, `off`, `turning_on` (android only), `turning_off` (android only). - -**Examples** - -```js -bleManagerEmitter.addListener("BleManagerDidUpdateState", (args) => { - // The new state: args.state -}); -``` - -### BleManagerDiscoverPeripheral - -The scanning find a new peripheral. - -**Arguments** - -- `id` - `String` - the id of the peripheral -- `name` - `String` - the name of the peripheral -- `rssi` - `Number` - the RSSI value -- `advertising` - `JSON` - the advertising payload, here are some examples: - - `isConnectable` - `Boolean` - - `serviceUUIDs` - `Array of String` - - `manufacturerData` - `JSON` - contains the raw `bytes` and `data` (Base64 encoded string) - - `serviceData` - `JSON` - contains the raw `bytes` and `data` (Base64 encoded string) - - `txPowerLevel` - `Int` - -**Examples** - -```js -bleManagerEmitter.addListener("BleManagerDiscoverPeripheral", (args) => { - // The id: args.id - // The name: args.name -}); +npm install +npm run build ``` -### BleManagerDidUpdateValueForCharacteristic - -A characteristic notify a new value. - -**Arguments** - -- `value` — `Array` — the read value -- `peripheral` — `String` — the id of the peripheral -- `characteristic` — `String` — the UUID of the characteristic -- `service` — `String` — the UUID of the characteristic - -> Event will only be emitted after successful `startNotification`. - -**Example** - -```js -import { bytesToString } from "convert-string"; -import { NativeModules, NativeEventEmitter } from "react-native"; +> if you are modifying the typescript files of the library (in `src/`) on the fly, you can run `npm run watch` instead. If you are modifying files from the native counterparts, you'll need to rebuild the whole app for your target environnement (`npm run android/ios`). -const BleManagerModule = NativeModules.BleManager; -const bleManagerEmitter = new NativeEventEmitter(BleManagerModule); +### Updating documentation -async function connectAndPrepare(peripheral, service, characteristic) { - // Connect to device - await BleManager.connect(peripheral); - // Before startNotification you need to call retrieveServices - await BleManager.retrieveServices(peripheral); - // To enable BleManagerDidUpdateValueForCharacteristic listener - await BleManager.startNotification(peripheral, service, characteristic); - // Add event listener - bleManagerEmitter.addListener( - "BleManagerDidUpdateValueForCharacteristic", - ({ value, peripheral, characteristic, service }) => { - // Convert bytes array to string - const data = bytesToString(value); - console.log(`Received ${data} for characteristic ${characteristic}`); - } - ); - // Actions triggereng BleManagerDidUpdateValueForCharacteristic event -} +Edit files in `docs/`, then test locally with: +```shell +cd docs +bundle install +bundle exec jekyll serve --watch --baseurl / ``` +Then open http://localhost:4000/ -### BleManagerConnectPeripheral - -A peripheral was connected. - -**Arguments** - -- `peripheral` - `String` - the id of the peripheral -- `status` - `Number` - [Android only] connect [`reasons`]() - -### BleManagerDisconnectPeripheral - -A peripheral was disconnected. - -**Arguments** - -- `peripheral` - `String` - the id of the peripheral -- `status` - `Number` - [Android only] disconnect [`reasons`]() -- `domain` - `String` - [iOS only] disconnect error domain -- `code` - `Number` - [iOS only] disconnect error code () - -### BleManagerPeripheralDidBond - -A bond with a peripheral was established - -**Arguments** - -Object with information about the device - -### BleManagerCentralManagerWillRestoreState [iOS only] - -This is fired when [`centralManager:WillRestoreState:`](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518819-centralmanager) is called (app relaunched in the background to handle a bluetooth event). - -**Arguments** - -- `peripherals` - `Array` - an array of previously connected peripherals. - -_For more on performing long-term bluetooth actions in the background:_ - -[iOS Bluetooth State Preservation and Restoration](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW10) - -[iOS Relaunch Conditions](https://developer.apple.com/library/archive/qa/qa1962/_index.html) - -### BleManagerDidUpdateNotificationStateFor [iOS only] - -The peripheral received a request to start or stop providing notifications for a specified characteristic's value. +## Generate the native code from specs +A react-native project is needed to generate the code via *codegen*. -**Arguments** +#### Generate Android code +- in the example folder generate the android project from expo: `npx expo prebuild --platform android` +- in the example/android folder run: `./gradlew generateCodegenArtifactsFromSchema` (you can add --info to have debug messages) +- if you have problems with the gradle cache `cd android && ./gradlew --stop && rm -rf ~/.gradle/caches` -- `peripheral` - `String` - the id of the peripheral -- `characteristic` - `String` - the UUID of the characteristic -- `isNotifying` - `Boolean` - Is the characteristic notifying or not -- `domain` - `String` - [iOS only] error domain -- `code` - `Number` - [iOS only] error code +#### Generate iOS code +- in the example folder generate the ios project from expo: `npx expo prebuild --platform ios` +- the codegen run during the first build, if you need to run it again use `pod install` in the ios folder diff --git a/RNBleManager.podspec b/RNBleManager.podspec new file mode 100644 index 0000000..8567189 --- /dev/null +++ b/RNBleManager.podspec @@ -0,0 +1,24 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = "RNBleManager" + s.summary = package['description'] + s.version = package['version'] + s.authors = package["author"] + s.homepage = package["homepage"] + s.license = package["license"] + s.platform = :ios, "11.0" + s.source = { :git => "https://github.com/pbsc/react-native-ble-manager.git" } + s.source_files = "ios/**/*.{h,mm,swift}" + + s.static_framework = true + + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + install_modules_dependencies(s) +end diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3dae2e4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + + +| Version | Supported | +| -------- | ------------------ | +| 11.x.x | :white_check_mark: | +| < 10.x | :x: | + + +## Reporting a Vulnerability + +If you find a vulnerability write to info@innove.it diff --git a/android/.npmignore b/android/.npmignore new file mode 100644 index 0000000..74d03a1 --- /dev/null +++ b/android/.npmignore @@ -0,0 +1,19 @@ +gradle/ +.git +*.DS_Store +.DS_Store +*Thumbs.db +.gradle +.idea +*.iml +npm-debug.log +node_modules +android/build +local.properties +android/gradle/wrapper +gradlew +*.swp +*.bat +/ios/**/*xcuserdata* +/ios/**/*xcshareddata* +*.log diff --git a/android/build.gradle b/android/build.gradle index 417d4da..15503da 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,55 +1,127 @@ buildscript { - // The Android Gradle plugin is only required when opening the android folder stand-alone. - // This avoids unnecessary downloads and potential conflicts when the library is included as a - // module dependency in an application project. + if (project == rootProject) { + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["kotlinVersion"] + repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:4.2.2") + classpath "com.android.tools.build:gradle:8.6.1" + classpath "com.facebook.react:react-native-gradle-plugin" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } + +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" } apply plugin: 'com.android.library' +apply plugin: "kotlin-android" +apply plugin: 'com.facebook.react' def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["BleManager_" + name] +} + +static def supportsNamespace() { + def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') + def major = parsed[0].toInteger() + def minor = parsed[1].toInteger() + + // Namespace support was added in 7.3.0 + return (major == 7 && minor >= 3) || major >= 8 +} + android { - compileSdkVersion safeExtGet("compileSdkVersion", 30) - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } + if (supportsNamespace()) { + namespace "it.innove" + + sourceSets { + main { + manifest.srcFile "src/main/AndroidManifestNew.xml" + } + } + } + + compileSdk safeExtGet("compileSdk", 34) - defaultConfig { - minSdkVersion safeExtGet("minSdkVersion", 21) - } - lintOptions { - abortOnError false - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_9 + targetCompatibility JavaVersion.VERSION_1_9 + } + + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 24) + targetSdk safeExtGet("targetSdk", 34) + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + // To support legacy module in the future + /* + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + "src/newarch", + // Codegen specs + "generated/java", + "generated/jni" + ] + } else { + java.srcDirs += ["src/oldarch"] + } + } + } + */ } + repositories { - mavenCentral() - google() - jcenter() - maven { - // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - //url "$rootDir/../node_modules/react-native/android" - url "$rootDir/../example/node_modules/react-native/android" - } + mavenCentral() + google() } +def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { - implementation "com.facebook.react:react-native:+" - implementation 'com.google.code.gson:gson:2.8.6' -// implementation "org.jetbrains.trove4j: trove4j: 20160824" + implementation "com.facebook.react:react-android:0.81.5" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' +} + +react { + jsRootDir = file("../src/") + libraryName = "BleManager" + codegenJavaPackageName = "it.innove" } diff --git a/android/gradle.properties b/android/gradle.properties index d015431..164a67d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,2 +1,5 @@ +kotlinVersion=1.9.24 android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true +# https://github.com/facebook/react-native/issues/35168 +org.gradle.jvmargs=-Xmx4096M diff --git a/android/settings.gradle b/android/settings.gradle index e4f7b07..5988677 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'react-native-ble-manager' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 1840b02..d24eed3 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,11 +1,30 @@ - - - - - - - - + package="it.innove" + xmlns:tools="http://schemas.android.com/tools"> + + + + + + + + + + + + + + diff --git a/android/src/main/java/it/innove/BLECommand.java b/android/src/main/java/it/innove/BLECommand.java deleted file mode 100644 index 7b3573b..0000000 --- a/android/src/main/java/it/innove/BLECommand.java +++ /dev/null @@ -1,47 +0,0 @@ -package it.innove; - -import java.util.UUID; - -class BLECommand { - // Types - public static int READ = 10000; - public static int REGISTER_NOTIFY = 10001; - public static int REMOVE_NOTIFY = 10002; - // BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE - // BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - - private UUID serviceUUID; - private UUID characteristicUUID; - private byte[] data; - private int type; - - - public BLECommand(UUID serviceUUID, UUID characteristicUUID, int type) { - this.serviceUUID = serviceUUID; - this.characteristicUUID = characteristicUUID; - this.type = type; - } - - public BLECommand(UUID serviceUUID, UUID characteristicUUID, byte[] data, int type) { - this.serviceUUID = serviceUUID; - this.characteristicUUID = characteristicUUID; - this.data = data; - this.type = type; - } - - public int getType() { - return type; - } - - public UUID getServiceUUID() { - return serviceUUID; - } - - public UUID getCharacteristicUUID() { - return characteristicUUID; - } - - public byte[] getData() { - return data; - } -} \ No newline at end of file diff --git a/android/src/main/java/it/innove/BleManager.java b/android/src/main/java/it/innove/BleManager.java index e73bb33..dced268 100644 --- a/android/src/main/java/it/innove/BleManager.java +++ b/android/src/main/java/it/innove/BleManager.java @@ -1,62 +1,70 @@ package it.innove; +import static android.app.Activity.RESULT_OK; +import static android.bluetooth.BluetoothProfile.GATT; + +import android.annotation.SuppressLint; import android.app.Activity; -import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothManager; +import android.companion.CompanionDeviceManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; -import androidx.annotation.Nullable; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.ResultReceiver; -import android.preference.PreferenceManager; import android.util.Log; -import com.facebook.react.bridge.*; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.content.ContextCompat; +import androidx.core.content.IntentCompat; + import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.BaseActivityEventListener; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; -import com.facebook.react.modules.core.RCTNativeAppEventEmitter; +import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.LifecycleState; import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Method; -import java.util.*; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import static android.app.Activity.RESULT_OK; -import static android.bluetooth.BluetoothProfile.GATT; -import static android.os.Build.VERSION_CODES.LOLLIPOP; -import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; +public class BleManager extends NativeBleManagerSpec { -class BleManager extends ReactContextBaseJavaModule { - - public static final String LOG_TAG = "ReactNativeBleManager"; + public static final String LOG_TAG = "RNBleManager"; private static final int ENABLE_REQUEST = 539; - private class BondRequest { + private static class BondRequest { private String uuid; private String pin; private Callback callback; @@ -82,43 +90,111 @@ private class BondRequest { private BondRequest bondRequest; private BondRequest removeBondRequest; private boolean forceLegacy; + /** + * Used for companion scanning, if supported. + */ + private final @Nullable CompanionScanner companionScanner; + public static ReadableMap moduleOptions; + + /** PBSC: mirrors RN AppState host resume/pause for background BLE recovery. */ + private static volatile boolean hostResumed = false; + + private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper()); + + private final LifecycleEventListener hostLifecycleListener = new LifecycleEventListener() { + @Override + public void onHostResume() { + hostResumed = true; + } + + @Override + public void onHostPause() { + hostResumed = false; + } + + @Override + public void onHostDestroy() { + hostResumed = false; + } + }; + + static boolean isHostInForeground() { + return hostResumed; + } private ResultReceiver getReceiver(final Callback callback) { - return new ResultReceiver(new Handler()) { - protected void onReceiveResult(int resultCode, Bundle resultData) { - Log.d("ReactNativeBleManager", "Callback Invoked"); - ArrayList args = (ArrayList) new Gson().fromJson(resultData.getString("ARGS"), Object.class); - if(args != null) { - callback.invoke(args.toArray(new Object[args.size()])); - } else { - callback.invoke(); - } - - } - }; - } - - private ResultReceiver getEventReciever() { - return new ResultReceiver(new Handler()) { - protected void onReceiveResult(int resultCode, Bundle resultData) { - String eventName = resultData.getString("EVENTNAME"); - String paramsStr = resultData.getString("PARAMS"); - WritableMap params = null; - - if(paramsStr != null) { - JSONObject paramsObject = null; - try { - paramsObject = new JSONObject(paramsStr); - params = convertJsonToMap(paramsObject); - } catch (JSONException e) { - e.printStackTrace(); - return; - } - } - sendEvent(eventName, params); - } - }; - } + return new ResultReceiver(MAIN_HANDLER) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + ArrayList args = new Gson().fromJson(resultData.getString("ARGS"), ArrayList.class); + if (args != null) { + callback.invoke(args.toArray(new Object[args.size()])); + } else { + callback.invoke(); + } + } + }; + } + + private ResultReceiver getEventReciever() { + return new ResultReceiver(MAIN_HANDLER) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + String eventName = resultData.getString("EVENTNAME"); + String paramsStr = resultData.getString("PARAMS"); + WritableMap params = null; + if (paramsStr != null) { + try { + params = convertJsonToMap(new JSONObject(paramsStr)); + } catch (JSONException e) { + Log.e(LOG_TAG, "PBSC event params parse failed", e); + return; + } + } + switch (eventName) { + case "BleManagerConnectPeripheral": + emitOnConnectPeripheral(params); + break; + case "BleManagerDisconnectPeripheral": + emitOnDisconnectPeripheral(params); + break; + case "BleManagerDidUpdateValueForCharacteristic": + emitOnDidUpdateValueForCharacteristic(params); + break; + default: + Log.w(LOG_TAG, "Unknown PBSC event: " + eventName); + } + } + }; + } + + private void startPbscService(Intent intent) { + ReactApplicationContext context = getReactApplicationContext(); + try { + ContextCompat.startForegroundService(context, intent); + } catch (IllegalStateException e) { + // Android 12+ may block startForegroundService from background when the service + // is already running from an earlier in-foreground operation. + PbscLog.d("startForegroundService blocked, trying startService"); + try { + context.startService(intent); + } catch (Exception fallbackError) { + Log.e(LOG_TAG, "Failed to start PBSC service", fallbackError); + notifyServiceStartFailed(intent, "Foreground service not allowed"); + } + } + } + + private void notifyServiceStartFailed(Intent intent, String message) { + ResultReceiver receiver = IntentCompat.getParcelableExtra( + intent, "resultReciever", ResultReceiver.class); + if (receiver == null) { + return; + } + Bundle bundle = new Bundle(); + bundle.putString("ARGS", new Gson().toJson(new Object[]{message})); + receiver.send(0, bundle); + } public ReactApplicationContext getReactContext() { return reactContext; @@ -128,7 +204,7 @@ public ReactApplicationContext getReactContext() { @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { - Log.d(LOG_TAG, "onActivityResult"); + PbscLog.d( "onActivityResult"); if (requestCode == ENABLE_REQUEST && enableBluetoothCallback != null) { if (resultCode == RESULT_OK) { enableBluetoothCallback.invoke(); @@ -138,8 +214,119 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode, enableBluetoothCallback = null; } } + }; + private class MyBroadcastReceiver extends BroadcastReceiver { + private final BleManager bleManager; + + public MyBroadcastReceiver(BleManager bleManager) { + this.bleManager = bleManager; + } + + @SuppressLint("MissingPermission") + @Override + public void onReceive(Context context, Intent intent) { + PbscLog.d( "onReceive"); + final String action = intent.getAction(); + + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + String stringState = ""; + + switch (state) { + case BluetoothAdapter.STATE_OFF: + stringState = "off"; + clearPeripherals(); + break; + case BluetoothAdapter.STATE_TURNING_OFF: + stringState = "turning_off"; + disconnectPeripherals(); + break; + case BluetoothAdapter.STATE_ON: + stringState = "on"; + break; + case BluetoothAdapter.STATE_TURNING_ON: + stringState = "turning_on"; + break; + default: + // should not happen as per https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#EXTRA_STATE + stringState = "off"; + break; + } + + WritableMap map = Arguments.createMap(); + map.putString("state", stringState); + PbscLog.d( "state: " + stringState); + emitOnDidUpdateState(map); + + } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { + final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); + final int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, + BluetoothDevice.ERROR); + BluetoothDevice device; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); + } else { + device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + } + + String bondStateStr = "UNKNOWN"; + switch (bondState) { + case BluetoothDevice.BOND_BONDED: + bondStateStr = "BOND_BONDED"; + break; + case BluetoothDevice.BOND_BONDING: + bondStateStr = "BOND_BONDING"; + break; + case BluetoothDevice.BOND_NONE: + bondStateStr = "BOND_NONE"; + break; + } + PbscLog.d( "bond state: " + bondStateStr); + + if (bondRequest != null && bondRequest.uuid.equals(device.getAddress())) { + if (bondState == BluetoothDevice.BOND_BONDED) { + bondRequest.callback.invoke(); + bondRequest = null; + } else if (bondState == BluetoothDevice.BOND_NONE || bondState == BluetoothDevice.ERROR) { + bondRequest.callback.invoke("Bond request has been denied"); + bondRequest = null; + } + } + + if (bondState == BluetoothDevice.BOND_BONDED) { + Peripheral peripheral; + if (!forceLegacy) { + peripheral = new DefaultPeripheral(device, bleManager); + } else { + peripheral = new Peripheral(device, bleManager); + } + WritableMap map = peripheral.asWritableMap(); + emitOnPeripheralDidBond(map); + } + + if (removeBondRequest != null && removeBondRequest.uuid.equals(device.getAddress()) + && bondState == BluetoothDevice.BOND_NONE && prevState == BluetoothDevice.BOND_BONDED) { + removeBondRequest.callback.invoke(); + removeBondRequest = null; + } + } else if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) { + BluetoothDevice bluetoothDevice; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); + } else { + bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + } + if (bondRequest != null && bondRequest.uuid.equals(bluetoothDevice.getAddress()) && bondRequest.pin != null) { + bluetoothDevice.setPin(bondRequest.pin.getBytes()); + bluetoothDevice.createBond(); + } + } + + } + } + // key is the MAC Address private final Map peripherals = new LinkedHashMap<>(); // scan session id @@ -148,10 +335,20 @@ public BleManager(ReactApplicationContext reactContext) { super(reactContext); context = reactContext; this.reactContext = reactContext; + + boolean supportsCompanion = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP); + this.companionScanner = supportsCompanion + ? new CompanionScanner(reactContext, this) + : null; + reactContext.addActivityEventListener(mActivityEventListener); - Log.d(LOG_TAG, "BleManager created"); + hostResumed = reactContext.getLifecycleState() == LifecycleState.RESUMED; + reactContext.addLifecycleEventListener(hostLifecycleListener); + PbscLog.d( "BleManager created"); } + @NonNull @Override public String getName() { return "BleManager"; @@ -172,63 +369,79 @@ private BluetoothManager getBluetoothManager() { return bluetoothManager; } - public void sendEvent(String eventName, @Nullable WritableMap params) { - getReactApplicationContext().getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, params); - } - @ReactMethod public void start(ReadableMap options, Callback callback) { - Log.d(LOG_TAG, "start"); + PbscLog.d( "start"); if (getBluetoothAdapter() == null) { - Log.d(LOG_TAG, "No bluetooth support"); + PbscLog.d( "No bluetooth support"); callback.invoke("No bluetooth support"); return; } forceLegacy = false; + moduleOptions = options; if (options.hasKey("forceLegacy")) { forceLegacy = options.getBoolean("forceLegacy"); } - if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { - scanManager = new LollipopScanManager(reactContext, this); - } else { - scanManager = new LegacyScanManager(reactContext, this); - } + scanManager = new DefaultScanManager(reactContext, this); IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - context.registerReceiver(mReceiver, filter); IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - context.registerReceiver(mReceiver, intentFilter); + if (Build.VERSION.SDK_INT >= 34) { + // Google in 2023 decides that flag RECEIVER_NOT_EXPORTED or RECEIVER_EXPORTED should be explicit set SDK 34(UPSIDE_DOWN_CAKE) on registering receivers. + // Also the export flags are available on Android 8 and higher, should be used with caution so that don't break compability with that devices. + context.registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); + context.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED); + } else { + context.registerReceiver(mReceiver, filter); + context.registerReceiver(mReceiver, intentFilter); + } + callback.invoke(); - Log.d(LOG_TAG, "BleManager initialized"); + PbscLog.d( "BleManager initialized"); + } + + @ReactMethod + public void isStarted(Callback callback) { + PbscLog.d( "isStarted"); + callback.invoke(null, scanManager != null); } + @SuppressLint("MissingPermission") @ReactMethod public void enableBluetooth(Callback callback) { if (getBluetoothAdapter() == null) { - Log.d(LOG_TAG, "No bluetooth support"); + PbscLog.d( "No bluetooth support"); callback.invoke("No bluetooth support"); return; } if (!getBluetoothAdapter().isEnabled()) { - enableBluetoothCallback = callback; Intent intentEnable = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); if (getCurrentActivity() == null) callback.invoke("Current activity not available"); - else - getCurrentActivity().startActivityForResult(intentEnable, ENABLE_REQUEST); + else { + enableBluetoothCallback = callback; + try { + getCurrentActivity().startActivityForResult(intentEnable, ENABLE_REQUEST); + } catch (Exception e) { + enableBluetoothCallback = null; + callback.invoke("Error starting enable bluetooth activity"); + } + + } + } else callback.invoke(); } @ReactMethod - public void scan(ReadableArray serviceUUIDs, final int scanSeconds, boolean allowDuplicates, ReadableMap options, + public void scan(ReadableMap scanningOptions, Callback callback) { - Log.d(LOG_TAG, "scan"); + PbscLog.d( "scan"); if (getBluetoothAdapter() == null) { - Log.d(LOG_TAG, "No bluetooth support"); + PbscLog.d( "No bluetooth support"); callback.invoke("No bluetooth support"); return; } @@ -247,14 +460,29 @@ public void scan(ReadableArray serviceUUIDs, final int scanSeconds, boolean allo } if (scanManager != null) - scanManager.scan(serviceUUIDs, scanSeconds, options, callback); + scanManager.scan(scanningOptions, callback); + } + + @SuppressLint("NewApi") // NOTE: constructor checks the API version. + @ReactMethod + public void companionScan(ReadableArray serviceUUIDs, ReadableMap options, Callback callback) { + if (this.companionScanner == null) { + callback.invoke("not supported"); + } else { + this.companionScanner.scan(serviceUUIDs, options, callback); + } + } + + @ReactMethod + public void supportsCompanion(Callback callback) { + callback.invoke(companionScanner != null); } @ReactMethod public void stopScan(Callback callback) { - Log.d(LOG_TAG, "Stop scan"); + PbscLog.d( "Stop scan"); if (getBluetoothAdapter() == null) { - Log.d(LOG_TAG, "No bluetooth support"); + PbscLog.d( "No bluetooth support"); callback.invoke("No bluetooth support"); return; } @@ -265,14 +493,16 @@ public void stopScan(Callback callback) { if (scanManager != null) { scanManager.stopScan(callback); WritableMap map = Arguments.createMap(); - map.putInt("status", 0); - sendEvent("BleManagerStopScan", map); + map.putInt("status", 0); + emitOnStopScan(map); } } + + @SuppressLint("MissingPermission") @ReactMethod public void createBond(String peripheralUUID, String peripheralPin, Callback callback) { - Log.d(LOG_TAG, "Request bond to: " + peripheralUUID); + PbscLog.d( "Request bond to: " + peripheralUUID); Set deviceSet = getBluetoothAdapter().getBondedDevices(); for (BluetoothDevice device : deviceSet) { @@ -290,8 +520,8 @@ public void createBond(String peripheralUUID, String peripheralPin, Callback cal callback.invoke("Only allow one bond request at a time"); return; } else if (peripheral.getDevice().createBond()) { - Log.d(LOG_TAG, "Request bond successful for: " + peripheralUUID); - bondRequest = new BondRequest(peripheralUUID, peripheralPin, callback); // request bond success, waiting for boradcast + PbscLog.d( "Request bond successful for: " + peripheralUUID); + bondRequest = new BondRequest(peripheralUUID, peripheralPin, callback); // request bond success, waiting for broadcast return; } @@ -299,8 +529,8 @@ public void createBond(String peripheralUUID, String peripheralPin, Callback cal } @ReactMethod - private void removeBond(String peripheralUUID, Callback callback) { - Log.d(LOG_TAG, "Remove bond to: " + peripheralUUID); + public void removeBond(String peripheralUUID, Callback callback) { + PbscLog.d( "Remove bond to: " + peripheralUUID); Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); if (peripheral == null) { @@ -313,7 +543,7 @@ private void removeBond(String peripheralUUID, Callback callback) { removeBondRequest = new BondRequest(peripheralUUID, callback); return; } catch (Exception e) { - Log.d(LOG_TAG, "Error in remove bond: " + peripheralUUID, e); + PbscLog.d( "Error in remove bond: " + peripheralUUID, e); callback.invoke("Remove bond request fail"); } } @@ -321,401 +551,428 @@ private void removeBond(String peripheralUUID, Callback callback) { } @ReactMethod - public void connect(String peripheralUUID, Callback callback) { - Log.d(LOG_TAG, "Connect to: " + peripheralUUID); - - Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); - if (peripheral == null) { - callback.invoke("Invalid peripheral uuid"); - return; - } - //peripheral.connect(callback, getCurrentActivity()); - //Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", peripheralUUID) - .putExtra("ACTION", "CONNECT") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); - } + public void connect(String peripheralUUID, ReadableMap options, Callback callback) { + PbscLog.d( "Connect to: " + peripheralUUID); + + Peripheral peripheral = retrieveOrCreatePeripheral(peripheralUUID); + if (peripheral == null) { + callback.invoke("Invalid peripheral uuid"); + return; + } + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", peripheralUUID) + .putExtra("ACTION", "CONNECT") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); + } @ReactMethod public void disconnect(String peripheralUUID, boolean force, Callback callback) { - Log.d(LOG_TAG, "Disconnect from: " + peripheralUUID); + PbscLog.d( "Disconnect from: " + peripheralUUID); Peripheral peripheral = peripherals.get(peripheralUUID); if (peripheral != null) { - //peripheral.disconnect(callback, force); - //callback.invoke(); - //Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) .putExtra("UUID", peripheralUUID) .putExtra("ACTION", "DISCONNECT") .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } - // This method added after upgrade 6.5 to 8.5 + private static SharedPreferences getDefaultSharedPreferences(Context context) { + Context appContext = context.getApplicationContext(); + return appContext.getSharedPreferences( + appContext.getPackageName() + "_preferences", + Context.MODE_PRIVATE); + } + @ReactMethod - public void startNotificationUseBuffer(String deviceUUID, String serviceUUID, String characteristicUUID, - Integer buffer, Callback callback) { - Log.d(LOG_TAG, "startNotification"); + public void setServiceRecoveryData(ReadableMap data, Callback callback) { + if (data != null) { + try { + getDefaultSharedPreferences(getReactApplicationContext()) + .edit() + .putString("serviceRecoveryData", convertMapToJson(data).toString()) + .commit(); + } catch (JSONException e) { + callback.invoke("Write service recovery data failed due to JSONException"); + return; + } + } else { + getDefaultSharedPreferences(getReactApplicationContext()) + .edit() + .putString("serviceRecoveryData", new JsonObject().toString()) + .commit(); + } + callback.invoke(); + } + + @ReactMethod + public void startNotificationWithBuffer(String deviceUUID, String serviceUUID, String characteristicUUID, + double bufferLength, Callback callback) { + PbscLog.d( "startNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { peripheral.registerNotify(UUIDHelper.uuidFromString(serviceUUID), - UUIDHelper.uuidFromString(characteristicUUID), buffer, callback); + UUIDHelper.uuidFromString(characteristicUUID), (int) bufferLength, callback); } else callback.invoke("Peripheral not found"); } @ReactMethod public void startNotification(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { - Log.d(LOG_TAG, "startNotification"); + PbscLog.d( "startNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.registerNotify(UUIDHelper.uuidFromString(serviceUUID), - //UUIDHelper.uuidFromString(characteristicUUID), 1, callback); - //Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("SERVICEUUID", serviceUUID) - .putExtra("CHARACTERISTICUUID", characteristicUUID) - .putExtra("ACTION", "STARTNOTIFICATION") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("SERVICEUUID", serviceUUID) + .putExtra("CHARACTERISTICUUID", characteristicUUID) + .putExtra("ACTION", "STARTNOTIFICATION") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } @ReactMethod public void stopNotification(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { - Log.d(LOG_TAG, "stopNotification"); + PbscLog.d( "stopNotification"); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.removeNotify(UUIDHelper.uuidFromString(serviceUUID), - //UUIDHelper.uuidFromString(characteristicUUID), callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) .putExtra("UUID", deviceUUID) .putExtra("SERVICEUUID", serviceUUID) .putExtra("CHARACTERISTICUUID", characteristicUUID) .putExtra("ACTION", "STOPNOTIFICATION") .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } - // Added by PBSC - @ReactMethod - public void setServiceRecoveryData(ReadableMap data, Callback callback) { - // sets last ble usage for recovery by service - if(data != null) { - try { - PreferenceManager.getDefaultSharedPreferences(getReactApplicationContext()).edit().putString("serviceRecoveryData", convertMapToJson(data).toString()).commit(); - } catch (JSONException e) { - callback.invoke("Write service recovery data failed due to JSONException"); - e.printStackTrace(); - } - } else { - PreferenceManager.getDefaultSharedPreferences(getReactApplicationContext()).edit().putString("serviceRecoveryData", new JsonObject().toString()).commit(); - } - callback.invoke(); - } - @ReactMethod public void write(String deviceUUID, String serviceUUID, String characteristicUUID, ReadableArray message, - Integer maxByteSize, Callback callback) { - Log.d(LOG_TAG, "Write to: " + deviceUUID); + double maxByteSize, Callback callback) { + PbscLog.d( "Write to: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { byte[] decoded = new byte[message.size()]; for (int i = 0; i < message.size(); i++) { - decoded[i] = new Integer(message.getInt(i)).byteValue(); + decoded[i] = Integer.valueOf(message.getInt(i)).byteValue(); } String strMessage = bytesToHex(decoded); - Log.d(LOG_TAG, "Message(" + decoded.length + "): " + strMessage); - //peripheral.write(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), - // decoded, maxByteSize, null, callback, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("SERVICEUUID", serviceUUID) - .putExtra("DECODED", decoded) - .putExtra("MESSAGE", strMessage) - .putExtra("MAXBYTESIZE", maxByteSize) - .putExtra("CHARACTERISTICUUID", characteristicUUID) - .putExtra("ACTION", "WRITE") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + PbscLog.d( "Message(" + decoded.length + "): " + strMessage); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("SERVICEUUID", serviceUUID) + .putExtra("DECODED", decoded) + .putExtra("MESSAGE", strMessage) + .putExtra("MAXBYTESIZE", (int) maxByteSize) + .putExtra("CHARACTERISTICUUID", characteristicUUID) + .putExtra("ACTION", "WRITE") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } @ReactMethod public void writeWithoutResponse(String deviceUUID, String serviceUUID, String characteristicUUID, - ReadableArray message, Integer maxByteSize, Integer queueSleepTime, Callback callback) { - Log.d(LOG_TAG, "Write without response to: " + deviceUUID); + ReadableArray message, double maxByteSize, double queueSleepTime, Callback callback) { + PbscLog.d( "Write without response to: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { byte[] decoded = new byte[message.size()]; for (int i = 0; i < message.size(); i++) { - decoded[i] = new Integer(message.getInt(i)).byteValue(); + decoded[i] = Integer.valueOf(message.getInt(i)).byteValue(); } - Log.d(LOG_TAG, "Message(" + decoded.length + "): " + bytesToHex(decoded)); - //peripheral.write(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), - // decoded, maxByteSize, queueSleepTime, callback, BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("SERVICEUUID", serviceUUID) - .putExtra("DECODED", decoded) - .putExtra("MAXBYTESIZE", maxByteSize) - .putExtra("CHARACTERISTICUUID", characteristicUUID) - .putExtra("ACTION", "WRITEWITHOUTRESPONSE") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + PbscLog.d( "Message(" + decoded.length + "): " + bytesToHex(decoded)); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("SERVICEUUID", serviceUUID) + .putExtra("DECODED", decoded) + .putExtra("MAXBYTESIZE", (int) maxByteSize) + .putExtra("CHARACTERISTICUUID", characteristicUUID) + .putExtra("ACTION", "WRITEWITHOUTRESPONSE") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } @ReactMethod public void read(String deviceUUID, String serviceUUID, String characteristicUUID, Callback callback) { - Log.d(LOG_TAG, "Read from: " + deviceUUID); + PbscLog.d( "Read from: " + deviceUUID); if (serviceUUID == null || characteristicUUID == null) { callback.invoke("ServiceUUID and characteristicUUID required."); return; } - // Added by PBSC - ResultReceiver reciever = new ResultReceiver(new Handler()) { - protected void onReceiveResult(int resultCode, Bundle resultData) { - Log.d("ReactNativeBleManager", "Callback Invoked"); - ArrayList args = (ArrayList) new Gson().fromJson(resultData.getString("ARGS"), Object.class); - String paramsStr = resultData.getString("MAP"); - WritableArray params = null; - if(paramsStr != null) { - JSONArray paramsObject = null; - try { - paramsObject = new JSONArray(paramsStr); - params = convertJsonToArray(paramsObject); - } catch (JSONException e) { - e.printStackTrace(); - callback.invoke(); - return; - } - if(args != null) { - args.add(params); - } else { - args = new ArrayList(); - args.add(null); - args.add(params); - } - } - if(args != null) { - callback.invoke(args.toArray(new Object[args.size()])); - } else { - callback.invoke(); - } - } - }; + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID); + return; + } + ResultReceiver reciever = new ResultReceiver(MAIN_HANDLER) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + ArrayList args = new Gson().fromJson(resultData.getString("ARGS"), ArrayList.class); + String paramsStr = resultData.getString("MAP"); + if (paramsStr != null) { + try { + WritableArray params = convertJsonToArray(new JSONArray(paramsStr)); + if (args != null) { + args.add(params); + } else { + args = new ArrayList(); + args.add(null); + args.add(params); + } + } catch (JSONException e) { + callback.invoke(); + return; + } + } + if (args != null) { + callback.invoke(args.toArray(new Object[args.size()])); + } else { + callback.invoke(); + } + } + }; Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.read(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), - // callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("SERVICEUUID", serviceUUID) - .putExtra("CHARACTERISTICUUID", characteristicUUID) - .putExtra("ACTION", "READ") - .putExtra("resultReciever", reciever) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("SERVICEUUID", serviceUUID) + .putExtra("CHARACTERISTICUUID", characteristicUUID) + .putExtra("ACTION", "READ") + .putExtra("resultReciever", reciever) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found", null); } + @ReactMethod + public void readDescriptor(String deviceUUID, String serviceUUID, String characteristicUUID, String descriptorUUID, Callback callback) { + PbscLog.d( "Read descriptor from: " + deviceUUID); + if (serviceUUID == null || characteristicUUID == null || descriptorUUID == null) { + callback.invoke("ServiceUUID, CharacteristicUUID and descriptorUUID required.", null); + return; + } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID, null); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID, null); + return; + } + if (!UUIDHelper.isValidBLEUUID(descriptorUUID)) { + callback.invoke("Invalid descriptor UUID format: " + descriptorUUID, null); + return; + } + + Peripheral peripheral = peripherals.get(deviceUUID); + if (peripheral == null) { + callback.invoke("Peripheral not found", null); + } else if (!peripheral.isConnected()) { + callback.invoke("Peripheral not connected", null); + } else { + peripheral.readDescriptor( + UUIDHelper.uuidFromString(serviceUUID), + UUIDHelper.uuidFromString(characteristicUUID), + UUIDHelper.uuidFromString(descriptorUUID), + callback); + } + } + + @ReactMethod + public void writeDescriptor(String deviceUUID, String serviceUUID, String characteristicUUID, String descriptorUUID, ReadableArray message, Callback callback) { + PbscLog.d( "Write descriptor from: " + deviceUUID); + if (serviceUUID == null || characteristicUUID == null || descriptorUUID == null) { + callback.invoke("ServiceUUID, CharacteristicUUID and descriptorUUID required.", null); + return; + } + // Validate UUID formats to prevent crash + if (!UUIDHelper.isValidBLEUUID(serviceUUID)) { + callback.invoke("Invalid service UUID format: " + serviceUUID, null); + return; + } + if (!UUIDHelper.isValidBLEUUID(characteristicUUID)) { + callback.invoke("Invalid characteristic UUID format: " + characteristicUUID, null); + return; + } + if (!UUIDHelper.isValidBLEUUID(descriptorUUID)) { + callback.invoke("Invalid descriptor UUID format: " + descriptorUUID, null); + return; + } + + Peripheral peripheral = peripherals.get(deviceUUID); + if (peripheral == null) { + callback.invoke("Peripheral not found", null); + } else if (!peripheral.isConnected()) { + callback.invoke("Peripheral not connected", null); + } else { + byte[] decoded = new byte[message.size()]; + for (int i = 0; i < message.size(); i++) { + decoded[i] = Integer.valueOf(message.getInt(i)).byteValue(); + } + PbscLog.d( "Message(" + decoded.length + "): " + bytesToHex(decoded)); + peripheral.writeDescriptor(UUIDHelper.uuidFromString(serviceUUID), UUIDHelper.uuidFromString(characteristicUUID), UUIDHelper.uuidFromString(descriptorUUID), decoded, callback); + } + } + @ReactMethod public void retrieveServices(String deviceUUID, ReadableArray services, Callback callback) { - Log.d(LOG_TAG, "Retrieve services from: " + deviceUUID); - // Added by PBSC - ResultReceiver reciever = new ResultReceiver(new Handler()) { + PbscLog.d( "Retrieve services from: " + deviceUUID); + ResultReceiver reciever = new ResultReceiver(MAIN_HANDLER) { + @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - Log.d("ReactNativeBleManager", "Callback Invoked"); - ArrayList args = (ArrayList) new Gson().fromJson(resultData.getString("ARGS"), Object.class); + ArrayList args = new Gson().fromJson(resultData.getString("ARGS"), ArrayList.class); String paramsStr = resultData.getString("MAP"); - WritableMap params = null; - - if(paramsStr != null) { - JSONObject paramsObject = null; + if (paramsStr != null) { try { - paramsObject = new JSONObject(paramsStr); - params = convertJsonToMap(paramsObject); + WritableMap params = convertJsonToMap(new JSONObject(paramsStr)); + if (args != null) { + args.add(params); + } else { + args = new ArrayList(); + args.add(null); + args.add(params); + } } catch (JSONException e) { callback.invoke(); return; } - if(args != null) { - args.add(params); - } else { - args = new ArrayList(); - args.add(null); - args.add(params); - } } - if(args != null) { + if (args != null) { callback.invoke(args.toArray(new Object[args.size()])); } else { callback.invoke(); } - } }; Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.retrieveServices(callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("ACTION", "RETRIEVESERVICES") - .putExtra("resultReciever", reciever) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("ACTION", "RETRIEVESERVICES") + .putExtra("resultReciever", reciever) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found", null); } - // Added by PBSC - private static WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException { - WritableMap map = new WritableNativeMap(); - Iterator iterator = jsonObject.keys(); - while (iterator.hasNext()) { - String key = iterator.next(); - Object value = jsonObject.get(key); - if (value instanceof JSONObject) { - map.putMap(key, convertJsonToMap((JSONObject) value)); - } else if (value instanceof JSONArray) { - map.putArray(key, convertJsonToArray((JSONArray) value)); - } else if (value instanceof Boolean) { - map.putBoolean(key, (Boolean) value); - } else if (value instanceof Integer) { - map.putInt(key, (Integer) value); - } else if (value instanceof Double) { - map.putDouble(key, (Double) value); - } else if (value instanceof String) { - map.putString(key, (String) value); - } else { - map.putString(key, value.toString()); - } - } - return map; - } - - // Added by PBSC - private static WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException { - WritableArray array = new WritableNativeArray(); - for (int i = 0; i < jsonArray.length(); i++) { - Object value = jsonArray.get(i); - if (value instanceof JSONObject) { - array.pushMap(convertJsonToMap((JSONObject) value)); - } else if (value instanceof JSONArray) { - array.pushArray(convertJsonToArray((JSONArray) value)); - } else if (value instanceof Boolean) { - array.pushBoolean((Boolean) value); - } else if (value instanceof Integer) { - array.pushInt((Integer) value); - } else if (value instanceof Double) { - array.pushDouble((Double) value); - } else if (value instanceof String) { - array.pushString((String) value); - } else { - array.pushString(value.toString()); - } - } - return array; - } - @ReactMethod public void refreshCache(String deviceUUID, Callback callback) { - Log.d(LOG_TAG, "Refershing cache for: " + deviceUUID); + PbscLog.d( "Refreshing cache for: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.refreshCache(callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("ACTION", "REFRESHCACHE") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("ACTION", "REFRESHCACHE") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found"); } @ReactMethod public void readRSSI(String deviceUUID, Callback callback) { - Log.d(LOG_TAG, "Read RSSI from: " + deviceUUID); + PbscLog.d( "Read RSSI from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.readRSSI(callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) .putExtra("UUID", deviceUUID) .putExtra("ACTION", "READRSSI") .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + .putExtra("eventReciever", getEventReciever())); } else callback.invoke("Peripheral not found", null); } - // This method added after upgrade 6.5 to 8.5 - private Peripheral savePeripheral(BluetoothDevice device) { + public Peripheral savePeripheral(BluetoothDevice device) { String address = device.getAddress(); synchronized (peripherals) { if (!peripherals.containsKey(address)) { Peripheral peripheral; - if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { - peripheral = new LollipopPeripheral(device, reactContext); + if (!forceLegacy) { + peripheral = new DefaultPeripheral(device, this); } else { - peripheral = new Peripheral(device, reactContext); + peripheral = new Peripheral(device, this); } peripherals.put(device.getAddress(), peripheral); } @@ -723,13 +980,11 @@ private Peripheral savePeripheral(BluetoothDevice device) { return peripherals.get(address); } - // This method added after upgrade 6.5 to 8.5 public Peripheral getPeripheral(BluetoothDevice device) { String address = device.getAddress(); return peripherals.get(address); } - // This method added after upgrade 6.5 to 8.5 public Peripheral savePeripheral(Peripheral peripheral) { synchronized (peripherals) { peripherals.put(peripheral.getDevice().getAddress(), peripheral); @@ -738,8 +993,8 @@ public Peripheral savePeripheral(Peripheral peripheral) { } @ReactMethod - public void checkState() { - Log.d(LOG_TAG, "checkState"); + public void checkState(Callback callback) { + PbscLog.d( "checkState"); BluetoothAdapter adapter = getBluetoothAdapter(); String state = "off"; @@ -753,125 +1008,59 @@ public void checkState() { case BluetoothAdapter.STATE_TURNING_ON: state = "turning_on"; break; - case BluetoothAdapter.STATE_OFF: - state = "off"; - break; case BluetoothAdapter.STATE_TURNING_OFF: state = "turning_off"; + if (scanManager != null) { + scanManager.setScanning(false); + } break; + case BluetoothAdapter.STATE_OFF: default: // should not happen as per https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#getState() state = "off"; + if (scanManager != null) { + scanManager.setScanning(false); + } break; } } WritableMap map = Arguments.createMap(); map.putString("state", state); - Log.d(LOG_TAG, "state:" + state); - sendEvent("BleManagerDidUpdateState", map); + PbscLog.d( "state:" + state); + emitOnDidUpdateState(map); + callback.invoke(state); } - // This method added after upgrade 6.5 to 8.5 @ReactMethod - public void setName(String name) { - BluetoothAdapter adapter = getBluetoothAdapter(); - adapter.setName(name); + public void isScanning(Callback callback) { + if (scanManager != null) { + callback.invoke(null, scanManager.isScanning()); + } else { + callback.invoke(null, false); + } } - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(LOG_TAG, "onReceive"); - final String action = intent.getAction(); - - if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - String stringState = ""; - - switch (state) { - case BluetoothAdapter.STATE_OFF: - stringState = "off"; - clearPeripherals(); - break; - case BluetoothAdapter.STATE_TURNING_OFF: - stringState = "turning_off"; - disconnectPeripherals(); - break; - case BluetoothAdapter.STATE_ON: - stringState = "on"; - break; - case BluetoothAdapter.STATE_TURNING_ON: - stringState = "turning_on"; - break; - default: - // should not happen as per https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#EXTRA_STATE - stringState = "off"; - break; - } - - WritableMap map = Arguments.createMap(); - map.putString("state", stringState); - Log.d(LOG_TAG, "state: " + stringState); - sendEvent("BleManagerDidUpdateState", map); - - } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { - final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR); - final int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, - BluetoothDevice.ERROR); - BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - - String bondStateStr = "UNKNOWN"; - switch (bondState) { - case BluetoothDevice.BOND_BONDED: - bondStateStr = "BOND_BONDED"; - break; - case BluetoothDevice.BOND_BONDING: - bondStateStr = "BOND_BONDING"; - break; - case BluetoothDevice.BOND_NONE: - bondStateStr = "BOND_NONE"; - break; - } - Log.d(LOG_TAG, "bond state: " + bondStateStr); + @Override + public void getMaximumWriteValueLengthForWithoutResponse(String peripheralUUID, Callback callback) { + callback.invoke("Not implemented"); + } - if (bondRequest != null && bondRequest.uuid.equals(device.getAddress())) { - if (bondState == BluetoothDevice.BOND_BONDED) { - bondRequest.callback.invoke(); - bondRequest = null; - } else if (bondState == BluetoothDevice.BOND_NONE || bondState == BluetoothDevice.ERROR) { - bondRequest.callback.invoke("Bond request has been denied"); - bondRequest = null; - } - } + @Override + public void getMaximumWriteValueLengthForWithResponse(String deviceUUID, Callback callback) { + callback.invoke("Not implemented"); + } - if (bondState == BluetoothDevice.BOND_BONDED) { - Peripheral peripheral; - if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { - peripheral = new LollipopPeripheral(device, reactContext); - } else { - peripheral = new Peripheral(device, reactContext); - } - WritableMap map = peripheral.asWritableMap(); - sendEvent("BleManagerPeripheralDidBond", map); - } + @ReactMethod + @SuppressLint("MissingPermission") + public void setName(String name) { + BluetoothAdapter adapter = getBluetoothAdapter(); + adapter.setName(name); + } - if (removeBondRequest != null && removeBondRequest.uuid.equals(device.getAddress()) - && bondState == BluetoothDevice.BOND_NONE && prevState == BluetoothDevice.BOND_BONDED) { - removeBondRequest.callback.invoke(); - removeBondRequest = null; - } - } else if (action.equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) { - BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (bondRequest != null && bondRequest.uuid.equals(bluetoothDevice.getAddress()) && bondRequest.pin != null) { - bluetoothDevice.setPin(bondRequest.pin.getBytes()); - bluetoothDevice.createBond(); - } - } - } + private final BroadcastReceiver mReceiver = new MyBroadcastReceiver(this) { }; - // This method added after upgrade 6.5 to 8.5 private void clearPeripherals() { if (!peripherals.isEmpty()) { synchronized (peripherals) { @@ -880,14 +1069,15 @@ private void clearPeripherals() { } } - // This method added after upgrade 6.5 to 8.5 private void disconnectPeripherals() { if (!peripherals.isEmpty()) { synchronized (peripherals) { for (Peripheral peripheral : peripherals.values()) { if (peripheral.isConnected()) { - peripheral.disconnect(null,true); + peripheral.disconnect(null, true); } + peripheral.errorAndClearAllCallbacks("disconnected by BleManager"); + peripheral.resetQueuesAndBuffers(); } } } @@ -895,7 +1085,7 @@ private void disconnectPeripherals() { @ReactMethod public void getDiscoveredPeripherals(Callback callback) { - Log.d(LOG_TAG, "Get discovered peripherals"); + PbscLog.d( "Get discovered peripherals"); WritableArray map = Arguments.createArray(); synchronized (peripherals) { for (Map.Entry entry : peripherals.entrySet()) { @@ -907,19 +1097,20 @@ public void getDiscoveredPeripherals(Callback callback) { callback.invoke(null, map); } + @SuppressLint("MissingPermission") @ReactMethod public void getConnectedPeripherals(ReadableArray serviceUUIDs, Callback callback) { - Log.d(LOG_TAG, "Get connected peripherals"); + PbscLog.d( "Get connected peripherals"); WritableArray map = Arguments.createArray(); if (getBluetoothAdapter() == null) { - Log.d(LOG_TAG, "No bluetooth support"); + PbscLog.d( "No bluetooth support"); callback.invoke("No bluetooth support"); return; } - List periperals = getBluetoothManager().getConnectedDevices(GATT); - for (BluetoothDevice entry : periperals) { + List peripherals = getBluetoothManager().getConnectedDevices(GATT); + for (BluetoothDevice entry : peripherals) { Peripheral peripheral = savePeripheral(entry); WritableMap jsonBundle = peripheral.asWritableMap(); map.pushMap(jsonBundle); @@ -927,17 +1118,28 @@ public void getConnectedPeripherals(ReadableArray serviceUUIDs, Callback callbac callback.invoke(null, map); } + @Override + public void isPeripheralConnected(String deviceUUID, Callback callback) { + PbscLog.d( "Checking connection state for: " + deviceUUID); + Peripheral peripheral = peripherals.get(deviceUUID); + if (peripheral != null) { + callback.invoke(null, peripheral.isConnected()); + } else + callback.invoke("Peripheral not found"); + } + + @SuppressLint("MissingPermission") @ReactMethod public void getBondedPeripherals(Callback callback) { - Log.d(LOG_TAG, "Get bonded peripherals"); + PbscLog.d( "Get bonded peripherals"); WritableArray map = Arguments.createArray(); Set deviceSet = getBluetoothAdapter().getBondedDevices(); for (BluetoothDevice device : deviceSet) { Peripheral peripheral; - if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { - peripheral = new LollipopPeripheral(device, reactContext); + if (!forceLegacy) { + peripheral = new DefaultPeripheral(device, this); } else { - peripheral = new Peripheral(device, reactContext); + peripheral = new Peripheral(device, this); } WritableMap jsonBundle = peripheral.asWritableMap(); map.pushMap(jsonBundle); @@ -947,7 +1149,7 @@ public void getBondedPeripherals(Callback callback) { @ReactMethod public void removePeripheral(String deviceUUID, Callback callback) { - Log.d(LOG_TAG, "Removing from list: " + deviceUUID); + PbscLog.d( "Removing from list: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { synchronized (peripherals) { @@ -963,45 +1165,79 @@ public void removePeripheral(String deviceUUID, Callback callback) { } @ReactMethod - public void requestConnectionPriority(String deviceUUID, int connectionPriority, Callback callback) { - Log.d(LOG_TAG, "Request connection priority of " + connectionPriority + " from: " + deviceUUID); + public void requestConnectionPriority(String deviceUUID, double connectionPriority, Callback callback) { + PbscLog.d( "Request connection priority of " + connectionPriority + " from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.requestConnectionPriority(connectionPriority, callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("CONNECTIONPRIORITY", connectionPriority) - .putExtra("ACTION", "REQUESTCONNECTIONPRIORITY") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("CONNECTIONPRIORITY", (int) connectionPriority) + .putExtra("ACTION", "REQUESTCONNECTIONPRIORITY") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else { callback.invoke("Peripheral not found", null); } } @ReactMethod - public void requestMTU(String deviceUUID, int mtu, Callback callback) { - Log.d(LOG_TAG, "Request MTU of " + mtu + " bytes from: " + deviceUUID); + public void requestMTU(String deviceUUID, double mtu, Callback callback) { + PbscLog.d( "Request MTU of " + mtu + " bytes from: " + deviceUUID); Peripheral peripheral = peripherals.get(deviceUUID); if (peripheral != null) { - //peripheral.requestMTU(mtu, callback); - // Added by PBSC - Intent serviceIntent = new Intent(getReactApplicationContext(), PeripheralService.class) - .putExtra("UUID", deviceUUID) - .putExtra("MTU", mtu) - .putExtra("ACTION", "REQUESTMTU") - .putExtra("resultReciever", getReceiver(callback)) - .putExtra("eventReciever", getEventReciever()); - - getReactApplicationContext().startService(serviceIntent); + startPbscService(new Intent(getReactApplicationContext(), PeripheralService.class) + .putExtra("UUID", deviceUUID) + .putExtra("MTU", (int) mtu) + .putExtra("ACTION", "REQUESTMTU") + .putExtra("resultReciever", getReceiver(callback)) + .putExtra("eventReciever", getEventReciever())); } else { callback.invoke("Peripheral not found", null); } } + @ReactMethod + public void getAssociatedPeripherals(Callback callback) { + PbscLog.d( "Get associated peripherals"); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + callback.invoke("Not supported"); + return; + } + + WritableArray peripherals = Arguments.createArray(); + for (String address : ((CompanionDeviceManager) getCompanionDeviceManager()).getAssociations()) { + peripherals.pushMap(retrieveOrCreatePeripheral(address).asWritableMap()); + } + + callback.invoke(null, peripherals); + } + + @ReactMethod + public void removeAssociatedPeripheral(String address, Callback callback) { + PbscLog.d( "Remove associated peripheral: " + address); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + callback.invoke("Not supported"); + return; + } + + CompanionDeviceManager manager = (CompanionDeviceManager) getCompanionDeviceManager(); + for (String association : manager.getAssociations()) { + if (association.equals(address)) { + manager.disassociate(address); + callback.invoke(); + return; + } + } + + callback.invoke("device not found"); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public Object getCompanionDeviceManager() { + return reactContext + .getCurrentActivity().getSystemService(Context.COMPANION_DEVICE_SERVICE); + } + private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); public static String bytesToHex(byte[] bytes) { @@ -1031,10 +1267,10 @@ private Peripheral retrieveOrCreatePeripheral(String peripheralUUID) { } if (BluetoothAdapter.checkBluetoothAddress(peripheralUUID)) { BluetoothDevice device = bluetoothAdapter.getRemoteDevice(peripheralUUID); - if (Build.VERSION.SDK_INT >= LOLLIPOP && !forceLegacy) { - peripheral = new LollipopPeripheral(device, reactContext); + if (!forceLegacy) { + peripheral = new DefaultPeripheral(device, this); } else { - peripheral = new Peripheral(device, reactContext); + peripheral = new Peripheral(device, this); } peripherals.put(peripheralUUID, peripheral); } @@ -1043,89 +1279,140 @@ private Peripheral retrieveOrCreatePeripheral(String peripheralUUID) { return peripheral; } - // This method added after upgrade 6.5 to 8.5 @ReactMethod public void addListener(String eventName) { // Keep: Required for RN built in Event Emitter Calls. } - // This method added after upgrade 6.5 to 8.5 @ReactMethod - public void removeListeners(Integer count) { + public void removeListeners(double count) { // Keep: Required for RN built in Event Emitter Calls. } - // This method added after upgrade 6.5 to 8.5 @Override - public void onCatalystInstanceDestroy() { + public void invalidate() { + reactContext.removeLifecycleEventListener(hostLifecycleListener); + try { + context.unregisterReceiver(mReceiver); + } catch (Exception e) { + Log.e(LOG_TAG, "Receiver not registered or already unregistered", e); + } try { // Disconnect all known peripherals, otherwise android system will think we are still connected // while we have lost the gatt instance disconnectPeripherals(); - }catch(Exception e) { - Log.d(LOG_TAG, "Could not disconnect peripherals", e); + } catch (Exception e) { + PbscLog.d( "Could not disconnect peripherals", e); } if (scanManager != null) { // Stop scan in case one was started to stop events from being emitted after destroy - scanManager.stopScan(args -> {}); + scanManager.stopScan(args -> { + }); + } + } + + private static WritableMap convertJsonToMap(JSONObject jsonObject) throws JSONException { + WritableMap map = new WritableNativeMap(); + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof JSONObject) { + map.putMap(key, convertJsonToMap((JSONObject) value)); + } else if (value instanceof JSONArray) { + map.putArray(key, convertJsonToArray((JSONArray) value)); + } else if (value instanceof Boolean) { + map.putBoolean(key, (Boolean) value); + } else if (value instanceof Integer) { + map.putInt(key, (Integer) value); + } else if (value instanceof Double) { + map.putDouble(key, (Double) value); + } else if (value instanceof String) { + map.putString(key, (String) value); + } else { + map.putString(key, value.toString()); + } } + return map; + } + + private static WritableArray convertJsonToArray(JSONArray jsonArray) throws JSONException { + WritableArray array = new WritableNativeArray(); + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof JSONObject) { + array.pushMap(convertJsonToMap((JSONObject) value)); + } else if (value instanceof JSONArray) { + array.pushArray(convertJsonToArray((JSONArray) value)); + } else if (value instanceof Boolean) { + array.pushBoolean((Boolean) value); + } else if (value instanceof Integer) { + array.pushInt((Integer) value); + } else if (value instanceof Double) { + array.pushDouble((Double) value); + } else if (value instanceof String) { + array.pushString((String) value); + } else { + array.pushString(value.toString()); + } + } + return array; } - // Added by PBSC private static JSONObject convertMapToJson(ReadableMap readableMap) throws JSONException { - JSONObject object = new JSONObject(); - ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - switch (readableMap.getType(key)) { - case Null: - object.put(key, JSONObject.NULL); - break; - case Boolean: - object.put(key, readableMap.getBoolean(key)); - break; - case Number: - object.put(key, readableMap.getDouble(key)); - break; - case String: - object.put(key, readableMap.getString(key)); - break; - case Map: - object.put(key, convertMapToJson(readableMap.getMap(key))); - break; - case Array: - object.put(key, convertArrayToJson(readableMap.getArray(key))); - break; - } - } - return object; - } - - // Added by PBSC - private static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException { - JSONArray array = new JSONArray(); - for (int i = 0; i < readableArray.size(); i++) { - switch (readableArray.getType(i)) { - case Null: - break; - case Boolean: - array.put(readableArray.getBoolean(i)); - break; - case Number: - array.put(readableArray.getDouble(i)); - break; - case String: - array.put(readableArray.getString(i)); - break; - case Map: - array.put(convertMapToJson(readableArray.getMap(i))); - break; - case Array: - array.put(convertArrayToJson(readableArray.getArray(i))); - break; - } - } - return array; - } + JSONObject object = new JSONObject(); + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + switch (readableMap.getType(key)) { + case Null: + object.put(key, JSONObject.NULL); + break; + case Boolean: + object.put(key, readableMap.getBoolean(key)); + break; + case Number: + object.put(key, readableMap.getDouble(key)); + break; + case String: + object.put(key, readableMap.getString(key)); + break; + case Map: + object.put(key, convertMapToJson(readableMap.getMap(key))); + break; + case Array: + object.put(key, convertArrayToJson(readableMap.getArray(key))); + break; + } + } + return object; + } + + private static JSONArray convertArrayToJson(ReadableArray readableArray) throws JSONException { + JSONArray array = new JSONArray(); + for (int i = 0; i < readableArray.size(); i++) { + switch (readableArray.getType(i)) { + case Null: + break; + case Boolean: + array.put(readableArray.getBoolean(i)); + break; + case Number: + array.put(readableArray.getDouble(i)); + break; + case String: + array.put(readableArray.getString(i)); + break; + case Map: + array.put(convertMapToJson(readableArray.getMap(i))); + break; + case Array: + array.put(convertArrayToJson(readableArray.getArray(i))); + break; + } + } + return array; + } + } diff --git a/android/src/main/java/it/innove/BleManagerPackage.java b/android/src/main/java/it/innove/BleManagerPackage.java index 3e6f77c..f000f9c 100644 --- a/android/src/main/java/it/innove/BleManagerPackage.java +++ b/android/src/main/java/it/innove/BleManagerPackage.java @@ -13,22 +13,23 @@ public class BleManagerPackage implements ReactPackage { - public BleManagerPackage() {} + public BleManagerPackage() { + } - @Override - public List createNativeModules(ReactApplicationContext reactApplicationContext) { - List modules = new ArrayList<>(); + @Override + public List createNativeModules(ReactApplicationContext reactApplicationContext) { + List modules = new ArrayList<>(); - modules.add(new BleManager(reactApplicationContext)); - return modules; - } + modules.add(new BleManager(reactApplicationContext)); + return modules; + } - public List> createJSModules() { - return new ArrayList<>(); - } + public List> createJSModules() { + return new ArrayList<>(); + } - @Override - public List createViewManagers(ReactApplicationContext reactApplicationContext) { - return Collections.emptyList(); - } + @Override + public List createViewManagers(ReactApplicationContext reactApplicationContext) { + return Collections.emptyList(); + } } diff --git a/android/src/main/java/it/innove/CompanionScanner.java b/android/src/main/java/it/innove/CompanionScanner.java new file mode 100644 index 0000000..e77d1e5 --- /dev/null +++ b/android/src/main/java/it/innove/CompanionScanner.java @@ -0,0 +1,189 @@ +package it.innove; + +import static android.app.Activity.RESULT_OK; + +import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; + +import android.app.Activity; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.companion.AssociationRequest; +import android.companion.BluetoothLeDeviceFilter; +import android.companion.CompanionDeviceManager; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Build; +import android.os.ParcelUuid; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.BaseActivityEventListener; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class CompanionScanner { + + private final BleManager bleManager; + private final ReactContext reactContext; + public static final String LOG_TAG = BleManager.LOG_TAG + "_Companion"; + private static final int SELECT_DEVICE_REQUEST_CODE = 540; + + private static Callback scanCallback = null; + + private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() { + @Override + public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) { + Log.d(LOG_TAG, "onActivityResult"); + if (requestCode != SELECT_DEVICE_REQUEST_CODE) { + super.onActivityResult(activity, requestCode, resultCode, intent); + return; + } + + // The user either selected a device or cancelled the activity and we're + // either going to pass a peripheral or null back to the scanCallback. + Peripheral peripheral = null; + + if (resultCode == RESULT_OK) { + // Have device? + Log.d(LOG_TAG, "Ok activity result"); + + Object result = intent.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE); + if (result != null) { + if (result instanceof BluetoothDevice) { + peripheral = bleManager.savePeripheral((BluetoothDevice) result); + } else if (result instanceof ScanResult) { + peripheral = bleManager.savePeripheral(((ScanResult) result).getDevice()); + } else { + Log.wtf(LOG_TAG, "Unexpected AssociationInfo device!"); + } + + if (peripheral != null && scanCallback != null) { + scanCallback.invoke(null, peripheral.asWritableMap()); + scanCallback = null; + bleManager.emitOnCompanionPeripheral(peripheral.asWritableMap()); + } + } else { + scanCallback.invoke(null, null); + scanCallback = null; + bleManager.emitOnCompanionPeripheral(null); + } + } else { + // No device, user cancelled? + Log.d(LOG_TAG, "Non-ok activity result"); + } + + + if (scanCallback != null) { + scanCallback.invoke(null, peripheral != null ? peripheral.asWritableMap() : null); + scanCallback = null; + } + bleManager.emitOnCompanionPeripheral(peripheral != null ? peripheral.asWritableMap() : null); + } + }; + + public CompanionScanner(ReactApplicationContext reactContext, BleManager bleManager) { + this.reactContext = reactContext; + this.bleManager = bleManager; + reactContext.addActivityEventListener(mActivityEventListener); + } + + public void scan(ReadableArray serviceUUIDs, ReadableMap options, Callback callback) { + Log.d(LOG_TAG, "companion scan start"); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + callback.invoke("Companion not supported"); + return; + } + + AssociationRequest.Builder builder = new AssociationRequest.Builder() + .setSingleDevice(options.hasKey("single") && options.getBoolean("single")) ; + + int validUUIDCount = 0; + for (int i = 0; i < serviceUUIDs.size(); i++) { + String uuidString = serviceUUIDs.getString(i); + // Validate UUID format to prevent crash + if (!UUIDHelper.isValidBLEUUID(uuidString)) { + Log.w(LOG_TAG, "Warning: Invalid UUID format in scan options: " + uuidString + ", skipping"); + continue; + } + final ParcelUuid uuid = new ParcelUuid(UUIDHelper.uuidFromString(uuidString)); + Log.d(LOG_TAG, "Filter service: " + uuid); + validUUIDCount++; + + builder = builder + // Add LE filter. + .addDeviceFilter(new BluetoothLeDeviceFilter.Builder() + .setScanFilter(new ScanFilter.Builder().setServiceUuid(uuid).build()) + .build()); + } + + // If serviceUUIDs were provided but none were valid, return error + if (serviceUUIDs.size() > 0 && validUUIDCount == 0) { + callback.invoke("Invalid UUID format in serviceUUIDs: all UUIDs are invalid"); + return; + } + + AssociationRequest pairingRequest = builder.build(); + if (scanCallback != null) { + scanCallback.invoke("New scan called", null); + } + scanCallback = callback; + + CompanionDeviceManager companionDeviceManager = (CompanionDeviceManager) bleManager.getCompanionDeviceManager(); + companionDeviceManager.associate(pairingRequest, new CompanionDeviceManager.Callback() { + @Override + public void onFailure(@Nullable CharSequence charSequence) { + Log.d(LOG_TAG, "companion failure: " + charSequence); + String msg = charSequence != null + ? "Companion association failed: " + charSequence.toString() + : "Companion association failed" ; + + // onFailure might be called after user cancels the assocation + // activity / dialog, and we've already called the callback + // with a null value. + if (scanCallback != null) { + scanCallback.invoke(msg); + scanCallback = null; + } + + WritableMap map = Arguments.createMap(); + map.putString("error", charSequence.toString()); + bleManager.emitOnCompanionFailure(map); + } + + @Override + public void onDeviceFound(@NonNull IntentSender intentSender) { + Log.d(LOG_TAG, "companion device found"); + try { + reactContext.getCurrentActivity().startIntentSenderForResult( + intentSender, SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0 + ); + } catch (IntentSender.SendIntentException e) { + Log.e(LOG_TAG, "Failed to send intent: " + e.toString()); + String msg = "Failed to send intent: " + e.toString(); + + if (scanCallback != null) { + scanCallback.invoke(msg); + scanCallback = null; + } + + WritableMap map = Arguments.createMap(); + map.putString("error", msg); + bleManager.emitOnCompanionFailure(map); + } + } + }, null); + + } +} diff --git a/android/src/main/java/it/innove/DefaultPeripheral.java b/android/src/main/java/it/innove/DefaultPeripheral.java new file mode 100644 index 0000000..8d1492c --- /dev/null +++ b/android/src/main/java/it/innove/DefaultPeripheral.java @@ -0,0 +1,121 @@ +package it.innove; + +import static it.innove.BleManager.LOG_TAG; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.os.Build; +import android.os.ParcelUuid; +import android.annotation.SuppressLint; +import android.util.Log; +import android.util.SparseArray; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Objects; + +@SuppressLint("MissingPermission") +public class DefaultPeripheral extends Peripheral { + + private ScanRecord advertisingData; + private ScanResult scanResult; + + public DefaultPeripheral(BleManager bleManager, ScanResult result) { + super(result.getDevice(), result.getRssi(), Objects.requireNonNull(result.getScanRecord()).getBytes(), bleManager); + this.advertisingData = result.getScanRecord(); + this.scanResult = result; + } + + public DefaultPeripheral(BluetoothDevice device, BleManager bleManager) { + super(device, bleManager); + } + + @Override + public WritableMap asWritableMap() { + WritableMap map = super.asWritableMap(); + WritableMap advertising = Arguments.createMap(); + + try { + String name = getSafeDeviceName(); + if (name == null && scanResult != null && scanResult.getScanRecord() != null) { + name = scanResult.getScanRecord().getDeviceName(); + } + map.putString("name", name); + map.putString("id", device.getAddress()); // mac address + map.putInt("rssi", advertisingRSSI); + + advertising.putMap("rawData", byteArrayToWritableMap(advertisingDataBytes)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // We can check if peripheral is connectable using the scanresult + if (this.scanResult != null) { + advertising.putBoolean("isConnectable", scanResult.isConnectable()); + } + } else { + // We can't check if peripheral is connectable + advertising.putBoolean("isConnectable", true); + } + + if (advertisingData != null) { + String deviceName = advertisingData.getDeviceName(); + if (deviceName != null) + advertising.putString("localName", deviceName.replace("\0", "")); + + WritableArray serviceUuids = Arguments.createArray(); + if (advertisingData.getServiceUuids() != null && !advertisingData.getServiceUuids().isEmpty()) { + for (ParcelUuid uuid : advertisingData.getServiceUuids()) { + serviceUuids.pushString(UUIDHelper.uuidToString(uuid.getUuid())); + } + } + advertising.putArray("serviceUUIDs", serviceUuids); + + WritableMap serviceData = Arguments.createMap(); + if (advertisingData.getServiceData() != null) { + for (Map.Entry entry : advertisingData.getServiceData().entrySet()) { + if (entry.getValue() != null) { + serviceData.putMap(UUIDHelper.uuidToString((entry.getKey()).getUuid()), byteArrayToWritableMap(entry.getValue())); + } + } + } + advertising.putMap("serviceData", serviceData); + + WritableMap manufacturerData = Arguments.createMap(); + SparseArray manufacturerRawData = advertisingData.getManufacturerSpecificData(); + byte[] manufacturerRawBytes = new byte[0]; + if (manufacturerRawData != null && manufacturerRawData.size() > 0) { + int key = manufacturerRawData.keyAt(0); + byte[] data = manufacturerRawData.valueAt(0); + manufacturerData.putMap(String.format("%04x", key), byteArrayToWritableMap(data)); + + ByteBuffer keyBuffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); + keyBuffer.putInt(key); + byte[] keyBytes = keyBuffer.array(); + manufacturerRawBytes = new byte[keyBytes.length + data.length]; + System.arraycopy(keyBytes, 0, manufacturerRawBytes, 0, keyBytes.length); + System.arraycopy(data, 0, manufacturerRawBytes, keyBytes.length, data.length); + } + advertising.putMap("manufacturerData", manufacturerData); + advertising.putMap("manufacturerRawData", byteArrayToWritableMap(manufacturerRawBytes)); + + advertising.putInt("txPowerLevel", advertisingData.getTxPowerLevel()); + } + + map.putMap("advertising", advertising); + } catch (Exception e) { // this shouldn't happen + Log.e(LOG_TAG, "asWritableMap error", e); + } + + return map; + } + + public void updateData(ScanResult result) { + scanResult = result; + advertisingData = result.getScanRecord(); + advertisingDataBytes = advertisingData != null ? advertisingData.getBytes() : null; + } +} diff --git a/android/src/main/java/it/innove/DefaultScanManager.java b/android/src/main/java/it/innove/DefaultScanManager.java new file mode 100644 index 0000000..9adcc9f --- /dev/null +++ b/android/src/main/java/it/innove/DefaultScanManager.java @@ -0,0 +1,431 @@ +package it.innove; + + +import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanRecord; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.ParcelUuid; +import android.util.Log; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +@SuppressLint("MissingPermission") +public class DefaultScanManager extends ScanManager { + + private boolean isScanning = false; + private PendingIntent scanPendingIntent; + private BroadcastReceiver scanReceiver; + private boolean scanReceiverRegistered = false; + private boolean scanningWithIntent = false; + private static final String ACTION_SCAN_RESULT = "it.innove.BleManager.ACTION_SCAN_RESULT"; + private static final String EXTRA_LIST_SCAN_RESULT = "android.bluetooth.le.extra.LIST_SCAN_RESULT"; + private static final String EXTRA_SCAN_RESULT = "android.bluetooth.le.extra.SCAN_RESULT"; + private static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE"; + + public DefaultScanManager(ReactApplicationContext reactContext, BleManager bleManager) { + super(reactContext, bleManager); + } + + + @Override + public void stopScan(Callback callback) { + // update scanSessionId to prevent stopping next scan by running timeout thread + scanSessionId.incrementAndGet(); + + stopActiveScan(); + callback.invoke(); + } + + @Override + public void scan(ReadableMap options, Callback callback) { + ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); + List filters = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.hasKey("legacy")) { + scanSettingsBuilder.setLegacy(options.getBoolean("legacy")); + } + + if (options.hasKey("scanMode")) { + scanSettingsBuilder.setScanMode(options.getInt("scanMode")); + } + + if (options.hasKey("numberOfMatches")) { + scanSettingsBuilder.setNumOfMatches(options.getInt("numberOfMatches")); + } + if (options.hasKey("matchMode")) { + scanSettingsBuilder.setMatchMode(options.getInt("matchMode")); + } + if (options.hasKey("callbackType")) { + scanSettingsBuilder.setCallbackType(options.getInt("callbackType")); + } + + if (options.hasKey("reportDelay")) { + scanSettingsBuilder.setReportDelay(options.getInt("reportDelay")); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.hasKey("phy")) { + int phy = options.getInt("phy"); + if (phy == BluetoothDevice.PHY_LE_CODED && getBluetoothAdapter().isLeCodedPhySupported()) { + scanSettingsBuilder.setPhy(BluetoothDevice.PHY_LE_CODED); + } + if (phy == BluetoothDevice.PHY_LE_2M && getBluetoothAdapter().isLe2MPhySupported()) { + scanSettingsBuilder.setPhy(BluetoothDevice.PHY_LE_2M); + } + } + + ReadableArray serviceUUIDs = options.getArray("serviceUUIDs"); + if (serviceUUIDs != null && serviceUUIDs.size() > 0) { + int validUUIDCount = 0; + for (int i = 0; i < serviceUUIDs.size(); i++) { + String uuidString = Objects.requireNonNull(serviceUUIDs.getString(i)); + // Validate UUID format to prevent crash + if (!UUIDHelper.isValidBLEUUID(uuidString)) { + Log.w(BleManager.LOG_TAG, "Warning: Invalid UUID format in scan options: " + uuidString + ", skipping"); + continue; + } + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(UUIDHelper.uuidFromString(uuidString))).build(); + filters.add(filter); + validUUIDCount++; + Log.d(BleManager.LOG_TAG, "Filter service: " + uuidString); + } + // If serviceUUIDs were provided but none were valid, return error + if (validUUIDCount == 0) { + callback.invoke("Invalid UUID format in serviceUUIDs: all UUIDs are invalid"); + return; + } + } + + + if (options.hasKey("exactAdvertisingName")) { + ReadableArray exactAdvertisingNameArray = options.getArray("exactAdvertisingName"); + if (exactAdvertisingNameArray == null) { + Log.w(BleManager.LOG_TAG, "exactAdvertisingName key present but array is null"); + } + if (exactAdvertisingNameArray != null) { + ArrayList expectedNames = exactAdvertisingNameArray.toArrayList(); + Log.d(BleManager.LOG_TAG, "Filter on advertising names:" + expectedNames); + for (Object name : expectedNames) { + ScanFilter filter = new ScanFilter.Builder().setDeviceName(name.toString()).build(); + filters.add(filter); + } + } + } + + if (options.hasKey("manufacturerData")) { + ReadableMap manufacturerDataMap = options.getMap("manufacturerData"); + if (manufacturerDataMap != null && manufacturerDataMap.hasKey("manufacturerId")) { + int manufacturerId = manufacturerDataMap.getInt("manufacturerId"); + ReadableArray manufacturerData = manufacturerDataMap.getArray("manufacturerData"); + ReadableArray manufacturerDataMask = manufacturerDataMap.getArray("manufacturerDataMask"); + byte[] manufacturerDataBytes = new byte[0]; + byte[] manufacturerDataMaskBytes = new byte[0]; + if (manufacturerData != null) { + manufacturerDataBytes = new byte[manufacturerData.size()]; + for (int i = 0; i < manufacturerData.size(); i++) { + manufacturerDataBytes[i] = Integer.valueOf(manufacturerData.getInt(i)).byteValue(); + } + } + if (manufacturerDataMask != null) { + manufacturerDataMaskBytes = new byte[manufacturerDataMask.size()]; + for (int i = 0; i < manufacturerDataMask.size(); i++) { + manufacturerDataMaskBytes[i] = Integer.valueOf(manufacturerDataMask.getInt(i)).byteValue(); + } + } + if (manufacturerDataBytes.length != manufacturerDataMaskBytes.length) { + callback.invoke("manufacturerData and manufacturerDataMask must have the same length"); + return; + } + Log.d( + BleManager.LOG_TAG, + String.format( + "Filter on manufacturerId: %d; manufacturerData: %s; manufacturerDataMask: %s", + manufacturerId, + Arrays.toString(manufacturerDataBytes), + Arrays.toString(manufacturerDataMaskBytes) + ) + ); + ScanFilter filter = new ScanFilter.Builder() + .setManufacturerData( + manufacturerId, + manufacturerDataBytes, + manufacturerDataMaskBytes + ).build(); + filters.add(filter); + } + } + + boolean useScanIntent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + && options.hasKey("useScanIntent") + && options.getBoolean("useScanIntent"); + + if (useScanIntent && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + callback.invoke("useScanIntent requires Android O (API 26) or higher"); + return; + } + + if (isScanning) { + scanSessionId.incrementAndGet(); + stopActiveScan(); + } + + BluetoothLeScanner scanner = getBluetoothAdapter().getBluetoothLeScanner(); + if (scanner == null) { + callback.invoke("No BLE scanner available"); + return; + } + + try { + if (useScanIntent) { + Log.i(BleManager.LOG_TAG, "Scan with intent"); + ensureScanReceiver(); + Intent intent = new Intent(ACTION_SCAN_RESULT); + intent.setPackage(context.getPackageName()); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_MUTABLE; + } + scanPendingIntent = PendingIntent.getBroadcast(context, 0, intent, flags); + scanner.startScan(filters, scanSettingsBuilder.build(), scanPendingIntent); + scanningWithIntent = true; + } else { + scanner.startScan(filters, scanSettingsBuilder.build(), mScanCallback); + scanningWithIntent = false; + } + } catch (Exception e) { + if (useScanIntent) { + if (scanPendingIntent != null) { + scanPendingIntent.cancel(); + scanPendingIntent = null; + } + unregisterScanReceiver(); + } + callback.invoke("Failed to start scan: " + e.getMessage()); + return; + } + + isScanning = true; + + long scanSeconds = (long) options.getDouble("seconds"); + if (scanSeconds > 0) { + Thread thread = new Thread() { + private final int currentScanSession = scanSessionId.incrementAndGet(); + + @Override + public void run() { + + try { + Thread.sleep(scanSeconds * 1000L); + } catch (InterruptedException ignored) { + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + BluetoothAdapter btAdapter = getBluetoothAdapter(); + + // check current scan session was not stopped + if (scanSessionId.intValue() == currentScanSession) { + if (btAdapter.getState() == BluetoothAdapter.STATE_ON) { + stopActiveScan(); + } + + WritableMap map = Arguments.createMap(); + map.putInt("status", 10); + bleManager.emitOnStopScan(map); + } + } + }); + + } + + }; + thread.start(); + } + callback.invoke(); + } + + private void onDiscoveredPeripheral(final ScanResult result) { + String info; + ScanRecord record = result.getScanRecord(); + + if (record != null) { + info = record.getDeviceName(); + } else if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) { + info = result.getDevice().getName(); + } else { + info = result.toString(); + } + + Log.i(BleManager.LOG_TAG, "DiscoverPeripheral: " + info); + + DefaultPeripheral peripheral = (DefaultPeripheral) bleManager.getPeripheral(result.getDevice()); + if (peripheral == null) { + peripheral = new DefaultPeripheral(bleManager, result); + } else { + peripheral.updateData(result); + peripheral.updateRssi(result.getRssi()); + } + bleManager.savePeripheral(peripheral); + + WritableMap map = peripheral.asWritableMap(); + bleManager.emitOnDiscoverPeripheral(map); + } + + private final ScanCallback mScanCallback = new ScanCallback() { + @Override + public void onScanResult(final int callbackType, final ScanResult result) { + runOnUiThread(new Runnable() { + @Override + public void run() { + onDiscoveredPeripheral(result); + } + }); + } + + @Override + public void onBatchScanResults(final List results) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (results.isEmpty()) { + return; + } + + for (ScanResult result : results) { + onDiscoveredPeripheral(result); + } + } + }); + } + + @Override + public void onScanFailed(final int errorCode) { + isScanning = false; + WritableMap map = Arguments.createMap(); + map.putInt("status", errorCode); + bleManager.emitOnStopScan(map); + } + }; + + @Override + public boolean isScanning() { + return isScanning; + } + + @Override + public void setScanning(boolean scanning) { + isScanning = scanning; + } + + private void stopActiveScan() { + BluetoothLeScanner scanner = getBluetoothAdapter() != null ? getBluetoothAdapter().getBluetoothLeScanner() : null; + if (scanner != null) { + try { + if (scanningWithIntent && scanPendingIntent != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + scanner.stopScan(scanPendingIntent); + } + } else { + scanner.stopScan(mScanCallback); + } + } catch (IllegalArgumentException | IllegalStateException ignored) { + Log.w(BleManager.LOG_TAG, "stopScan ignored error: " + ignored.getMessage()); + } + } + if (scanPendingIntent != null) { + scanPendingIntent.cancel(); + scanPendingIntent = null; + } + unregisterScanReceiver(); + scanningWithIntent = false; + isScanning = false; + } + + private void ensureScanReceiver() { + if (scanReceiver == null) { + scanReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!ACTION_SCAN_RESULT.equals(intent.getAction())) { + return; + } + + if (intent.hasExtra(EXTRA_ERROR_CODE)) { + final int errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, ScanCallback.SCAN_FAILED_INTERNAL_ERROR); + runOnUiThread(new Runnable() { + @Override + public void run() { + stopActiveScan(); + WritableMap map = Arguments.createMap(); + map.putInt("status", errorCode); + bleManager.emitOnStopScan(map); + } + }); + return; + } + + final ArrayList results = intent.getParcelableArrayListExtra(EXTRA_LIST_SCAN_RESULT); + final ScanResult singleResult = intent.getParcelableExtra(EXTRA_SCAN_RESULT); + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (results != null) { + for (ScanResult result : results) { + onDiscoveredPeripheral(result); + } + } else if (singleResult != null) { + onDiscoveredPeripheral(singleResult); + } + } + }); + } + }; + } + + if (!scanReceiverRegistered) { + IntentFilter intentFilter = new IntentFilter(ACTION_SCAN_RESULT); + ContextCompat.registerReceiver(context, scanReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED); + scanReceiverRegistered = true; + } + } + + private void unregisterScanReceiver() { + if (scanReceiverRegistered) { + try { + context.unregisterReceiver(scanReceiver); + } catch (IllegalArgumentException ignored) { + } + scanReceiverRegistered = false; + } + } +} diff --git a/android/src/main/java/it/innove/Helper.java b/android/src/main/java/it/innove/Helper.java index 13bac92..5ecf975 100644 --- a/android/src/main/java/it/innove/Helper.java +++ b/android/src/main/java/it/innove/Helper.java @@ -4,7 +4,6 @@ import android.bluetooth.BluetoothGattDescriptor; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; -import org.json.JSONArray; public class Helper { diff --git a/android/src/main/java/it/innove/LegacyScanManager.java b/android/src/main/java/it/innove/LegacyScanManager.java deleted file mode 100644 index b80d7ca..0000000 --- a/android/src/main/java/it/innove/LegacyScanManager.java +++ /dev/null @@ -1,97 +0,0 @@ -package it.innove; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.util.Log; - -import com.facebook.react.bridge.*; - -import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; - -public class LegacyScanManager extends ScanManager { - - public LegacyScanManager(ReactApplicationContext reactContext, BleManager bleManager) { - super(reactContext, bleManager); - } - - @Override - public void stopScan(Callback callback) { - // update scanSessionId to prevent stopping next scan by running timeout thread - scanSessionId.incrementAndGet(); - - getBluetoothAdapter().stopLeScan(mLeScanCallback); - callback.invoke(); - } - - private BluetoothAdapter.LeScanCallback mLeScanCallback = - new BluetoothAdapter.LeScanCallback() { - - @Override - public void onLeScan(final BluetoothDevice device, final int rssi, - final byte[] scanRecord) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Log.i(bleManager.LOG_TAG, "DiscoverPeripheral: " + device.getName()); - - Peripheral peripheral = bleManager.getPeripheral(device); - if (peripheral == null) { - peripheral = new Peripheral(device, rssi, scanRecord, bleManager.getReactContext()); - } else { - peripheral.updateData(scanRecord); - peripheral.updateRssi(rssi); - } - bleManager.savePeripheral(peripheral); - - WritableMap map = peripheral.asWritableMap(); - bleManager.sendEvent("BleManagerDiscoverPeripheral", map); - } - }); - } - - - }; - - @Override - public void scan(ReadableArray serviceUUIDs, final int scanSeconds, ReadableMap options, Callback callback) { - if (serviceUUIDs.size() > 0) { - Log.d(bleManager.LOG_TAG, "Filter is not working in pre-lollipop devices"); - } - getBluetoothAdapter().startLeScan(mLeScanCallback); - - if (scanSeconds > 0) { - Thread thread = new Thread() { - private int currentScanSession = scanSessionId.incrementAndGet(); - - @Override - public void run() { - - try { - Thread.sleep(scanSeconds * 1000); - } catch (InterruptedException ignored) { - } - - runOnUiThread(new Runnable() { - @Override - public void run() { - BluetoothAdapter btAdapter = getBluetoothAdapter(); - // check current scan session was not stopped - if (scanSessionId.intValue() == currentScanSession) { - if (btAdapter.getState() == BluetoothAdapter.STATE_ON) { - btAdapter.stopLeScan(mLeScanCallback); - } - WritableMap map = Arguments.createMap(); - map.putInt("status", 0); - bleManager.sendEvent("BleManagerStopScan", map); - } - } - }); - - } - - }; - thread.start(); - } - callback.invoke(); - } -} diff --git a/android/src/main/java/it/innove/LollipopPeripheral.java b/android/src/main/java/it/innove/LollipopPeripheral.java deleted file mode 100644 index 4113087..0000000 --- a/android/src/main/java/it/innove/LollipopPeripheral.java +++ /dev/null @@ -1,93 +0,0 @@ -package it.innove; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.le.ScanRecord; -import android.bluetooth.le.ScanResult; -import android.os.Build; -import android.os.ParcelUuid; -import androidx.annotation.RequiresApi; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableMap; - -import java.util.Map; - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -public class LollipopPeripheral extends Peripheral { - - private ScanRecord advertisingData; - private ScanResult scanResult; - - public LollipopPeripheral(ReactContext reactContext, ScanResult result) { - super(result.getDevice(), result.getRssi(), result.getScanRecord().getBytes(), reactContext); - this.advertisingData = result.getScanRecord(); - this.scanResult = result; - } - - public LollipopPeripheral(BluetoothDevice device, ReactApplicationContext reactContext) { - super(device, reactContext); - } - - @Override - public WritableMap asWritableMap() { - WritableMap map = super.asWritableMap(); - WritableMap advertising = Arguments.createMap(); - - try { - advertising.putMap("manufacturerData", byteArrayToWritableMap(advertisingDataBytes)); - - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ - // We can check if peripheral is connectable using the scanresult - if (this.scanResult != null) { - advertising.putBoolean("isConnectable", scanResult.isConnectable()); - } - } else{ - // We can't check if peripheral is connectable - advertising.putBoolean("isConnectable", true); - } - - - if (advertisingData != null) { - String deviceName = advertisingData.getDeviceName(); - if (deviceName != null) - advertising.putString("localName", deviceName.replace("\0", "")); - - WritableArray serviceUuids = Arguments.createArray(); - if (advertisingData.getServiceUuids() != null && advertisingData.getServiceUuids().size() != 0) { - for (ParcelUuid uuid : advertisingData.getServiceUuids()) { - serviceUuids.pushString(UUIDHelper.uuidToString(uuid.getUuid())); - } - } - advertising.putArray("serviceUUIDs", serviceUuids); - - WritableMap serviceData = Arguments.createMap(); - if (advertisingData.getServiceData() != null) { - for (Map.Entry entry : advertisingData.getServiceData().entrySet()) { - if (entry.getValue() != null) { - serviceData.putMap(UUIDHelper.uuidToString((entry.getKey()).getUuid()), byteArrayToWritableMap(entry.getValue())); - } - } - } - - advertising.putMap("serviceData", serviceData); - advertising.putInt("txPowerLevel", advertisingData.getTxPowerLevel()); - } - - map.putMap("advertising", advertising); - } catch (Exception e) { // this shouldn't happen - e.printStackTrace(); - } - - return map; - } - - public void updateData(ScanResult result) { - advertisingData = result.getScanRecord(); - advertisingDataBytes = advertisingData.getBytes(); - } - - -} diff --git a/android/src/main/java/it/innove/LollipopScanManager.java b/android/src/main/java/it/innove/LollipopScanManager.java deleted file mode 100644 index 2a95e02..0000000 --- a/android/src/main/java/it/innove/LollipopScanManager.java +++ /dev/null @@ -1,176 +0,0 @@ -package it.innove; - - -import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread; - -import android.Manifest; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanRecord; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.ParcelUuid; -import android.util.Log; - -import androidx.annotation.RequiresApi; -import androidx.core.app.ActivityCompat; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.Callback; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableMap; - -import java.util.ArrayList; -import java.util.List; - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -public class LollipopScanManager extends ScanManager { - - public LollipopScanManager(ReactApplicationContext reactContext, BleManager bleManager) { - super(reactContext, bleManager); - } - - @Override - public void stopScan(Callback callback) { - // update scanSessionId to prevent stopping next scan by running timeout thread - scanSessionId.incrementAndGet(); - - getBluetoothAdapter().getBluetoothLeScanner().stopScan(mScanCallback); - callback.invoke(); - } - - @Override - public void scan(ReadableArray serviceUUIDs, final int scanSeconds, ReadableMap options, Callback callback) { - ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder(); - List filters = new ArrayList<>(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.hasKey("legacy")) { - scanSettingsBuilder.setLegacy(options.getBoolean("legacy")); - } - - if (options.hasKey("scanMode")) { - scanSettingsBuilder.setScanMode(options.getInt("scanMode")); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (options.hasKey("numberOfMatches")) { - scanSettingsBuilder.setNumOfMatches(options.getInt("numberOfMatches")); - } - if (options.hasKey("matchMode")) { - scanSettingsBuilder.setMatchMode(options.getInt("matchMode")); - } - if (options.hasKey("callbackType")) { - scanSettingsBuilder.setCallbackType(options.getInt("callbackType")); - } - } - - if (options.hasKey("reportDelay")) { - scanSettingsBuilder.setReportDelay(options.getInt("reportDelay")); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.hasKey("phy")) { - int phy = options.getInt("phy"); - if (phy == BluetoothDevice.PHY_LE_CODED && getBluetoothAdapter().isLeCodedPhySupported()) { - scanSettingsBuilder.setPhy(BluetoothDevice.PHY_LE_CODED); - } - if (phy == BluetoothDevice.PHY_LE_2M && getBluetoothAdapter().isLe2MPhySupported()) { - scanSettingsBuilder.setPhy(BluetoothDevice.PHY_LE_2M); - } - } - - if (serviceUUIDs.size() > 0) { - for (int i = 0; i < serviceUUIDs.size(); i++) { - ScanFilter filter = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(UUIDHelper.uuidFromString(serviceUUIDs.getString(i)))).build(); - filters.add(filter); - Log.d(bleManager.LOG_TAG, "Filter service: " + serviceUUIDs.getString(i)); - } - } - - getBluetoothAdapter().getBluetoothLeScanner().startScan(filters, scanSettingsBuilder.build(), mScanCallback); - if (scanSeconds > 0) { - Thread thread = new Thread() { - private int currentScanSession = scanSessionId.incrementAndGet(); - - @Override - public void run() { - - try { - Thread.sleep(scanSeconds * 1000); - } catch (InterruptedException ignored) { - } - - runOnUiThread(new Runnable() { - @Override - public void run() { - BluetoothAdapter btAdapter = getBluetoothAdapter(); - // check current scan session was not stopped - if (scanSessionId.intValue() == currentScanSession) { - if (btAdapter.getState() == BluetoothAdapter.STATE_ON) { - btAdapter.getBluetoothLeScanner().stopScan(mScanCallback); - } - WritableMap map = Arguments.createMap(); - map.putInt("status", 10); - bleManager.sendEvent("BleManagerStopScan", map); - } - } - }); - - } - - }; - thread.start(); - } - callback.invoke(); - } - - private ScanCallback mScanCallback = new ScanCallback() { - @Override - public void onScanResult(final int callbackType, final ScanResult result) { - - runOnUiThread(new Runnable() { - @Override - public void run() { - String info; - ScanRecord record = result.getScanRecord(); - if (record != null) - info = record.getDeviceName(); - else if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) - info = result.getDevice().getName(); - else - info = result.toString(); - - Log.i(bleManager.LOG_TAG, "DiscoverPeripheral: " + info); - - LollipopPeripheral peripheral = (LollipopPeripheral) bleManager.getPeripheral(result.getDevice()); - if (peripheral == null) { - peripheral = new LollipopPeripheral(bleManager.getReactContext(), result); - } else { - peripheral.updateData(result); - peripheral.updateRssi(result.getRssi()); - } - bleManager.savePeripheral(peripheral); - - WritableMap map = peripheral.asWritableMap(); - bleManager.sendEvent("BleManagerDiscoverPeripheral", map); - } - }); - } - - @Override - public void onBatchScanResults(final List results) { - } - - @Override - public void onScanFailed(final int errorCode) { - WritableMap map = Arguments.createMap(); - map.putInt("status", errorCode); - bleManager.sendEvent("BleManagerStopScan", map); - } - }; -} diff --git a/android/src/main/java/it/innove/NotifyBufferContainer.java b/android/src/main/java/it/innove/NotifyBufferContainer.java index cc01950..f04bd1c 100644 --- a/android/src/main/java/it/innove/NotifyBufferContainer.java +++ b/android/src/main/java/it/innove/NotifyBufferContainer.java @@ -3,30 +3,37 @@ import java.nio.ByteBuffer; public class NotifyBufferContainer { - public final Integer maxBufferSize; - private Integer bufferCount; public ByteBuffer items; - public NotifyBufferContainer(Integer size) { - this.maxBufferSize = size; - this.resetBuffer(); + public NotifyBufferContainer(int size) { + this.items = ByteBuffer.allocate(size); } public void resetBuffer(){ - this.bufferCount = 0; - this.items = ByteBuffer.allocate(this.maxBufferSize); + this.items.clear(); } - public void put(byte[] value){ - this.bufferCount += value.length; - if (this.items.remaining() < value.length) { - return; + public byte[] put(byte[] value){ + byte[] toInsert = null; + byte[] rest = null; + + if (value.length > this.items.remaining()) { + int restLength = value.length - this.items.remaining(); + rest = new byte[restLength]; + toInsert = new byte[this.items.remaining()]; + System.arraycopy(value, 0, toInsert, 0, toInsert.length); + System.arraycopy(value, toInsert.length, rest, 0, rest.length); + } else { + toInsert = value; } - this.items.put(value); + + this.items.put(toInsert); + + return rest; } public boolean isBufferFull(){ - return this.bufferCount >= this.maxBufferSize; + return this.items.remaining() == 0; } - public Integer size() { - return this.bufferCount; + public int size() { + return this.items.limit(); } @Override protected void finalize() throws Throwable { diff --git a/android/src/main/java/it/innove/PbscLog.java b/android/src/main/java/it/innove/PbscLog.java new file mode 100644 index 0000000..6dff5f1 --- /dev/null +++ b/android/src/main/java/it/innove/PbscLog.java @@ -0,0 +1,20 @@ +package it.innove; + +import android.util.Log; + +final class PbscLog { + private PbscLog() { + } + + static void d(String message) { + if (BuildConfig.DEBUG) { + Log.d(BleManager.LOG_TAG, message); + } + } + + static void d(String message, Throwable throwable) { + if (BuildConfig.DEBUG) { + Log.d(BleManager.LOG_TAG, message, throwable); + } + } +} diff --git a/android/src/main/java/it/innove/Peripheral.java b/android/src/main/java/it/innove/Peripheral.java index 9bc7e5e..01cea99 100644 --- a/android/src/main/java/it/innove/Peripheral.java +++ b/android/src/main/java/it/innove/Peripheral.java @@ -1,8 +1,10 @@ package it.innove; +import static com.facebook.react.common.ReactConstants.TAG; + +import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; -import android.app.PendingIntent; -import android.app.Service; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; @@ -10,1091 +12,1402 @@ import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; -import android.bluetooth.le.ScanRecord; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; -import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.ParcelUuid; -import androidx.annotation.Nullable; -import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.RCTNativeAppEventEmitter; -import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; -import org.json.JSONArray; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.*; +import java.util.HashMap; import java.util.ArrayList; -import java.util.Map; import java.util.Arrays; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; -import java.util.UUID; +import java.util.Map; import java.util.Queue; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; -import static android.os.Build.VERSION_CODES.LOLLIPOP; -import static com.facebook.react.common.ReactConstants.TAG; - /** * Peripheral wraps the BluetoothDevice and provides methods to convert to JSON. */ +@SuppressLint("MissingPermission") public class Peripheral extends BluetoothGattCallback { - private static final String CHARACTERISTIC_NOTIFICATION_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; - public static final int GATT_INSUFFICIENT_AUTHENTICATION = 5; - public static final int GATT_AUTH_FAIL = 137; - - private final BluetoothDevice device; - private final Map bufferedCharacteristics; - private ScanRecord advertisingData; - protected byte[] advertisingDataBytes = new byte[0]; - protected int advertisingRSSI; - private boolean connected = false; - private boolean connecting = false; - private ReactContext reactContext; - - private BluetoothGatt gatt; - - private Callback connectCallback; - private Callback retrieveServicesCallback; - private Callback readCallback; - private Callback readRSSICallback; - private Callback writeCallback; - private Callback registerNotifyCallback; - private Callback requestMTUCallback; - private PeripheralService peripheralService; - private Context serviceContext; - - private final Queue commandQueue = new ConcurrentLinkedQueue<>(); - private final Handler mainHandler = new Handler(Looper.getMainLooper()); - private Runnable discoverServicesRunnable; - private boolean commandQueueBusy = false; - - private List writeQueue = new ArrayList<>(); - - public Peripheral(BluetoothDevice device, PeripheralService peripheralService, Context serviceContext) { - this.device = device; - this.bufferedCharacteristics = new HashMap(); - this.peripheralService = peripheralService; - this.serviceContext = serviceContext; - } - - public Peripheral(BluetoothDevice device, int advertisingRSSI, byte[] scanRecord, ReactContext reactContext) { - this.device = device; - this.bufferedCharacteristics = new ConcurrentHashMap(); - this.advertisingRSSI = advertisingRSSI; - this.advertisingDataBytes = scanRecord; - this.reactContext = reactContext; - } - - public Peripheral(BluetoothDevice device, ReactContext reactContext) { - this.device = device; - this.bufferedCharacteristics = new ConcurrentHashMap(); - this.reactContext = reactContext; - } - - private void sendEvent(String eventName, @Nullable WritableMap params) { - //reactContext.getJSModule(RCTNativeAppEventEmitter.class).emit(eventName, params); - //َ Added by PBSC - Log.d(BleManager.LOG_TAG, eventName + " { - if (!connected) { - BluetoothDevice device = getDevice(); - this.connectCallback = callback; - this.connecting = true; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Log.d(BleManager.LOG_TAG, " Is Or Greater than M $mBluetoothDevice"); - gatt = device.connectGatt(activity, false, this, BluetoothDevice.TRANSPORT_LE); - } else { - Log.d(BleManager.LOG_TAG, " Less than M"); - try { - Log.d(BleManager.LOG_TAG, " Trying TRANPORT LE with reflection"); - Method m = device.getClass().getDeclaredMethod("connectGatt", Context.class, Boolean.class, - BluetoothGattCallback.class, Integer.class); - m.setAccessible(true); - Integer transport = device.getClass().getDeclaredField("TRANSPORT_LE").getInt(null); - gatt = (BluetoothGatt) m.invoke(device, activity, false, this, transport); - } catch (Exception e) { - e.printStackTrace(); - Log.d(TAG, " Catch to call normal connection"); - gatt = device.connectGatt(activity, false, this); - } - } - } else { - if (gatt != null) { - Log.d(BleManager.LOG_TAG, "invoking callback"); - callback.invoke(); - } else { - callback.invoke("BluetoothGatt is null"); - - } - } - }); - } - - public void disconnect(final Callback callback, final boolean force) { - mainHandler.post(() -> { - connectCallback = null; - connected = false; - clearBuffers(); - commandQueue.clear(); - commandQueueBusy = false; - - if (gatt != null) { - try { - gatt.disconnect(); - if (force) { - gatt.close(); - gatt = null; - sendConnectionEvent(device, "BleManagerDisconnectPeripheral", BluetoothGatt.GATT_SUCCESS); - } - Log.d(BleManager.LOG_TAG, "Disconnect"); - } catch (Exception e) { - sendConnectionEvent(device, "BleManagerDisconnectPeripheral", BluetoothGatt.GATT_FAILURE); - Log.d(BleManager.LOG_TAG, "Error on disconnect", e); - } - } else { - Log.d(BleManager.LOG_TAG, "GATT is null"); - if(peripheralService != null) { - peripheralService.stopService(); - } - if (callback != null) - callback.invoke(); - } - }); - } - - public WritableMap asWritableMap() { - WritableMap map = Arguments.createMap(); - WritableMap advertising = Arguments.createMap(); - - try { - map.putString("name", device.getName()); - map.putString("id", device.getAddress()); // mac address - map.putInt("rssi", advertisingRSSI); - - String name = device.getName(); - if (name != null) - advertising.putString("localName", name); - - advertising.putMap("manufacturerData", byteArrayToWritableMap(advertisingDataBytes)); - - // No scanResult to access so we can't check if peripheral is connectable - advertising.putBoolean("isConnectable", true); - - map.putMap("advertising", advertising); - } catch (Exception e) { // this shouldn't happen - e.printStackTrace(); - } - - return map; - } - - public WritableMap asWritableMap(BluetoothGatt gatt) { - - WritableMap map = asWritableMap(); - - WritableArray servicesArray = Arguments.createArray(); - WritableArray characteristicsArray = Arguments.createArray(); - - if (connected && gatt != null) { - for (Iterator it = gatt.getServices().iterator(); it.hasNext();) { - BluetoothGattService service = it.next(); - WritableMap serviceMap = Arguments.createMap(); - serviceMap.putString("uuid", UUIDHelper.uuidToString(service.getUuid())); - - for (Iterator itCharacteristic = service.getCharacteristics() - .iterator(); itCharacteristic.hasNext();) { - BluetoothGattCharacteristic characteristic = itCharacteristic.next(); - WritableMap characteristicsMap = Arguments.createMap(); - - characteristicsMap.putString("service", UUIDHelper.uuidToString(service.getUuid())); - characteristicsMap.putString("characteristic", UUIDHelper.uuidToString(characteristic.getUuid())); - - characteristicsMap.putMap("properties", Helper.decodeProperties(characteristic)); - - if (characteristic.getPermissions() > 0) { - characteristicsMap.putMap("permissions", Helper.decodePermissions(characteristic)); - } - - WritableArray descriptorsArray = Arguments.createArray(); - - for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { - WritableMap descriptorMap = Arguments.createMap(); - descriptorMap.putString("uuid", UUIDHelper.uuidToString(descriptor.getUuid())); - if (descriptor.getValue() != null) { - descriptorMap.putString("value", - Base64.encodeToString(descriptor.getValue(), Base64.NO_WRAP)); - } else { - descriptorMap.putString("value", null); - } - - if (descriptor.getPermissions() > 0) { - descriptorMap.putMap("permissions", Helper.decodePermissions(descriptor)); - } - descriptorsArray.pushMap(descriptorMap); - } - if (descriptorsArray.size() > 0) { - characteristicsMap.putArray("descriptors", descriptorsArray); - } - characteristicsArray.pushMap(characteristicsMap); - } - servicesArray.pushMap(serviceMap); - } - map.putArray("services", servicesArray); - map.putArray("characteristics", characteristicsArray); - } - - return map; - } - - static WritableMap byteArrayToWritableMap(byte[] bytes) throws JSONException { - WritableMap object = Arguments.createMap(); - object.putString("CDVType", "ArrayBuffer"); - object.putString("data", bytes != null ? Base64.encodeToString(bytes, Base64.NO_WRAP) : null); - object.putArray("bytes", bytes != null ? BleManager.bytesToWritableArray(bytes) : null); - return object; - } - - public boolean isConnected() { - return connected; - } - - public boolean isConnecting() { - return connecting; - } - - public BluetoothDevice getDevice() { - return device; - } - - public Boolean hasService(UUID uuid) { - if (gatt == null) { - return null; - } - return gatt.getService(uuid) != null; - } - - // Added by PBSC - private void runServicesDiscovery() { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - try { - gatt.discoverServices(); - } - catch (NullPointerException e) { - Log.d(BleManager.LOG_TAG, "runServicesDiscovery connected but gatt of Run method was null"); - } - } - }); - } - - @Override - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - super.onServicesDiscovered(gatt, status); - mainHandler.post(() -> { - if (retrieveServicesCallback != null) { - WritableMap map = this.asWritableMap(gatt); - retrieveServicesCallback.invoke(null, map); - retrieveServicesCallback = null; - } - completedCommand(); - }); - } - - @Override - public void onConnectionStateChange(BluetoothGatt gatta, int status, final int newState) { - - Log.d(BleManager.LOG_TAG, "onConnectionStateChange to " + newState + " on peripheral: " + device.getAddress() - + " with status " + status); - - mainHandler.post(() -> { - gatt = gatta; - - if (status != BluetoothGatt.GATT_SUCCESS) { - gatt.close(); - } - - connecting = false; - if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { - connected = true; - - discoverServicesRunnable = new Runnable() { - @Override - public void run() { - try { - gatt.discoverServices(); - } catch (NullPointerException e) { - Log.d(BleManager.LOG_TAG, "onConnectionStateChange connected but gatt of Run method was null"); - } - discoverServicesRunnable = null; - } - }; - - mainHandler.post(discoverServicesRunnable); - - sendConnectionEvent(device, "BleManagerConnectPeripheral", status); - - if (connectCallback != null) { - Log.d(BleManager.LOG_TAG, "Connected to: " + device.getAddress()); - connectCallback.invoke(); - connectCallback = null; - } - - } else if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) { - - if (discoverServicesRunnable != null) { - mainHandler.removeCallbacks(discoverServicesRunnable); - discoverServicesRunnable = null; - } - - List callbacks = Arrays.asList(writeCallback, retrieveServicesCallback, readRSSICallback, - readCallback, registerNotifyCallback, requestMTUCallback); - for (Callback currentCallback : callbacks) { - if (currentCallback != null) { - try { - currentCallback.invoke("Device disconnected"); - } catch (Exception e) { - e.printStackTrace(); - } } - } - if (connectCallback != null) { - connectCallback.invoke("Connection error"); - connectCallback = null; - } - writeCallback = null; - writeQueue.clear(); - readCallback = null; - retrieveServicesCallback = null; - readRSSICallback = null; - registerNotifyCallback = null; - requestMTUCallback = null; - commandQueue.clear(); - commandQueueBusy = false; - connectCallback = null; - connected = false; - clearBuffers(); - commandQueue.clear(); - commandQueueBusy = false; - - gatt.disconnect(); - gatt.close(); - gatt = null; - sendConnectionEvent(device, "BleManagerDisconnectPeripheral", BluetoothGatt.GATT_SUCCESS); - - } - - }); - - } - - public void updateRssi(int rssi) { - advertisingRSSI = rssi; - } - - public void updateData(byte[] data) { - advertisingDataBytes = data; - } - - public void updateData(ScanRecord scanRecord) { - advertisingData = scanRecord; - } - - public int unsignedToBytes(byte b) { - return b & 0xFF; - } - - @Override - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - super.onCharacteristicChanged(gatt, characteristic); - try { - String charString = characteristic.getUuid().toString(); - String service = characteristic.getService().getUuid().toString(); - NotifyBufferContainer buffer = this.bufferedCharacteristics - .get(this.bufferedCharacteristicsKey(service, charString)); - byte[] dataValue = characteristic.getValue(); - if (buffer != null) { - buffer.put(dataValue); - Log.d(BleManager.LOG_TAG, "onCharacteristicChanged-buffering: " + - buffer.size() + " from peripheral: " + device.getAddress()); - - if (buffer.isBufferFull()) { - Log.d(BleManager.LOG_TAG, "onCharacteristicChanged sending buffered data " + buffer.size()); - - // send'm and reset - dataValue = buffer.items.array(); - buffer.resetBuffer(); - } else { - return; - } - } - Log.d(BleManager.LOG_TAG, "onCharacteristicChanged: " + BleManager.bytesToHex(dataValue) - + " from peripheral: " + device.getAddress()); - WritableMap map = Arguments.createMap(); - map.putString("peripheral", device.getAddress()); - map.putString("characteristic", charString); - map.putString("service", service); - map.putArray("value", BleManager.bytesToWritableArray(dataValue)); - sendEvent("BleManagerDidUpdateValueForCharacteristic", map); - - } catch (Exception e) { - Log.d(BleManager.LOG_TAG, "onCharacteristicChanged ERROR: " + e.toString()); - } - } - - @Override - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - super.onCharacteristicRead(gatt, characteristic, status); - - mainHandler.post(() -> { - if (status != BluetoothGatt.GATT_SUCCESS) { - if (status == GATT_AUTH_FAIL || status == GATT_INSUFFICIENT_AUTHENTICATION) { - Log.d(BleManager.LOG_TAG, "Read needs bonding"); - } - readCallback.invoke("Error reading " + characteristic.getUuid() + " status=" + status, null); - readCallback = null; - } else if (readCallback != null) { - final byte[] dataValue = copyOf(characteristic.getValue()); - readCallback.invoke(null, BleManager.bytesToWritableArray(dataValue)); - readCallback = null; - } - completedCommand(); - }); - - } - - @Override - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - super.onCharacteristicWrite(gatt, characteristic, status); - - mainHandler.post(() -> { - if (writeQueue.size() > 0) { - byte[] data = writeQueue.get(0); - writeQueue.remove(0); - doWrite(characteristic, data, writeCallback); - } else if (status != BluetoothGatt.GATT_SUCCESS) { - if (status == GATT_AUTH_FAIL || status == GATT_INSUFFICIENT_AUTHENTICATION) { - Log.d(BleManager.LOG_TAG, "Write needs bonding"); - // *not* doing completedCommand() - return; - } - writeCallback.invoke( "Error writing " + characteristic.getUuid() + " status=" + status, null); - writeCallback = null; - } else if (writeCallback != null) { - writeCallback.invoke(); - writeCallback = null; - } - completedCommand(); - }); - } - - @Override - public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - mainHandler.post(() -> { - if (registerNotifyCallback != null) { - if (status == BluetoothGatt.GATT_SUCCESS) { - registerNotifyCallback.invoke(); - Log.d(BleManager.LOG_TAG, "onDescriptorWrite success"); - } else { - registerNotifyCallback.invoke("Error writing descriptor status=" + status, null); - Log.e(BleManager.LOG_TAG, "Error writing descriptor status=" + status); - } - - registerNotifyCallback = null; - } else { - Log.e(BleManager.LOG_TAG, "onDescriptorWrite with no callback"); - } - - completedCommand(); - }); - } - - @Override - public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { - super.onReadRemoteRssi(gatt, rssi, status); - - mainHandler.post(() -> { - if (readRSSICallback != null) { - if (status == BluetoothGatt.GATT_SUCCESS) { - updateRssi(rssi); - readRSSICallback.invoke(null, rssi); - } else { - readRSSICallback.invoke("Error reading RSSI status=" + status, null); - } - - readRSSICallback = null; - } - - completedCommand(); - }); - } - - private String bufferedCharacteristicsKey(String serviceUUID, String characteristicUUID) { - return serviceUUID + "-" + characteristicUUID; - } - - private void clearBuffers() { - for (Map.Entry entry : this.bufferedCharacteristics.entrySet()) - entry.getValue().resetBuffer(); - } - - private void setNotify(UUID serviceUUID, UUID characteristicUUID, final Boolean notify, Callback callback) { - if (! isConnected() || gatt == null) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } - - BluetoothGattService service = gatt.getService(serviceUUID); - final BluetoothGattCharacteristic characteristic = findNotifyCharacteristic(service, characteristicUUID); - - if (characteristic == null) { - callback.invoke("Characteristic " + characteristicUUID + " not found"); - completedCommand(); - return; - } - - if (! gatt.setCharacteristicNotification(characteristic, notify)) { - callback.invoke("Failed to register notification for " + characteristicUUID); - completedCommand(); - return; - } - - final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUIDHelper.uuidFromString(CHARACTERISTIC_NOTIFICATION_CONFIG)); - if (descriptor == null) { - callback.invoke("Set notification failed for " + characteristicUUID); - completedCommand(); - return; - } - - // Prefer notify over indicate - byte[] value; - if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { - Log.d(BleManager.LOG_TAG, "Characteristic " + characteristicUUID + " set NOTIFY"); - value = notify ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; - } else if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) { - Log.d(BleManager.LOG_TAG, "Characteristic " + characteristicUUID + " set INDICATE"); - value = notify ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; - } else { - String msg = "Characteristic " + characteristicUUID + " does not have NOTIFY or INDICATE property set"; - Log.d(BleManager.LOG_TAG, msg); - callback.invoke(msg); - completedCommand(); - return; - } - final byte[] finalValue = notify ? value : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; - final Callback finalCallback = callback; - - boolean result = false; - try { - result = gatt.setCharacteristicNotification(characteristic, notify); - // Then write to descriptor - descriptor.setValue(finalValue); - registerNotifyCallback = finalCallback; - result &= gatt.writeDescriptor(descriptor); - } catch(Exception e) { - Log.d(BleManager.LOG_TAG, "Exception in setNotify", e); - } - - if (! result) { - callback.invoke( "writeDescriptor failed for descriptor: " + descriptor.getUuid(), null); - registerNotifyCallback = null; - completedCommand(); - } - } - - public void registerNotify(UUID serviceUUID, UUID characteristicUUID, Integer buffer, Callback callback) { - if (!enqueue(() -> { - Log.d(BleManager.LOG_TAG, "registerNotify"); - if (buffer > 1) { - Log.d(BleManager.LOG_TAG, "registerNotify using buffer"); - String bufferKey = this.bufferedCharacteristicsKey(serviceUUID.toString(), characteristicUUID.toString()); - this.bufferedCharacteristics.put(bufferKey, new NotifyBufferContainer(buffer)); - } - this.setNotify(serviceUUID, characteristicUUID, true, callback); - })) { - Log.e(BleManager.LOG_TAG, "Could not enqueue setNotify command to register notify"); - } - } - - public void removeNotify(UUID serviceUUID, UUID characteristicUUID, Callback callback) { - if (!enqueue(() -> { - Log.d(BleManager.LOG_TAG, "removeNotify"); - String bufferKey = this.bufferedCharacteristicsKey(serviceUUID.toString(), characteristicUUID.toString()); - if (this.bufferedCharacteristics.containsKey(bufferKey)) { - NotifyBufferContainer buffer = this.bufferedCharacteristics.get(bufferKey); - this.bufferedCharacteristics.remove(bufferKey); - } - this.setNotify(serviceUUID, characteristicUUID, false, callback); - })) { - Log.e(BleManager.LOG_TAG, "Could not enqueue setNotify command to remove notify"); - } - } - - // Some devices reuse UUIDs across characteristics, so we can't use - // service.getCharacteristic(characteristicUUID) - // instead check the UUID and properties for each characteristic in the service - // until we find the best match - // This function prefers Notify over Indicate - private BluetoothGattCharacteristic findNotifyCharacteristic(BluetoothGattService service, - UUID characteristicUUID) { - - try { - // Check for Notify first - List characteristics = service.getCharacteristics(); - for (BluetoothGattCharacteristic characteristic : characteristics) { - if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 - && characteristicUUID.equals(characteristic.getUuid())) { - return characteristic; - } - } - - // If there wasn't Notify Characteristic, check for Indicate - for (BluetoothGattCharacteristic characteristic : characteristics) { - if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0 - && characteristicUUID.equals(characteristic.getUuid())) { - return characteristic; - } - } - - // As a last resort, try and find ANY characteristic with this UUID, even if it - // doesn't have the correct properties - return service.getCharacteristic(characteristicUUID); - } catch (Exception e) { - Log.e(BleManager.LOG_TAG, "Error retriving characteristic " + characteristicUUID, e); - return null; - } - } - - public void read(UUID serviceUUID, UUID characteristicUUID, final Callback callback) { - enqueue(() -> { - if (!isConnected() || gatt == null) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } - - BluetoothGattService service = gatt.getService(serviceUUID); - final BluetoothGattCharacteristic characteristic = findReadableCharacteristic(service, characteristicUUID); - - if (characteristic == null) { - callback.invoke("Characteristic " + characteristicUUID + " not found.", null); - completedCommand(); - return; - } - - readCallback = callback; - if (!gatt.readCharacteristic(characteristic)) { - callback.invoke("Read failed", null); - readCallback = null; - completedCommand(); - } - - }); - } - - private byte[] copyOf(byte[] source) { - if (source == null) return new byte[0]; - final int sourceLength = source.length; - final byte[] copy = new byte[sourceLength]; - System.arraycopy(source, 0, copy, 0, sourceLength); - return copy; - } - - private boolean enqueue(Runnable command) { - final boolean result = commandQueue.add(command); - if (result) { - nextCommand(); - } else { - Log.d(BleManager.LOG_TAG, "could not enqueue command"); - } - return result; - } - - private void completedCommand() { - commandQueue.poll(); - commandQueueBusy = false; - nextCommand(); - } - - private void nextCommand() { - synchronized (this) { - if (commandQueueBusy) { - Log.d(BleManager.LOG_TAG, "Command queue busy"); - return; - } - - final Runnable nextCommand = commandQueue.peek(); - if (nextCommand == null) { - Log.d(BleManager.LOG_TAG, "Command queue empty"); - return; - } - - // Check if we still have a valid gatt object - if (gatt == null) { - Log.d(BleManager.LOG_TAG, "Error, gatt is null"); - commandQueue.clear(); - commandQueueBusy = false; - return; - } - - // Execute the next command in the queue - commandQueueBusy = true; - mainHandler.post(new Runnable() { - @Override - public void run() { - try { - nextCommand.run(); - } catch (Exception ex) { - Log.d(BleManager.LOG_TAG, "Error, command exception"); - completedCommand(); - } - } - }); - } - } - - public void readRSSI(final Callback callback) { - if (!enqueue(() -> { - if (!isConnected()) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } else if (gatt == null) { - callback.invoke("BluetoothGatt is null", null); - completedCommand(); - return; - } else { - readRSSICallback = callback; - if (!gatt.readRemoteRssi()) { - callback.invoke("Read RSSI failed", null); - readRSSICallback = null; - completedCommand(); - } - } - })) { - Log.d(BleManager.LOG_TAG, "Could not queue readRemoteRssi command"); - } - } - - public void refreshCache(Callback callback) { - enqueue(() -> { - try { - Method localMethod = gatt.getClass().getMethod("refresh", new Class[0]); - if (localMethod != null) { - boolean res = ((Boolean) localMethod.invoke(gatt, new Object[0])).booleanValue(); - callback.invoke(null, res); - } else { - callback.invoke("Could not refresh cache for device."); - } - } catch (Exception localException) { - Log.e(TAG, "An exception occured while refreshing device"); - callback.invoke(localException.getMessage()); - } finally { - completedCommand(); - } - }); - } - - public void retrieveServices(Callback callback) { - enqueue(() -> { - if (!isConnected()) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } else if (gatt == null) { - callback.invoke("BluetoothGatt is null", null); - completedCommand(); - return; - } else { - this.retrieveServicesCallback = callback; - //gatt.discoverServices(); - // Added by PBSC - runServicesDiscovery(); - } - }); - } - - // Some peripherals re-use UUIDs for multiple characteristics so we need to - // check the properties - // and UUID of all characteristics instead of using - // service.getCharacteristic(characteristicUUID) - private BluetoothGattCharacteristic findReadableCharacteristic(BluetoothGattService service, - UUID characteristicUUID) { - - if (service != null) { - int read = BluetoothGattCharacteristic.PROPERTY_READ; - - List characteristics = service.getCharacteristics(); - for (BluetoothGattCharacteristic characteristic : characteristics) { - if ((characteristic.getProperties() & read) != 0 - && characteristicUUID.equals(characteristic.getUuid())) { - return characteristic; - } - } - - // As a last resort, try and find ANY characteristic with this UUID, even if it - // doesn't have the correct properties - return service.getCharacteristic(characteristicUUID); - } - - return null; - } - - public boolean doWrite(final BluetoothGattCharacteristic characteristic, byte[] data, final Callback callback) { - final byte[] copyOfData = copyOf(data); - return enqueue(new Runnable() { - @Override - public void run() { - characteristic.setValue(copyOfData); - if (characteristic.getWriteType() == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) - writeCallback = callback; - else - writeCallback = null; - if (!gatt.writeCharacteristic(characteristic)) { - // write without response, caller will handle the callback - if (writeCallback != null) { - writeCallback.invoke("Write failed", writeCallback); - writeCallback = null; - } - completedCommand(); - } - } - }); - } - - public void write(UUID serviceUUID, UUID characteristicUUID, byte[] data, Integer maxByteSize, Integer queueSleepTime, Callback callback, int writeType) { - enqueue(() -> { - if (!isConnected() || gatt == null) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } - - BluetoothGattService service = gatt.getService(serviceUUID); - BluetoothGattCharacteristic characteristic = findWritableCharacteristic(service, characteristicUUID, writeType); - - if (characteristic == null) { - callback.invoke("Characteristic " + characteristicUUID + " not found."); - completedCommand(); - return; - } - - characteristic.setWriteType(writeType); - - if (data.length <= maxByteSize) { - if (! doWrite(characteristic, data, callback)) { - callback.invoke("Write failed"); - } else { - if (BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE == writeType) { - callback.invoke(); - } - } - } else { - int dataLength = data.length; - int count = 0; - byte[] firstMessage = null; - List splittedMessage = new ArrayList<>(); - - while (count < dataLength && (dataLength - count > maxByteSize)) { - if (count == 0) { - firstMessage = Arrays.copyOfRange(data, count, count + maxByteSize); - } else { - byte[] splitMessage = Arrays.copyOfRange(data, count, count + maxByteSize); - splittedMessage.add(splitMessage); - } - count += maxByteSize; - } - if (count < dataLength) { - // Other bytes in queue - byte[] splitMessage = Arrays.copyOfRange(data, count, data.length); - splittedMessage.add(splitMessage); - } - - if (BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT == writeType) { - writeQueue.addAll(splittedMessage); - if (! doWrite(characteristic, firstMessage, callback)) { - writeQueue.clear(); - callback.invoke("Write failed"); - } - } else { - try { - boolean writeError = false; - if (! doWrite(characteristic, firstMessage, callback)) { - writeError = true; - callback.invoke("Write failed"); - } - if (! writeError) { - Thread.sleep(queueSleepTime); - for (byte[] message : splittedMessage) { - if (! doWrite(characteristic, message, callback)) { - writeError = true; - callback.invoke("Write failed"); - break; - } - Thread.sleep(queueSleepTime); - } - if (! writeError) { - callback.invoke(); - } - } - } catch (InterruptedException e) { - callback.invoke("Error during writing"); - } - } - } - - completedCommand(); - }); - } - - public void requestConnectionPriority(int connectionPriority, Callback callback) { - enqueue(() -> { - if (gatt != null) { - if (Build.VERSION.SDK_INT >= LOLLIPOP) { - boolean status = gatt.requestConnectionPriority(connectionPriority); - callback.invoke(null, status); - } else { - callback.invoke("Requesting connection priority requires at least API level 21", null); - } - } else { - callback.invoke("BluetoothGatt is null", null); - } - - completedCommand(); - }); - } - - public void requestMTU(int mtu, Callback callback) { - enqueue(() -> { - if (!isConnected()) { - callback.invoke("Device is not connected", null); - completedCommand(); - return; - } - - if (gatt == null) { - callback.invoke("BluetoothGatt is null", null); - completedCommand(); - return; - } - - if (Build.VERSION.SDK_INT >= LOLLIPOP) { - requestMTUCallback = callback; - if (!gatt.requestMtu(mtu)) { - requestMTUCallback.invoke("Request MTU failed", null); - requestMTUCallback = null; - completedCommand(); - } - } else { - callback.invoke("Requesting MTU requires at least API level 21", null); - completedCommand(); - } - }); - } - - @Override - public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { - super.onMtuChanged(gatt, mtu, status); - mainHandler.post(() -> { - if (requestMTUCallback != null) { - if (status == BluetoothGatt.GATT_SUCCESS) { - requestMTUCallback.invoke(null, mtu); - } else { - requestMTUCallback.invoke("Error requesting MTU status = " + status, null); - } - - requestMTUCallback = null; - } - - completedCommand(); - }); - } - - // Some peripherals re-use UUIDs for multiple characteristics so we need to - // check the properties - // and UUID of all characteristics instead of using - // service.getCharacteristic(characteristicUUID) - private BluetoothGattCharacteristic findWritableCharacteristic(BluetoothGattService service, - UUID characteristicUUID, int writeType) { - try { - // get write property - int writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE; - if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) { - writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; - } - - List characteristics = service.getCharacteristics(); - for (BluetoothGattCharacteristic characteristic : characteristics) { - if ((characteristic.getProperties() & writeProperty) != 0 - && characteristicUUID.equals(characteristic.getUuid())) { - return characteristic; - } - } - - // As a last resort, try and find ANY characteristic with this UUID, even if it - // doesn't have the correct properties - return service.getCharacteristic(characteristicUUID); - } catch (Exception e) { - Log.e(BleManager.LOG_TAG, "Error on findWritableCharacteristic", e); - return null; - } - } - - private String generateHashKey(BluetoothGattCharacteristic characteristic) { - return generateHashKey(characteristic.getService().getUuid(), characteristic); - } - - private String generateHashKey(UUID serviceUUID, BluetoothGattCharacteristic characteristic) { - return String.valueOf(serviceUUID) + "|" + characteristic.getUuid() + "|" + characteristic.getInstanceId(); - } + private static final String CHARACTERISTIC_NOTIFICATION_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; + public static final int GATT_INSUFFICIENT_AUTHENTICATION = 5; + public static final int GATT_AUTH_FAIL = 137; + + protected final BluetoothDevice device; + private final Map bufferedCharacteristics; + protected volatile byte[] advertisingDataBytes = new byte[0]; + protected volatile int advertisingRSSI; + private volatile boolean connected = false; + private volatile boolean connecting = false; + private BleManager bleManager; + private PeripheralService peripheralService; + private Context serviceContext; + + private BluetoothGatt gatt; + + private LinkedList connectCallbacks = new LinkedList<>(); + private LinkedList retrieveServicesCallbacks = new LinkedList<>(); + private LinkedList readCallbacks = new LinkedList<>(); + private LinkedList readDescriptorCallbacks = new LinkedList<>(); + private LinkedList writeDescriptorCallbacks = new LinkedList<>(); + private LinkedList readRSSICallbacks = new LinkedList<>(); + private LinkedList writeCallbacks = new LinkedList<>(); + private LinkedList registerNotifyCallbacks = new LinkedList<>(); + private LinkedList requestMTUCallbacks = new LinkedList<>(); + + private final Queue commandQueue = new ConcurrentLinkedQueue<>(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private boolean commandQueueBusy = false; + + private final Queue writeQueue = new LinkedList<>(); + + public Peripheral(BluetoothDevice device, int advertisingRSSI, byte[] scanRecord, BleManager bleManager) { + this.device = device; + this.bufferedCharacteristics = new ConcurrentHashMap(); + this.advertisingRSSI = advertisingRSSI; + this.advertisingDataBytes = scanRecord; + this.bleManager = bleManager; + } + + public Peripheral(BluetoothDevice device, BleManager bleManager) { + this.device = device; + this.bufferedCharacteristics = new ConcurrentHashMap(); + this.bleManager = bleManager; + } + + public Peripheral(BluetoothDevice device, PeripheralService peripheralService, Context serviceContext) { + this.device = device; + this.bufferedCharacteristics = new HashMap<>(); + this.peripheralService = peripheralService; + this.serviceContext = serviceContext; + } + + private void forwardEventToService(String eventName, WritableMap params) { + PbscLog.d(eventName + " { + if (connected) { + if (gatt != null) { + callback.invoke(); + } else { + callback.invoke("BluetoothGatt is null"); + } + return; + } + + this.connectCallbacks.addLast(callback); + + if (connecting) { + return; + } + + BluetoothDevice device = getDevice(); + this.connecting = true; + Context connectContext = activity != null ? activity + : (serviceContext != null ? serviceContext : bleManager.getReactContext()); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PbscLog.d( " Is Or Greater than M $mBluetoothDevice"); + boolean autoconnect = false; + if (options.hasKey("autoconnect")) { + autoconnect = options.getBoolean("autoconnect"); + } + if (!autoconnect && options.hasKey("phy") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int phy = options.getInt("phy"); + gatt = device.connectGatt(connectContext, false, this, BluetoothDevice.TRANSPORT_LE, phy); + } else { + gatt = device.connectGatt(connectContext, autoconnect, this, BluetoothDevice.TRANSPORT_LE); + } + } else { + PbscLog.d( " Less than M"); + try { + PbscLog.d( " Trying TRANPORT LE with reflection"); + Method m = device.getClass().getDeclaredMethod("connectGatt", Context.class, Boolean.class, + BluetoothGattCallback.class, Integer.class); + m.setAccessible(true); + Integer transport = device.getClass().getDeclaredField("TRANSPORT_LE").getInt(null); + gatt = (BluetoothGatt) m.invoke(device, connectContext, false, this, transport); + } catch (Exception e) { + e.printStackTrace(); + PbscLog.d( " Catch to call normal connection"); + gatt = device.connectGatt(connectContext, false, this); + } + } + + // connectGatt() may return null if the adapter is not ready + if (gatt == null) { + throw new IllegalStateException("connectGatt returned null"); + } + } catch (Exception e) { + Log.e(BleManager.LOG_TAG, "[ON CONNECT][NATIVE] connectGatt failed for " + device.getAddress(), e); + this.connecting = false; + this.gatt = null; + for (Callback connectCallback : connectCallbacks) { + connectCallback.invoke("Connection failed to start: " + e.getMessage()); + } + connectCallbacks.clear(); + } + }); + } + // bt_btif : Register with GATT stack failed. + + public void disconnect(final Callback callback, final boolean force) { + connected = false; + connecting = false; + mainHandler.post(() -> { + errorAndClearAllCallbacks("Disconnect called before the command completed"); + resetQueuesAndBuffers(); + + if (gatt != null) { + try { + gatt.disconnect(); + if (force) { + gatt.close(); + gatt = null; + sendDisconnectionEvent(device, BluetoothGatt.GATT_SUCCESS); + } + PbscLog.d( "Disconnect"); + } catch (Exception e) { + sendDisconnectionEvent(device, BluetoothGatt.GATT_FAILURE); + PbscLog.d( "Error on disconnect", e); + } + } else { + PbscLog.d( "GATT is null"); + if (peripheralService != null) { + peripheralService.stopService(); + } + } + if (callback != null) + callback.invoke(); + }); + } + + public WritableMap asWritableMap() { + WritableMap map = Arguments.createMap(); + WritableMap advertising = Arguments.createMap(); + + try { + String name = getSafeDeviceName(); + map.putString("name", name); + map.putString("id", device.getAddress()); // mac address + map.putInt("rssi", advertisingRSSI); + + if (name != null) + advertising.putString("localName", name); + + advertising.putMap("rawData", byteArrayToWritableMap(advertisingDataBytes)); + + // No scanResult to access so we can't check if peripheral is connectable + advertising.putBoolean("isConnectable", true); + + map.putMap("advertising", advertising); + } catch (Exception e) { // this shouldn't happen + Log.e(BleManager.LOG_TAG, "Unexpected error on asWritableMap", e); + } + + return map; + } + + @Nullable + protected String getSafeDeviceName() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Context context = bleManager != null ? bleManager.getReactContext() : null; + if (context == null) { + return null; + } + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) + != PackageManager.PERMISSION_GRANTED) { + return null; + } + } + + return device.getName(); + } + + public WritableMap asWritableMap(BluetoothGatt gatt) { + + WritableMap map = asWritableMap(); + + WritableArray servicesArray = Arguments.createArray(); + WritableArray characteristicsArray = Arguments.createArray(); + + if (connected && gatt != null) { + for (Iterator it = gatt.getServices().iterator(); it.hasNext(); ) { + BluetoothGattService service = it.next(); + WritableMap serviceMap = Arguments.createMap(); + serviceMap.putString("uuid", UUIDHelper.uuidToString(service.getUuid())); + + for (Iterator itCharacteristic = service.getCharacteristics() + .iterator(); itCharacteristic.hasNext(); ) { + BluetoothGattCharacteristic characteristic = itCharacteristic.next(); + WritableMap characteristicsMap = Arguments.createMap(); + + characteristicsMap.putString("service", UUIDHelper.uuidToString(service.getUuid())); + characteristicsMap.putString("characteristic", UUIDHelper.uuidToString(characteristic.getUuid())); + + characteristicsMap.putMap("properties", Helper.decodeProperties(characteristic)); + + if (characteristic.getPermissions() > 0) { + characteristicsMap.putMap("permissions", Helper.decodePermissions(characteristic)); + } + + WritableArray descriptorsArray = Arguments.createArray(); + + for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()) { + WritableMap descriptorMap = Arguments.createMap(); + descriptorMap.putString("uuid", UUIDHelper.uuidToString(descriptor.getUuid())); + if (descriptor.getValue() != null) { + descriptorMap.putString("value", + Base64.encodeToString(descriptor.getValue(), Base64.NO_WRAP)); + } else { + descriptorMap.putString("value", null); + } + + if (descriptor.getPermissions() > 0) { + descriptorMap.putMap("permissions", Helper.decodePermissions(descriptor)); + } + descriptorsArray.pushMap(descriptorMap); + } + if (descriptorsArray.size() > 0) { + characteristicsMap.putArray("descriptors", descriptorsArray); + } + characteristicsArray.pushMap(characteristicsMap); + } + servicesArray.pushMap(serviceMap); + } + map.putArray("services", servicesArray); + map.putArray("characteristics", characteristicsArray); + } + + return map; + } + + static WritableMap byteArrayToWritableMap(byte[] bytes) throws JSONException { + WritableMap object = Arguments.createMap(); + object.putString("CDVType", "ArrayBuffer"); + object.putString("data", bytes != null ? Base64.encodeToString(bytes, Base64.NO_WRAP) : null); + object.putArray("bytes", bytes != null ? BleManager.bytesToWritableArray(bytes) : null); + return object; + } + + public boolean isConnected() { + return connected; + } + + public boolean isConnecting() { + return connecting; + } + + public BluetoothDevice getDevice() { + return device; + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + super.onServicesDiscovered(gatt, status); + mainHandler.post(() -> { + if (gatt == null) { + for (Callback retrieveServicesCallback : retrieveServicesCallbacks) { + retrieveServicesCallback.invoke("Error during service retrieval: gatt is null"); + } + } else if (status == BluetoothGatt.GATT_SUCCESS) { + for (Callback retrieveServicesCallback : retrieveServicesCallbacks) { + WritableMap map = this.asWritableMap(gatt); + retrieveServicesCallback.invoke(null, map); + } + } else { + for (Callback retrieveServicesCallback : retrieveServicesCallbacks) { + retrieveServicesCallback.invoke("Error during service retrieval."); + } + } + retrieveServicesCallbacks.clear(); + completedCommand(); + }); + } + + public void errorAndClearAllCallbacks(final String errorMessage) { + + for (Callback writeCallback : writeCallbacks) { + writeCallback.invoke(errorMessage); + } + writeCallbacks.clear(); + + for (Callback retrieveServicesCallback : retrieveServicesCallbacks) { + retrieveServicesCallback.invoke(errorMessage); + } + retrieveServicesCallbacks.clear(); + + for (Callback readRSSICallback : readRSSICallbacks) { + readRSSICallback.invoke(errorMessage); + } + readRSSICallbacks.clear(); + + for (Callback registerNotifyCallback : registerNotifyCallbacks) { + registerNotifyCallback.invoke(errorMessage); + } + registerNotifyCallbacks.clear(); + + for (Callback requestMTUCallback : requestMTUCallbacks) { + requestMTUCallback.invoke(errorMessage); + } + requestMTUCallbacks.clear(); + + for (Callback readCallback : readCallbacks) { + readCallback.invoke(errorMessage); + } + readCallbacks.clear(); + + for (Callback readDescriptorCallback : readDescriptorCallbacks) { + readDescriptorCallback.invoke(errorMessage); + } + readDescriptorCallbacks.clear(); + + for (Callback callback : writeDescriptorCallbacks) { + callback.invoke(errorMessage); + } + writeDescriptorCallbacks.clear(); + + for (Callback connectCallback : connectCallbacks) { + connectCallback.invoke(errorMessage); + } + connectCallbacks.clear(); + } + + public void resetQueuesAndBuffers() { + writeQueue.clear(); + commandQueue.clear(); + commandQueueBusy = false; + connected = false; + connecting = false; + clearBuffers(); + } + + @Override + public void onConnectionStateChange(BluetoothGatt gatta, int status, final int newState) { + + PbscLog.d( "onConnectionStateChange to " + newState + " on peripheral: " + device.getAddress() + + " with status " + status); + + // We immediately update the internal connection status + if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { + connected = true; + } else if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) { + connected = false; + } + + mainHandler.post(() -> { + gatt = gatta; + + if (gatt != null && status != BluetoothGatt.GATT_SUCCESS) { + gatt.close(); + } + + connecting = false; + if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { + sendConnectionEvent(device, status); + + PbscLog.d( "Connected to: " + device.getAddress()); + for (Callback connectCallback : connectCallbacks) { + connectCallback.invoke(); + } + connectCallbacks.clear(); + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED || status != BluetoothGatt.GATT_SUCCESS) { + + errorAndClearAllCallbacks("Device disconnected"); + resetQueuesAndBuffers(); + if (gatt != null) { + gatt.disconnect(); + gatt.close(); + } + + gatt = null; + sendDisconnectionEvent(device, BluetoothGatt.GATT_SUCCESS); + } + + }); + + } + + public void updateRssi(int rssi) { + advertisingRSSI = rssi; + } + + public void updateData(byte[] data) { + advertisingDataBytes = data; + } + + /// /// + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicChanged(gatt, characteristic); + onCharacteristicChanged(gatt, characteristic, characteristic.getValue()); + } + } + + @Override + public void onCharacteristicChanged(@NonNull final BluetoothGatt gatt, @NonNull final BluetoothGattCharacteristic characteristic, @NonNull final byte[] data) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicChanged(gatt, characteristic, data); + } + try { + String charString = characteristic.getUuid().toString(); + String service = characteristic.getService().getUuid().toString(); + NotifyBufferContainer buffer = this.bufferedCharacteristics + .get(this.bufferedCharacteristicsKey(service, charString)); + byte[] dataValue = data; + // If for some reason the value's length >= 2*buffer size this will be able to + // handle it + while (dataValue != null) { + byte[] rest = null; + if (buffer != null) { + rest = buffer.put(dataValue); + if (buffer.isBufferFull()) { + + // fetch and reset + dataValue = buffer.items.array(); + buffer.resetBuffer(); + } else { + return; + } + } + + WritableMap map = Arguments.createMap(); + map.putString("peripheral", device.getAddress()); + map.putString("characteristic", charString); + map.putString("service", service); + map.putArray("value", BleManager.bytesToWritableArray(dataValue)); + if (peripheralService != null) { + forwardEventToService("BleManagerDidUpdateValueForCharacteristic", map); + } else { + bleManager.emitOnDidUpdateValueForCharacteristic(map); + } + + // Check if rest exists. If so it needs to be added to the clean buffer + dataValue = rest; + } + + } catch (Exception e) { + PbscLog.d( "onCharacteristicChanged ERROR: " + e); + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicRead(gatt, characteristic, status); + onCharacteristicRead(gatt, characteristic, characteristic.getValue(), status); + } + } + + @Override + public void onCharacteristicRead(@NonNull final BluetoothGatt gatt, + @NonNull final BluetoothGattCharacteristic characteristic, + @NonNull byte[] data, int status) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicRead(gatt, characteristic, data, status); + } + mainHandler.post(() -> { + if (status != BluetoothGatt.GATT_SUCCESS) { + if (status == GATT_AUTH_FAIL || status == GATT_INSUFFICIENT_AUTHENTICATION) { + PbscLog.d( "Read needs bonding"); + } + + for (Callback readCallback : readCallbacks) { + readCallback.invoke( + "Error reading " + characteristic.getUuid() + " status=" + status, + null); + } + readCallbacks.clear(); + } else if (!readCallbacks.isEmpty()) { + final byte[] dataValue = copyOf(data); + + for (Callback readCallback : readCallbacks) { + readCallback.invoke(null, BleManager.bytesToWritableArray(dataValue)); + } + readCallbacks.clear(); + } + completedCommand(); + }); + + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicWrite(gatt, characteristic, status); + + mainHandler.post(() -> { + if (!writeQueue.isEmpty()) { + byte[] data = writeQueue.poll(); + doWrite(characteristic, data); + } else { + if (status != BluetoothGatt.GATT_SUCCESS) { + if (status == GATT_AUTH_FAIL || status == GATT_INSUFFICIENT_AUTHENTICATION) { + PbscLog.d( "Write needs bonding"); + // *not* doing completedCommand() + return; + } + for (Callback writeCallback : writeCallbacks) { + writeCallback.invoke("Error writing " + characteristic.getUuid() + " status=" + status, null); + } + writeCallbacks.clear(); + } else if (!writeCallbacks.isEmpty()) { + for (Callback writeCallback : writeCallbacks) { + writeCallback.invoke(); + } + writeCallbacks.clear(); + } + completedCommand(); + } + }); + } + + @Override + public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + mainHandler.post(() -> { + if (!registerNotifyCallbacks.isEmpty()) { + if (status == BluetoothGatt.GATT_SUCCESS) { + for (Callback registerNotifyCallback : registerNotifyCallbacks) { + registerNotifyCallback.invoke(); + } + PbscLog.d( "onDescriptorWrite success"); + } else { + for (Callback registerNotifyCallback : registerNotifyCallbacks) { + registerNotifyCallback.invoke("Error writing descriptor status=" + status, null); + } + Log.e(BleManager.LOG_TAG, "Error writing descriptor status=" + status); + } + + registerNotifyCallbacks.clear(); + } else if (!writeDescriptorCallbacks.isEmpty()) { + if (status == BluetoothGatt.GATT_SUCCESS) { + for (Callback callback : writeDescriptorCallbacks) { + callback.invoke(); + } + PbscLog.d( "onDescriptorWrite success"); + } else { + for (Callback callback : writeDescriptorCallbacks) { + callback.invoke("Error writing descriptor status=" + status, null); + } + Log.e(BleManager.LOG_TAG, "Error writing descriptor status=" + status); + } + + writeDescriptorCallbacks.clear(); + } else { + Log.e(BleManager.LOG_TAG, "onDescriptorWrite with no callback"); + } + + completedCommand(); + }); + } + + @Override + public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { + super.onDescriptorRead(gatt, descriptor, status); + + mainHandler.post(() -> { + if (status != BluetoothGatt.GATT_SUCCESS) { + if (status == GATT_AUTH_FAIL || status == GATT_INSUFFICIENT_AUTHENTICATION) { + PbscLog.d( "Read needs bonding"); + } + + for (Callback readDescriptorCallback : readDescriptorCallbacks) { + readDescriptorCallback.invoke( + "Error reading descriptor " + descriptor.getUuid() + " status=" + status, + null); + } + readDescriptorCallbacks.clear(); + } else if (!readDescriptorCallbacks.isEmpty()) { + final byte[] dataValue = copyOf(descriptor.getValue()); + + for (Callback readDescriptorCallback : readDescriptorCallbacks) { + readDescriptorCallback.invoke( + null, + BleManager.bytesToWritableArray(dataValue)); + } + + readDescriptorCallbacks.clear(); + } + completedCommand(); + }); + } + + @Override + public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { + super.onReadRemoteRssi(gatt, rssi, status); + + mainHandler.post(() -> { + if (!readRSSICallbacks.isEmpty()) { + if (status == BluetoothGatt.GATT_SUCCESS) { + updateRssi(rssi); + for (Callback readRSSICallback : readRSSICallbacks) { + readRSSICallback.invoke(null, rssi); + } + } else { + for (Callback readRSSICallback : readRSSICallbacks) { + readRSSICallback.invoke("Error reading RSSI status=" + status, null); + } + } + + readRSSICallbacks.clear(); + } + + completedCommand(); + }); + } + + private String bufferedCharacteristicsKey(String serviceUUID, String characteristicUUID) { + return serviceUUID + "-" + characteristicUUID; + } + + private void clearBuffers() { + for (Map.Entry entry : this.bufferedCharacteristics.entrySet()) + entry.getValue().resetBuffer(); + } + + private void setNotify(UUID serviceUUID, UUID characteristicUUID, final Boolean notify, Callback callback) { + if (!isConnected() || gatt == null) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + BluetoothGattService service = gatt.getService(serviceUUID); + final BluetoothGattCharacteristic characteristic = findNotifyCharacteristic(service, characteristicUUID); + + if (characteristic == null) { + callback.invoke("Characteristic " + characteristicUUID + " not found"); + completedCommand(); + return; + } + + if (!gatt.setCharacteristicNotification(characteristic, notify)) { + callback.invoke("Failed to register notification for " + characteristicUUID); + completedCommand(); + return; + } + + final BluetoothGattDescriptor descriptor = characteristic + .getDescriptor(UUIDHelper.uuidFromString(CHARACTERISTIC_NOTIFICATION_CONFIG)); + if (descriptor == null) { + callback.invoke("Set notification failed for " + characteristicUUID); + completedCommand(); + return; + } + + // Prefer notify over indicate + byte[] value; + if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { + PbscLog.d( "Characteristic " + characteristicUUID + " set NOTIFY"); + value = notify ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; + } else if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) { + PbscLog.d( "Characteristic " + characteristicUUID + " set INDICATE"); + value = notify ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE + : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; + } else { + String msg = "Characteristic " + characteristicUUID + " does not have NOTIFY or INDICATE property set"; + PbscLog.d( msg); + callback.invoke(msg); + completedCommand(); + return; + } + final byte[] finalValue = notify ? value : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE; + + boolean result = false; + try { + result = gatt.setCharacteristicNotification(characteristic, notify); + // Then write to descriptor + registerNotifyCallbacks.addLast(callback); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result &= BluetoothStatusCodes.SUCCESS == gatt.writeDescriptor(descriptor, finalValue); + } else { + descriptor.setValue(finalValue); + result &= gatt.writeDescriptor(descriptor); + } + } catch (Exception e) { + PbscLog.d( "Exception in setNotify", e); + } + + if (!result) { + for (Callback registerNotifyCallback : registerNotifyCallbacks) { + registerNotifyCallback.invoke("writeDescriptor failed for descriptor: " + descriptor.getUuid(), null); + } + registerNotifyCallbacks.clear(); + completedCommand(); + } + } + + public void registerNotify(UUID serviceUUID, UUID characteristicUUID, Integer buffer, Callback callback) { + if (!enqueue(() -> { + PbscLog.d( "registerNotify"); + if (buffer > 1) { + PbscLog.d( "registerNotify using buffer"); + String bufferKey = this.bufferedCharacteristicsKey(serviceUUID.toString(), + characteristicUUID.toString()); + this.bufferedCharacteristics.put(bufferKey, new NotifyBufferContainer(buffer)); + } + this.setNotify(serviceUUID, characteristicUUID, true, callback); + })) { + Log.e(BleManager.LOG_TAG, "Could not enqueue setNotify command to register notify"); + } + } + + public void removeNotify(UUID serviceUUID, UUID characteristicUUID, Callback callback) { + if (!enqueue(() -> { + PbscLog.d( "removeNotify"); + String bufferKey = this.bufferedCharacteristicsKey(serviceUUID.toString(), characteristicUUID.toString()); + if (this.bufferedCharacteristics.containsKey(bufferKey)) { + NotifyBufferContainer buffer = this.bufferedCharacteristics.get(bufferKey); + this.bufferedCharacteristics.remove(bufferKey); + } + this.setNotify(serviceUUID, characteristicUUID, false, callback); + })) { + Log.e(BleManager.LOG_TAG, "Could not enqueue setNotify command to remove notify"); + } + } + + // Some devices reuse UUIDs across characteristics, so we can't use + // service.getCharacteristic(characteristicUUID) + // instead check the UUID and properties for each characteristic in the service + // until we find the best match + // This function prefers Notify over Indicate + private BluetoothGattCharacteristic findNotifyCharacteristic(BluetoothGattService service, + UUID characteristicUUID) { + + try { + // Check for Notify first + List characteristics = service.getCharacteristics(); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 + && characteristicUUID.equals(characteristic.getUuid())) { + return characteristic; + } + } + + // If there wasn't Notify Characteristic, check for Indicate + for (BluetoothGattCharacteristic characteristic : characteristics) { + if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0 + && characteristicUUID.equals(characteristic.getUuid())) { + return characteristic; + } + } + + // As a last resort, try and find ANY characteristic with this UUID, even if it + // doesn't have the correct properties + return service.getCharacteristic(characteristicUUID); + } catch (Exception e) { + Log.e(BleManager.LOG_TAG, "Error retriving characteristic " + characteristicUUID, e); + return null; + } + } + + public void read(UUID serviceUUID, UUID characteristicUUID, final Callback callback) { + enqueue(() -> { + if (!isConnected() || gatt == null) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + BluetoothGattService service = gatt.getService(serviceUUID); + final BluetoothGattCharacteristic characteristic = findReadableCharacteristic(service, characteristicUUID); + + if (characteristic == null) { + callback.invoke("Characteristic " + characteristicUUID + " not found.", null); + completedCommand(); + return; + } + + this.readCallbacks.addLast(callback); + if (!gatt.readCharacteristic(characteristic)) { + for (Callback readCallback : readCallbacks) { + readCallback.invoke("Read failed", null); + } + readCallbacks.clear(); + completedCommand(); + } + }); + } + + public void readDescriptor(UUID serviceUUID, UUID characteristicUUID, UUID descriptorUUID, + final Callback callback) { + enqueue(() -> { + if (!isConnected() || gatt == null) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + BluetoothGattService service = gatt.getService(serviceUUID); + final BluetoothGattCharacteristic characteristic = findReadableCharacteristic(service, characteristicUUID); + + if (characteristic == null) { + callback.invoke("Characteristic " + characteristicUUID + " not found.", null); + completedCommand(); + return; + } + + final BluetoothGattDescriptor descriptor = characteristic.getDescriptor(descriptorUUID); + if (descriptor == null) { + callback.invoke("Read descriptor failed for " + descriptorUUID, null); + completedCommand(); + return; + } + + final int readPermissionBitMask = BluetoothGattDescriptor.PERMISSION_READ + | BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED + | BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM; + if ((descriptor.getPermissions() & readPermissionBitMask) != 0) { + callback.invoke( + "Read descriptor failed for " + descriptorUUID + ": Descriptor is missing read permission", + null); + completedCommand(); + return; + } + + this.readDescriptorCallbacks.addLast(callback); + if (!gatt.readDescriptor(descriptor)) { + for (Callback readDescriptorCallback : readDescriptorCallbacks) { + readDescriptorCallback.invoke("Reading descriptor failed", null); + } + readDescriptorCallbacks.clear(); + completedCommand(); + } + }); + } + + public void writeDescriptor(UUID serviceUUID, UUID characteristicUUID, UUID descriptorUUID, byte[] data, Callback callback) { + enqueue(() -> { + if (!isConnected() || gatt == null) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + BluetoothGattService service = gatt.getService(serviceUUID); + BluetoothGattCharacteristic characteristic = findCharacteristic(service, characteristicUUID); + + if (characteristic == null) { + callback.invoke("Characteristic " + characteristicUUID + " not found."); + completedCommand(); + return; + } + + BluetoothGattDescriptor descriptor = characteristic.getDescriptor(descriptorUUID); + if (descriptor == null) { + callback.invoke("Read descriptor failed for " + descriptorUUID, null); + completedCommand(); + return; + } + + this.writeDescriptorCallbacks.add(callback); + boolean success; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + success = BluetoothStatusCodes.SUCCESS == gatt.writeDescriptor(descriptor, data); + } else { + descriptor.setValue(data); + success = gatt.writeDescriptor(descriptor); + } + if (!success) { + for (Callback writeCallback : writeDescriptorCallbacks) { + writeCallback.invoke("writeDescriptor failed for descriptor: " + descriptor.getUuid(), null); + } + writeDescriptorCallbacks.clear(); + completedCommand(); + } + }); + } + + private byte[] copyOf(byte[] source) { + if (source == null) + return new byte[0]; + final int sourceLength = source.length; + final byte[] copy = new byte[sourceLength]; + System.arraycopy(source, 0, copy, 0, sourceLength); + return copy; + } + + private boolean enqueue(Runnable command) { + + final boolean result = commandQueue.add(command); + + if (result) { + nextCommand(); + } else { + PbscLog.d( "could not enqueue command"); + } + return result; + } + + private void completedCommand() { + commandQueue.poll(); + commandQueueBusy = false; + nextCommand(); + } + + private void nextCommand() { + synchronized (this) { + if (commandQueueBusy) { + PbscLog.d( "Command queue busy"); + return; + } + + final Runnable nextCommand = commandQueue.peek(); + if (nextCommand == null) { + PbscLog.d( "Command queue empty"); + return; + } + + // Execute the next command in the queue + commandQueueBusy = true; + mainHandler.post(new Runnable() { + @Override + public void run() { + try { + nextCommand.run(); + } catch (Exception ex) { + PbscLog.d( "Error, command exception"); + completedCommand(); + } + } + }); + } + } + + public void readRSSI(final Callback callback) { + if (!enqueue(() -> { + if (!isConnected()) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } else if (gatt == null) { + callback.invoke("BluetoothGatt is null", null); + completedCommand(); + return; + } else { + readRSSICallbacks.addLast(callback); + if (!gatt.readRemoteRssi()) { + for (Callback readRSSICallback : readRSSICallbacks) { + readRSSICallback.invoke("Read RSSI failed", null); + } + readRSSICallbacks.clear(); + completedCommand(); + } + } + })) { + PbscLog.d( "Could not queue readRemoteRssi command"); + } + } + + public void refreshCache(Callback callback) { + enqueue(() -> { + try { + if (gatt == null) { + throw new Exception("gatt is null"); + } + + Method localMethod = gatt.getClass().getMethod("refresh", new Class[0]); + boolean res = (Boolean) localMethod.invoke(gatt, new Object[0]); + callback.invoke(null, res); + } catch (Exception localException) { + Log.e(TAG, "An exception occured while refreshing device"); + callback.invoke(localException.getMessage()); + } finally { + completedCommand(); + } + }); + } + + private boolean runServicesDiscovery() { + if (gatt == null) { + return false; + } + return gatt.discoverServices(); + } + + public void retrieveServices(Callback callback) { + enqueue(() -> { + if (!isConnected()) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } else if (gatt == null) { + callback.invoke("BluetoothGatt is null", null); + completedCommand(); + return; + } else { + this.retrieveServicesCallbacks.addLast(callback); + boolean started = runServicesDiscovery(); + if (!started) { + this.retrieveServicesCallbacks.removeLastOccurrence(callback); + callback.invoke("Failed to start service discovery", null); + completedCommand(); + } + } + }); + } + + // Some peripherals re-use UUIDs for multiple characteristics so we need to + // check the properties + // and UUID of all characteristics instead of using + // service.getCharacteristic(characteristicUUID) + private BluetoothGattCharacteristic findReadableCharacteristic(BluetoothGattService service, + UUID characteristicUUID) { + + if (service != null) { + int read = BluetoothGattCharacteristic.PROPERTY_READ; + + List characteristics = service.getCharacteristics(); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if ((characteristic.getProperties() & read) != 0 + && characteristicUUID.equals(characteristic.getUuid())) { + return characteristic; + } + } + + // As a last resort, try and find ANY characteristic with this UUID, even if it + // doesn't have the correct properties + return service.getCharacteristic(characteristicUUID); + } + + return null; + } + + private BluetoothGattCharacteristic findCharacteristic(BluetoothGattService service, + UUID characteristicUUID) { + + if (service != null) { + return service.getCharacteristic(characteristicUUID); + } + + return null; + } + + private void doWrite(final BluetoothGattCharacteristic characteristic, final byte[] data) { + characteristic.setValue(data); + if (!gatt.writeCharacteristic(characteristic)) { + // write without response, caller will handle the callback + for (Callback writeCallback : writeCallbacks) { + writeCallback.invoke("Write failed", null); + } + writeCallbacks.clear(); + completedCommand(); + } + } + + private boolean enqueueWrite(final BluetoothGattCharacteristic characteristic, byte[] data, final Callback callback) { + final byte[] copyOfData = copyOf(data); + final boolean withResponse = characteristic.getWriteType() == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT; + if (withResponse && callback != null) { + writeCallbacks.addLast(callback); + } + return enqueue(() -> { + try { + if (!isConnected() || gatt == null) { + if (withResponse && callback != null) { + if (writeCallbacks.removeLastOccurrence(callback)) { + try { + callback.invoke("Device is not connected", null); + } catch (Exception callbackException) { + Log.e(BleManager.LOG_TAG, "Error invoking write callback for disconnected device", callbackException); + } + } + } + completedCommand(); + return; + } + doWrite(characteristic, copyOfData); + } catch (Exception e) { + Log.e(BleManager.LOG_TAG, "Error in enqueueWrite lambda", e); + if (withResponse && callback != null) { + if (writeCallbacks.removeLastOccurrence(callback)) { + try { + callback.invoke("Write failed: " + e.getMessage(), null); + } catch (Exception callbackException) { + Log.e(BleManager.LOG_TAG, "Error invoking write callback", callbackException); + } + } + } + completedCommand(); + } + }); + } + + public void write(UUID serviceUUID, UUID characteristicUUID, byte[] data, Integer maxByteSize, + Integer queueSleepTime, Callback callback, int writeType) { + enqueue(() -> { + if (!isConnected() || gatt == null) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + BluetoothGattService service = gatt.getService(serviceUUID); + BluetoothGattCharacteristic characteristic = findWritableCharacteristic(service, characteristicUUID, + writeType); + + if (characteristic == null) { + callback.invoke("Characteristic " + characteristicUUID + " not found."); + completedCommand(); + return; + } + + characteristic.setWriteType(writeType); + + if (data.length <= maxByteSize) { + if (!enqueueWrite(characteristic, data, callback)) { + if (BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT == writeType) { + writeCallbacks.removeLastOccurrence(callback); + } + callback.invoke("Write failed"); + completedCommand(); + return; + } else if (BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE == writeType) { + callback.invoke(); + } + } else { + int dataLength = data.length; + int count = 0; + byte[] firstMessage = null; + List splittedMessage = new ArrayList<>(); + + while (count < dataLength && (dataLength - count > maxByteSize)) { + if (count == 0) { + firstMessage = Arrays.copyOfRange(data, count, count + maxByteSize); + } else { + byte[] splitMessage = Arrays.copyOfRange(data, count, count + maxByteSize); + splittedMessage.add(splitMessage); + } + count += maxByteSize; + } + if (count < dataLength) { + // Other bytes in queue + byte[] splitMessage = Arrays.copyOfRange(data, count, data.length); + splittedMessage.add(splitMessage); + } + + if (BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT == writeType) { + writeQueue.addAll(splittedMessage); + if (!enqueueWrite(characteristic, firstMessage, callback)) { + writeQueue.clear(); + writeCallbacks.remove(callback); + callback.invoke("Write failed"); + completedCommand(); + return; + } + } else { + try { + boolean writeError = false; + if (!enqueueWrite(characteristic, firstMessage, callback)) { + writeError = true; + callback.invoke("Write failed"); + } + if (!writeError) { + Thread.sleep(queueSleepTime); + for (byte[] message : splittedMessage) { + if (!enqueueWrite(characteristic, message, callback)) { + writeError = true; + callback.invoke("Write failed"); + break; + } + Thread.sleep(queueSleepTime); + } + if (!writeError) { + callback.invoke(); + } + } + if (writeError) { + completedCommand(); + return; + } + } catch (InterruptedException e) { + callback.invoke("Error during writing"); + completedCommand(); + return; + } + } + } + + completedCommand(); + }); + } + + public void requestConnectionPriority(int connectionPriority, Callback callback) { + enqueue(() -> { + if (gatt != null) { + boolean status = gatt.requestConnectionPriority(connectionPriority); + callback.invoke(null, status); + } else { + callback.invoke("BluetoothGatt is null", null); + } + + completedCommand(); + }); + } + + public void requestMTU(int mtu, Callback callback) { + enqueue(() -> { + if (!isConnected()) { + callback.invoke("Device is not connected", null); + completedCommand(); + return; + } + + if (gatt == null) { + callback.invoke("BluetoothGatt is null", null); + completedCommand(); + return; + } + + requestMTUCallbacks.addLast(callback); + if (!gatt.requestMtu(mtu)) { + for (Callback requestMTUCallback : requestMTUCallbacks) { + requestMTUCallback.invoke("Request MTU failed", null); + } + requestMTUCallbacks.clear(); + completedCommand(); + } + }); + } + + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + super.onMtuChanged(gatt, mtu, status); + mainHandler.post(() -> { + if (!requestMTUCallbacks.isEmpty()) { + if (status == BluetoothGatt.GATT_SUCCESS) { + for (Callback requestMTUCallback : requestMTUCallbacks) { + requestMTUCallback.invoke(null, mtu); + } + } else { + for (Callback requestMTUCallback : requestMTUCallbacks) { + requestMTUCallback.invoke("Error requesting MTU status = " + status, null); + } + } + + requestMTUCallbacks.clear(); + } + + completedCommand(); + }); + } + + // Some peripherals re-use UUIDs for multiple characteristics so we need to + // check the properties + // and UUID of all characteristics instead of using + // service.getCharacteristic(characteristicUUID) + private BluetoothGattCharacteristic findWritableCharacteristic(BluetoothGattService service, + UUID characteristicUUID, int writeType) { + try { + // get write property + int writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE; + if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) { + writeProperty = BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE; + } + + if (service == null) { + throw new Exception("Service is null."); + } + List characteristics = service.getCharacteristics(); + for (BluetoothGattCharacteristic characteristic : characteristics) { + if ((characteristic.getProperties() & writeProperty) != 0 + && characteristicUUID.equals(characteristic.getUuid())) { + return characteristic; + } + } + + // As a last resort, try and find ANY characteristic with this UUID, even if it + // doesn't have the correct properties + return service.getCharacteristic(characteristicUUID); + } catch (Exception e) { + Log.e(BleManager.LOG_TAG, "Error on findWritableCharacteristic", e); + return null; + } + } + + private String generateHashKey(BluetoothGattCharacteristic characteristic) { + return generateHashKey(characteristic.getService().getUuid(), characteristic); + } + + private String generateHashKey(UUID serviceUUID, BluetoothGattCharacteristic characteristic) { + return serviceUUID + "|" + characteristic.getUuid() + "|" + characteristic.getInstanceId(); + } } diff --git a/android/src/main/java/it/innove/PeripheralService.java b/android/src/main/java/it/innove/PeripheralService.java index beb3d10..f26d96e 100644 --- a/android/src/main/java/it/innove/PeripheralService.java +++ b/android/src/main/java/it/innove/PeripheralService.java @@ -3,7 +3,6 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -11,19 +10,18 @@ import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ServiceInfo; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.ResultReceiver; -import android.preference.PreferenceManager; -import android.telecom.Call; import android.util.Log; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import org.json.JSONArray; import org.json.JSONException; @@ -44,6 +42,8 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; +import androidx.core.app.ServiceCompat; +import androidx.core.content.IntentCompat; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -51,6 +51,9 @@ import okhttp3.Response; public class PeripheralService extends Service { + private static final String CHANNEL_ID = "my_channel_01"; + private static final int NOTIFICATION_ID = 1; + public Map peripherals = new LinkedHashMap<>(); private BluetoothAdapter bluetoothAdapter; public ResultReceiver broadcastReciever; @@ -75,7 +78,7 @@ private static final String getLockReturnURL(String networkUrl, String lockuid) @Nullable @Override public IBinder onBind(Intent intent) { - Log.d("ReactNativeBleManager", "bind attempt"); + PbscLog.d("bind attempt"); return null; } @@ -83,21 +86,48 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { super.onCreate(); - String CHANNEL_ID = "my_channel_01"; + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + "Interaction with the bike's Smartlock", + NotificationManager.IMPORTANCE_LOW); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, - "Interaction with the bike's Smartlock", - NotificationManager.IMPORTANCE_DEFAULT); + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); + promoteToForeground(); + } - ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel); + private Notification buildForegroundNotification() { + int icon = getApplicationInfo().icon; + if (icon == 0) { + icon = android.R.drawable.stat_sys_data_bluetooth; } + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(icon) + .setContentText("") + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build(); + } - Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("Interacting with smartlock") - .setContentText("").build(); + private boolean promoteToForeground() { + try { + ServiceCompat.startForeground( + this, + NOTIFICATION_ID, + buildForegroundNotification(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE); + return true; + } catch (Exception e) { + Log.e(BleManager.LOG_TAG, "PeripheralService startForeground failed", e); + return false; + } + } - startForeground(1, notification); + private void sendError(ResultReceiver reciever, String message) { + if (reciever == null) { + return; + } + Bundle bundle = new Bundle(); + bundle.putString("ARGS", new Gson().toJson(new Object[]{message})); + reciever.send(0, bundle); } private BluetoothAdapter getBluetoothAdapter() { @@ -109,31 +139,57 @@ private BluetoothAdapter getBluetoothAdapter() { } public void stopService() { - - //stop foreground effectively (remove icon notification on status bar) - stopForeground(true); + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE); stopSelf(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - final Peripheral peripheral = retrieveOrCreatePeripheral(intent.getStringExtra("UUID")); + if (intent == null) { + Log.w(BleManager.LOG_TAG, "PeripheralService restarted with null intent, stopping"); + stopSelf(); + return START_NOT_STICKY; + } + + final ResultReceiver reciever = IntentCompat.getParcelableExtra( + intent, "resultReciever", ResultReceiver.class); + + if (!promoteToForeground()) { + sendError(reciever, "Foreground service not allowed"); + stopSelf(); + return START_NOT_STICKY; + } + final String action = intent.getStringExtra("ACTION"); - final ResultReceiver reciever = intent.getParcelableExtra("resultReciever"); - this.broadcastReciever = intent.getParcelableExtra("eventReciever"); + if (action == null) { + Log.w(BleManager.LOG_TAG, "PeripheralService missing ACTION, stopping"); + sendError(reciever, "Missing service action"); + stopSelf(); + return START_NOT_STICKY; + } + + final Peripheral peripheral = retrieveOrCreatePeripheral(intent.getStringExtra("UUID")); + if (peripheral == null) { + Log.w(BleManager.LOG_TAG, "PeripheralService invalid UUID, stopping"); + sendError(reciever, "Invalid peripheral uuid"); + stopSelf(); + return START_NOT_STICKY; + } + this.broadcastReciever = IntentCompat.getParcelableExtra( + intent, "eventReciever", ResultReceiver.class); this.lastUUID = intent.getStringExtra("UUID"); - Log.d("ReactNativeBleManager", "Service started"); - Log.d("ReactNativeBleManager", action); + PbscLog.d("Service started"); + PbscLog.d(action); if(action.equals("CONNECT")) { - Log.d("ReactNativeBleManager", "Service connect"); + PbscLog.d("Service connect"); peripheral.connect(new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -143,20 +199,25 @@ public void invoke(Object... args) { } if(action.equals("DISCONNECT")) { - peripheral.disconnect(null, true); - Bundle bundle = new Bundle(); - reciever.send(0, bundle); + peripheral.disconnect(new Callback() { + @Override + public void invoke(Object... args) { + if (reciever != null) { + reciever.send(0, new Bundle()); + } + } + }, true); } if(action.equals("STARTNOTIFICATION")) { - Log.d("ReactNativeBleManager", "Service start notify"); + PbscLog.d("Service start notify"); UUID serviceUUID = UUIDHelper.uuidFromString(intent.getStringExtra("SERVICEUUID")); UUID characteristicUUID = UUIDHelper.uuidFromString(intent.getStringExtra("CHARACTERISTICUUID")); - peripheral.registerNotify(serviceUUID, characteristicUUID,1, new Callback() { + peripheral.registerNotify(serviceUUID, characteristicUUID, 1, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -165,14 +226,14 @@ public void invoke(Object... args) { } if(action.equals("STOPNOTIFICATION")) { - Log.d("ReactNativeBleManager", "Service stop notify"); + PbscLog.d("Service stop notify"); UUID serviceUUID = UUIDHelper.uuidFromString(intent.getStringExtra("SERVICEUUID")); UUID characteristicUUID = UUIDHelper.uuidFromString(intent.getStringExtra("CHARACTERISTICUUID")); peripheral.removeNotify(serviceUUID, characteristicUUID, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -181,15 +242,15 @@ public void invoke(Object... args) { } if(action.equals("WRITE")) { - Log.d("ReactNativeBleManager", "Service start write"); + PbscLog.d("Service start write"); UUID serviceUUID = UUIDHelper.uuidFromString(intent.getStringExtra("SERVICEUUID")); UUID characteristicUUID = UUIDHelper.uuidFromString(intent.getStringExtra("CHARACTERISTICUUID")); final String strMessage = intent.getStringExtra("MESSAGE"); peripheral.write(serviceUUID, characteristicUUID, intent.getByteArrayExtra("DECODED"), intent.getIntExtra("MAXBYTESIZE", 20), null, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); lastWrittenMessage = strMessage; @@ -204,8 +265,8 @@ public void invoke(Object... args) { peripheral.write(serviceUUID, characteristicUUID, intent.getByteArrayExtra("DECODED"), intent.getIntExtra("MAXBYTESIZE", 20), null, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -214,14 +275,14 @@ public void invoke(Object... args) { } if(action.equals("READ")) { - Log.d("ReactNativeBleManager", "Service read"); + PbscLog.d("Service read"); UUID serviceUUID = UUIDHelper.uuidFromString(intent.getStringExtra("SERVICEUUID")); UUID characteristicUUID = UUIDHelper.uuidFromString(intent.getStringExtra("CHARACTERISTICUUID")); peripheral.read(serviceUUID, characteristicUUID, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); if(args.length > 1 && args[1] != null) { @@ -239,12 +300,12 @@ public void invoke(Object... args) { } if(action.equals("RETRIEVESERVICES")) { - Log.d("ReactNativeBleManager", "Service retrieve"); + PbscLog.d("Service retrieve"); peripheral.retrieveServices(new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args[0])); if(args.length > 1 && args[1] != null) { @@ -262,12 +323,12 @@ public void invoke(Object... args) { } if(action.equals("REFRESHCACHE")) { - Log.d("ReactNativeBleManager", "Service refresh cache"); + PbscLog.d("Service refresh cache"); peripheral.refreshCache(new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -279,8 +340,8 @@ public void invoke(Object... args) { peripheral.readRSSI(new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -293,8 +354,8 @@ public void invoke(Object... args) { peripheral.requestConnectionPriority(connectionPriority, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -307,8 +368,8 @@ public void invoke(Object... args) { peripheral.requestMTU(mtu, new Callback() { @Override public void invoke(Object... args) { - Log.d("ReactNativeBleManager", args.toString()); - Log.d("ReactNativeBleManager", "Callback Called"); + PbscLog.d(args.toString()); + PbscLog.d("Callback Called"); Bundle bundle = new Bundle(); bundle.putString("ARGS", new Gson().toJson(args)); reciever.send(0, bundle); @@ -316,7 +377,7 @@ public void invoke(Object... args) { }); } - return 0; + return START_NOT_STICKY; } private String getRandomHexString(int numchars){ @@ -329,13 +390,21 @@ private String getRandomHexString(int numchars){ return sb.toString().substring(0, numchars); } + private static SharedPreferences getDefaultSharedPreferences(Context context) { + Context appContext = context.getApplicationContext(); + return appContext.getSharedPreferences( + appContext.getPackageName() + "_preferences", + Context.MODE_PRIVATE); + } + public void backupEventHandler(String eventName, JSONObject params) { if(!eventName.equals("BleManagerDidUpdateValueForCharacteristic")) { retrieveOrCreatePeripheral(lastUUID).disconnect(null, true); return; } try { - JSONObject serviceRecoveryData = new JSONObject(PreferenceManager.getDefaultSharedPreferences(this).getString("serviceRecoveryData", "")); + JSONObject serviceRecoveryData = new JSONObject( + getDefaultSharedPreferences(this).getString("serviceRecoveryData", "")); String lastSmartlockUsage = serviceRecoveryData.getString("lastSmartlockUsage"); String lockuid = serviceRecoveryData.getString("lockuid"); Boolean lastUsageIsLocking = lastSmartlockUsage.equals("TEMPORARY_LOCK") || lastSmartlockUsage.equals("RETURN"); @@ -356,7 +425,7 @@ public void backupEventHandler(String eventName, JSONObject params) { JSONObject requestBody = new JSONObject(); requestBody.put("otpKey", lastWrittenMessage); String res = post(getConfirmTemplockURL(serviceRecoveryData.getString("url"), lockuid), requestBody.toString(), client, serviceRecoveryData.getString("apiKey"), serviceRecoveryData.getString("token"), isInSSOMode); - Log.d(BleManager.LOG_TAG, "tempLockConfirmed " + res); + PbscLog.d("tempLockConfirmed " + res); } else if(lastSmartlockUsage.equals("RETURN")){ TimeZone tz = TimeZone.getTimeZone("UTC"); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); @@ -368,7 +437,7 @@ public void backupEventHandler(String eventName, JSONObject params) { requestBody.put("sequence", getRandomHexString(10)); requestBody.put("timestamp", timestamp); String res = post(getLockReturnURL(serviceRecoveryData.getString("url"), lockuid), requestBody.toString(), client, serviceRecoveryData.getString("apiKey"), serviceRecoveryData.getString("token"), isInSSOMode); - Log.d(BleManager.LOG_TAG, "returnDone " + res); + PbscLog.d("returnDone " + res); } retrieveOrCreatePeripheral(lastUUID).disconnect(null, true); } else { @@ -379,12 +448,12 @@ public void backupEventHandler(String eventName, JSONObject params) { } } catch (JSONException | IOException e) { retrieveOrCreatePeripheral(lastUUID).disconnect(null, true); - e.printStackTrace(); + Log.e(BleManager.LOG_TAG, "backupEventHandler failed", e); } } private String post(String url, String json, OkHttpClient client, String apiKey, String token, boolean isInSSOMode) throws IOException { - RequestBody body = RequestBody.create(JSON, json); + RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(url) .addHeader("X-API-KEY", apiKey) diff --git a/android/src/main/java/it/innove/ScanManager.java b/android/src/main/java/it/innove/ScanManager.java index fa2747b..a95429f 100644 --- a/android/src/main/java/it/innove/ScanManager.java +++ b/android/src/main/java/it/innove/ScanManager.java @@ -3,6 +3,7 @@ import android.bluetooth.BluetoothAdapter; import android.content.Context; + import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; @@ -13,27 +14,31 @@ public abstract class ScanManager { - protected BluetoothAdapter bluetoothAdapter; - protected Context context; - protected ReactContext reactContext; - protected BleManager bleManager; - protected AtomicInteger scanSessionId = new AtomicInteger(); - - public ScanManager(ReactApplicationContext reactContext, BleManager bleManager) { - context = reactContext; - this.reactContext = reactContext; - this.bleManager = bleManager; - } - - protected BluetoothAdapter getBluetoothAdapter() { - if (bluetoothAdapter == null) { - android.bluetooth.BluetoothManager manager = (android.bluetooth.BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); - bluetoothAdapter = manager.getAdapter(); - } - return bluetoothAdapter; - } - - public abstract void stopScan(Callback callback); - - public abstract void scan(ReadableArray serviceUUIDs, final int scanSeconds, ReadableMap options, Callback callback); + protected BluetoothAdapter bluetoothAdapter; + protected Context context; + protected ReactContext reactContext; + protected BleManager bleManager; + protected AtomicInteger scanSessionId = new AtomicInteger(); + + public ScanManager(ReactApplicationContext reactContext, BleManager bleManager) { + context = reactContext; + this.reactContext = reactContext; + this.bleManager = bleManager; + } + + protected BluetoothAdapter getBluetoothAdapter() { + if (bluetoothAdapter == null) { + android.bluetooth.BluetoothManager manager = (android.bluetooth.BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + bluetoothAdapter = manager.getAdapter(); + } + return bluetoothAdapter; + } + + public abstract void stopScan(Callback callback); + + public abstract void scan(ReadableMap options, Callback callback); + + public abstract boolean isScanning(); + + public abstract void setScanning(boolean value); } diff --git a/android/src/main/java/it/innove/UUIDHelper.java b/android/src/main/java/it/innove/UUIDHelper.java index 100d44d..6ac695b 100644 --- a/android/src/main/java/it/innove/UUIDHelper.java +++ b/android/src/main/java/it/innove/UUIDHelper.java @@ -9,6 +9,27 @@ public class UUIDHelper { // base UUID used to build 128 bit Bluetooth UUIDs public static final String UUID_BASE = "0000XXXX-0000-1000-8000-00805f9b34fb"; + // Validate BLE UUID format (16-bit or 128-bit) + // Returns true if the string is a valid BLE UUID format + public static boolean isValidBLEUUID(String uuidString) { + if (uuidString == null || uuidString.isEmpty()) { + return false; + } + + // Validate 16-bit UUID (4 hex characters) + if (uuidString.length() == 4) { + return uuidString.matches("[0-9A-Fa-f]{4}"); + } + + // Validate 128-bit UUID (standard UUID format) + try { + UUID.fromString(uuidString); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + // handle 16 and 128 bit UUIDs public static UUID uuidFromString(String uuid) { diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 0000000..4bb0707 --- /dev/null +++ b/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build/withBLE') \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..29f3a60 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + ['module:react-native-builder-bob/babel-preset', { modules: 'commonjs' }], + ], +}; diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 0000000..f1dcc3a --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + commit-message: + # Prefix all commit messages with "npm: " + prefix: "npm" diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..f40fbd8 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 0000000..086a5c9 --- /dev/null +++ b/docs/404.html @@ -0,0 +1,25 @@ +--- +permalink: /404.html +layout: default +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..2361683 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3.3" + +gem "just-the-docs" + +gem "webrick" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..606b7f9 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,88 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + colorator (1.1.0) + concurrent-ruby (1.2.2) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.16.3) + forwardable-extended (2.6.0) + google-protobuf (3.25.5-arm64-darwin) + google-protobuf (3.25.5-x86_64-linux) + http_parser.rb (0.8.0) + i18n (1.14.1) + concurrent-ruby (~> 1.0) + jekyll (4.3.4) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + just-the-docs (0.7.0) + jekyll (>= 3.8.5) + jekyll-include-cache + jekyll-seo-tag (>= 2.0) + rake (>= 12.3.1) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (7.0.5) + rake (13.1.0) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.4.2) + rouge (3.30.0) + safe_yaml (1.0.5) + sass-embedded (1.58.3-arm64-darwin) + google-protobuf (~> 3.21) + sass-embedded (1.58.3-x86_64-linux-gnu) + google-protobuf (~> 3.21) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.5.0) + webrick (1.9.1) + +PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + arm64-darwin-24 + x86_64-linux + +DEPENDENCIES + jekyll (~> 4.3.3) + just-the-docs + webrick + +BUNDLED WITH + 2.3.26 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..083297b --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,20 @@ +title: react-native-ble-manager +email: info@innove.it +description: >- + react-native-ble-manager documentation +baseurl: "/react-native-ble-manager" +url: "https://innoveit.github.io" + +theme: just-the-docs + +color_scheme: dark + +aux_links: + "On GitHub": + - https://github.com/innoveit/react-native-ble-manager + +nav_external_links: + - title: Project on GitHub + url: https://github.com/innoveit/react-native-ble-manager + hide_icon: false + opens_in_new_tab: false diff --git a/docs/_includes/footer_custom.html b/docs/_includes/footer_custom.html new file mode 100644 index 0000000..5179afe --- /dev/null +++ b/docs/_includes/footer_custom.html @@ -0,0 +1 @@ +Distributed by an Apache license 2.0. \ No newline at end of file diff --git a/docs/_includes/head_custom.html b/docs/_includes/head_custom.html new file mode 100644 index 0000000..1668c63 --- /dev/null +++ b/docs/_includes/head_custom.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/_includes/nav_footer_custom.html b/docs/_includes/nav_footer_custom.html new file mode 100644 index 0000000..42953b2 --- /dev/null +++ b/docs/_includes/nav_footer_custom.html @@ -0,0 +1,24 @@ +
+ +
+ diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 0000000..4213ea9 --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,119 @@ +.site-title { + @include mq(md) { + font-size: 0.9rem !important; + } + + @include mq(lg) { + font-size: 1rem !important; + } +} + +.switch-container { + display: none; + + @include mq(md) { + display: block; + width: $nav-width-md; + } + + @include mq(lg) { + display: block; + width: $nav-width; + } +} + +.switch { + position: relative; + display: inline-block; + width: 50px; + height: 25px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked+.slider { + background-color: #2196F3; +} + +input:focus+.slider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked+.slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Forma rotonda dello slider */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.slider:before { + height: 21px; + width: 21px; + left: 2px; + bottom: 2px; +} + +.slider { + .icon { + display: none; + position: absolute; + top: 50%; + transform: translateY(-50%); + color: #FFF; + } + + + .sun { + left: 6px; + } + + .moon { + right: -6px; + color: #000; + } +} + + +input:checked+.slider .sun { + display: block; +} + +input:not(:checked)+.slider .moon { + display: block; +} \ No newline at end of file diff --git a/docs/_sass/custom/setup.scss b/docs/_sass/custom/setup.scss new file mode 100644 index 0000000..8e9dca8 --- /dev/null +++ b/docs/_sass/custom/setup.scss @@ -0,0 +1 @@ +//$nav-width: 21.5rem; diff --git a/docs/changelog.markdown b/docs/changelog.markdown new file mode 100644 index 0000000..7f4597c --- /dev/null +++ b/docs/changelog.markdown @@ -0,0 +1,56 @@ +--- +layout: page +title: Changelog +permalink: /changelog/ +nav_order: 100 +--- + +# Changelog + +To read all the details go to the [Github Releases section](https://github.com/innoveit/react-native-ble-manager/releases). + +## Release v12.4.x +- **BREAKING CHANGE** - Refactor `scan` method. +- Added `isStarted` method. + +## Release v12.3.x + +- [Android only] Added `useScanIntent` scan option to receive results via `PendingIntent` on API 26+. + +## Release v12.2.x + +- [iOS only] Feature start notification with buffer. + +## Release v12.1.X + +- Added expo plugin. + + +## Release v12.0.X + +- Added support for React Native 0.76 new architecture. + +## Release v11.5.X + +- [Android only] Implement companion device manager support. + +## Release v11.4.X + +- Add support for Android 14. + +## Release v11.3.X + +- [Android only] Added scan filter for manufacturer data. + +## Release v11.2.X + +- Added `isScanning` method. + +## Release v11.1.X + +- Added `writeDescriptor` method. + +## Release v11.0.X + +- The iOS module has been completely rewritten in Swift. +- The manufacturerData field has been made consistent between Android and iOS. diff --git a/docs/events.markdown b/docs/events.markdown new file mode 100644 index 0000000..595551f --- /dev/null +++ b/docs/events.markdown @@ -0,0 +1,175 @@ +--- +layout: page +title: Events +permalink: /events/ +nav_order: 2 +parent: Usage +--- + +
+ + Table of contents + + {: .text-delta } +1. TOC +{:toc} +
+ +# Events +{: .no_toc } + +Since react-native version 0.76, events are handled with specific methods that return the listener. + +**Examples** + +```js +useEffect(() => { + const onStopListener = BleManager.onStopScan((args) => { + // Scanning is stopped args.status + }); + + return () => { + onStopListener.remove(); + }; + +}, []); +``` +--- + +### onStopScan + +The scanning for peripherals is ended. + +**Arguments** + +- `status` - `Number` - [iOS] the reason for stopping the scan. Error code 10 is used for timeouts, 0 covers everything else. [Android] the reason for stopping the scan (). Error code 10 is used for timeouts + + +--- + +### onDidUpdateState + +The BLE state changed. + +**Arguments** + +- `state` - `String` - the new BLE state. Can be one of `unknown` (iOS only), `resetting` (iOS only), `unsupported`, `unauthorized` (iOS only), `on`, `off`, `turning_on` (android only), `turning_off` (android only). + + +--- + +### onDiscoverPeripheral + +The scanning found a new peripheral. + +**Arguments** + +- `id` - `String` - the id of the peripheral +- `name` - `String` - the name of the peripheral +- `rssi` - `Number` - the RSSI value +- `advertising` - `JSON` - the advertising payload, here are some examples: + - `isConnectable` - `Boolean` + - `serviceUUIDs` - `String[]` + - `manufacturerData` - `JSON` - contains a json with the company id as field and the custom value as raw `bytes` and `data` (Base64 encoded string) + - `serviceData` - `JSON` - contains the raw `bytes` and `data` (Base64 encoded string) + - `txPowerLevel` - `Int` + - `rawData` - [Android only] `JSON` - contains the raw `bytes` and `data` (Base64 encoded string) of all the advertising data + +--- + +### onDidUpdateValueForCharacteristic + +A characteristic notified a new value. + +> Event will only be emitted after successful `startNotification`. + +**Arguments** + +- `value` — `Number[]` — the read value +- `peripheral` — `String` — the id of the peripheral +- `characteristic` — `String` — the UUID of the characteristic +- `service` — `String` — the UUID of the characteristic + +--- + +### onConnectPeripheral + +A peripheral was connected. + +**Arguments** + +- `peripheral` - `String` - the id of the peripheral +- `status` - `Number` - [Android only] connect [`reasons`]() + +--- + +### onDisconnectPeripheral + +A peripheral was disconnected. + +**Arguments** + +- `peripheral` - `String` - the id of the peripheral +- `status` - `Number` - [Android only] disconnect [`reasons`]() +- `domain` - `String` - [iOS only] disconnect error domain +- `code` - `Number` - [iOS only] disconnect error code () + +--- + +### onPeripheralDidBond + +A bond with a peripheral was established. + +**Arguments** + +Object with information about the device. + +--- + +### onCentralManagerWillRestoreState [iOS only] + +This is fired when [`centralManager:WillRestoreState:`](https://developer.apple.com/documentation/corebluetooth/cbcentralmanagerdelegate/1518819-centralmanager) is called (app relaunched in the background to handle a bluetooth event). + +_For more on performing long-term bluetooth actions in the background:_ + +[iOS Bluetooth State Preservation and Restoration](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW10) + +[iOS Relaunch Conditions](https://developer.apple.com/documentation/technotes/tn3115-bluetooth-state-restoration-app-relaunch-rules/) + +**Arguments** + +- `peripherals` - `Array` - an array of previously connected peripherals. + +--- +### onDidUpdateNotificationStateFor [iOS only] + +The peripheral received a request to start or stop providing notifications for a specified characteristic's value. + +**Arguments** + +- `peripheral` - `String` - the id of the peripheral +- `characteristic` - `String` - the UUID of the characteristic +- `isNotifying` - `Boolean` - Is the characteristic notifying or not +- `domain` - `String` - [iOS only] error domain +- `code` - `Number` - [iOS only] error code + +--- + +### onCompanionPeripheral [Android only] + +User picked a device to associate with. + +Null if the request was cancelled by the user. + +**Arguments** + +- `id` - `String` - the id of the peripheral +- `name` - `String` - the name of the peripheral +- `rssi` - `Number` - the RSSI value + +--- + +### onCompanionFailure [Android only] + +Associate callback received a failure or failed to start the intent to +pick the device to associate. \ No newline at end of file diff --git a/docs/expo.markdown b/docs/expo.markdown new file mode 100644 index 0000000..49b7ba4 --- /dev/null +++ b/docs/expo.markdown @@ -0,0 +1,36 @@ +--- +layout: page +title: Expo +permalink: /expo/ +nav_order: 4 +--- + +# Expo + +You can use the library in Expo via a [development build](https://docs.expo.dev/develop/development-builds/introduction/). + +Since Expo 52, it is possible to make full use of the new architecture of React Native and thus version 12.x of the library. + +--- + +To help configure the app, we added a plugin from version 12.1.x, add the configuration in the `app.json` file + +```js +{ + ... + "plugins" : [ + ... + ["react-native-ble-manager", { options }] + ], +} +``` + +**Options** + +| Platform| Name| Type | Default | Description | +| --- | --- | --- | --- | --- | +| Android | `neverForLocation` | `Boolean` | `false` | The BLE is not used for location | +| Android | `companionDeviceEnabled` | `Boolean` | `false` | You are using the companion device | +| Android | `isBleRequired` | `Boolean` | `false` | The app require the BLE to work | +| iOS | `bluetoothAlwaysPermission` | `String | Boolean` | `'Allow $(PRODUCT_NAME) to connect to bluetooth devices'` | The reason you use the BLE | + diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..09d81e3 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/index.markdown b/docs/index.markdown new file mode 100644 index 0000000..d5ec54f --- /dev/null +++ b/docs/index.markdown @@ -0,0 +1,27 @@ +--- +# Feel free to add content and custom Front Matter to this file. +# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults + +layout: home +title: Home +nav_order: 1 +--- + +# A React Native Bluetooth Low Energy library. + +## Introduction + +The library is a simple connection with the OS APIs, the BLE stack should be standard but often has different behaviors based on the device used, the operating system and the BLE chip it connects to. Before opening an issue, verify that the problem is related to the library. + +## Requirements + +RN 0.76+ only the new architecture is supported + +RN 0.60-0.75 supported until 11.5.X +RN 0.40-0.59 supported until 6.7.X +RN 0.30-0.39 supported until 2.4.3 + +## Supported Platforms + +- iOS 15.1+ +- Android (API 23+) \ No newline at end of file diff --git a/docs/install.markdown b/docs/install.markdown new file mode 100644 index 0000000..a487c3a --- /dev/null +++ b/docs/install.markdown @@ -0,0 +1,113 @@ +--- +layout: page +title: Install +permalink: /install/ +nav_order: 2 +--- + +# Install + +The library support the react native autolink feature. + +```shell +npm i --save react-native-ble-manager +``` + +To use BLE in your app, you need to set specific permissions. + +## Android + +Update your manifest file + +```xml +// file: android/app/src/main/AndroidManifest.xml + + + + + + + + + + + + + + + + + + + + + +... +``` + +If you need communication while the app is not in the foreground you need the `ACCESS_BACKGROUND_LOCATION` permission. + +If you are working with Beacons remove the `android:usesPermissionFlags="neverForLocation"`. + +For more information, refer to the [official documentation](https://developer.android.com/develop/connectivity/bluetooth/bt-permissions). + +Runtime permissions must also be requested during app execution. While the exact requirements may vary depending on your product's needs, the code above should generally be sufficient for most use cases. + +```js +/** + * Request runtime permission. + * @returns {boolean} + */ +async function requestBluetoothPermissions() { + if (Platform.OS === 'android') { + const permissions = []; + if (Platform.Version >= 23 && Platform.Version <= 30) { + permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); + } else if (Platform.Version >= 31) { + permissions.push( + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, + ); + } + + if (permissions.length === 0) { + return true; + } + const granted = await PermissionsAndroid.requestMultiple(permissions); + return Object.values(granted).every( + result => result === PermissionsAndroid.RESULTS.GRANTED, + ); + } + return true; +} +``` + +## iOS + +Update the Info.plist file. + +In iOS >= 13 you need to add the `NSBluetoothAlwaysUsageDescription` string key. + +If the deployment target is earlier than iOS 13, you also need to add the `NSBluetoothPeripheralUsageDescription` string key. + +For background use you need to add `central-peripheral` in `UIBackgroundModes` key. Refer to the [documentation](https://developer.apple.com/documentation/xcode/configuring-background-execution-modes/). diff --git a/docs/methods.markdown b/docs/methods.markdown new file mode 100644 index 0000000..4a8473a --- /dev/null +++ b/docs/methods.markdown @@ -0,0 +1,928 @@ +--- +layout: page +title: Methods +permalink: /methods/ +nav_order: 1 +parent: Usage +--- + +
+ + Table of contents + + {: .text-delta } +1. TOC +{:toc} +
+ +# Methods +{: .no_toc } + +## Common (iOS & Android) + +These APIs are available on both platforms. + +### start(options) + +Init the module. +Returns a `Promise` object. +Don't call this multiple times. + +**Arguments** + +- `options` - `JSON` + +The parameter is optional the configuration keys are: + +- `showAlert` - `Boolean` - [iOS only] Show or hide the alert if the bluetooth is turned off during initialization +- `restoreIdentifierKey` - `String` - [iOS only] Unique key to use for CoreBluetooth state restoration +- `queueIdentifierKey` - `String` - [iOS only] Unique key to use for a queue identifier on which CoreBluetooth events will be dispatched +- `forceLegacy` - `Boolean` - [Android only] Force to use the LegacyScanManager + +**Examples** + +```js +BleManager.start({ showAlert: false }).then(() => { + // Success code + console.log("Module initialized"); +}); +``` + +--- + +### isStarted() + +Returns if the module was initialised with `start`. + +**Examples** + +```js +BleManager.isStarted().then((started) => { + // Success code + console.log(`Module is ${isStarted ? '' : 'not '}started`); +}); +``` + +--- + +### scan(scanningOptions) + +Scan for available peripherals. + +See `onDiscoverPeripheral` to get live updates of devices being discovered. + +See `getDiscoveredPeripherals` to get a list of discovered devices after a scan is completed. + +Returns a `Promise` object. + +**Arguments** +- `scanningOptions` - `JSON` - user can control specific ble scan behaviors: + - `serviceUUIDs` - `String[]` - the UUIDs of the services to look for. + - `seconds` - `Integer` - the amount of seconds to scan. If not set or set to `0`, scans until `stopScan()` is called. + - `exactAdvertisingName` - `String[]` - In Android corresponds to the `ScanFilter` [deviceName](). In iOS the filter is done manually before sending the peripheral. + - `allowDuplicates` - `Boolean` - [iOS only] allow duplicates in device scanning + - `numberOfMatches` - `Number` - [Android only] corresponding to [`setNumOfMatches`](). Defaults to `ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT`. /!\ anything other than default may only work when a `ScanFilter` is active /!\ + - `matchMode` - `Number` - [Android only] corresponding to [`setMatchMode`](). Defaults to `ScanSettings.MATCH_MODE_AGGRESSIVE`. + - `callbackType` - `Number` - [Android only] corresponding to [`setCallbackType`](). Defaults `ScanSettings.CALLBACK_TYPE_ALL_MATCHES`. /!\ anything other than default may only work when a `ScanFilter` is active /!\ + - `scanMode` - `Number` - [Android only] corresponding to [`setScanMode`](). Defaults to `ScanSettings.SCAN_MODE_LOW_POWER`. + - `reportDelay` - `Number` - [Android only] corresponding to [`setReportDelay`](). Defaults to `0ms`. + - `phy` - `Number` - [Android only] corresponding to [`setPhy`]() + - `legacy` - `Boolean` - [Android only] corresponding to [`setLegacy`]() + - `manufacturerData` - `Object` - [Android only] corresponding to [`setManufacturerData`](). Filter by manufacturer id or data. + - `manufacturerId` - `Number` - Manufacturer / company id to filter for. + - `manufacturerData` - `Number[]` - Additional manufacturer data filter. + - `manufacturerDataMask` - `Number[]` - Mask for manufacturer data, must have the same length as `manufacturerData`. + For any bit in the mask, set it to 1 if it needs to match the one in manufacturer data, otherwise set it to 0. + - `useScanIntent` - `Boolean` - [Android only, API 26+] deliver scan results through a `PendingIntent` instead of the default callback. Any ongoing callback scan is automatically stopped before switching to this mode. + +**Examples** + +```js +BleManager.scan({ serviceUUIDs: [], seconds: 5 }).then(() => { + // Success code + console.log("Scan started"); +}); +``` + +--- + +### stopScan() + +Stop the scanning. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.stopScan().then(() => { + // Success code + console.log("Scan stopped"); +}); +``` + +--- + +### connect(peripheralId, options) + +Attempts to connect to a peripheral. In many case if you can't connect you have to scan for the peripheral before. +Returns a `Promise` object. + +> In iOS, attempts to connect to a peripheral do not time out (please see [Apple's doc](https://developer.apple.com/documentation/corebluetooth/cbcentralmanager/1518766-connect)), so you might need to set a timer explicitly if you don't want this behavior. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral to connect. +- `options` - `JSON` - The parameter is optional the configuration keys are: + + - `phy` - `Number` - [Android only] corresponding to the preferred phy channel ([`Android doc`]()) + - `autoconnect` - `Boolean` - [Android only] whether to directly connect to the remote device (false) or to automatically connect as soon as the remote device becomes available (true) ([`Android doc`]()) + +**Examples** + +```js +BleManager.connect("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + .then(() => { + // Success code + console.log("Connected"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### disconnect(peripheralId, force) + +Disconnect from a peripheral. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral to disconnect. +- `force` - `boolean` - [Android only] defaults to true. If true force closes gatt + connection and send the event to `onDisconnectPeripheral` + immediately, else disconnects the + connection and waits for [`disconnected state`](https://developer.android.com/reference/android/bluetooth/BluetoothProfile#STATE_DISCONNECTED) to + [`close the gatt connection`]() + and then sends the event to `onDisconnectPeripheral` + +**Examples** + +```js +BleManager.disconnect("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + .then(() => { + // Success code + console.log("Disconnected"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### checkState() + +Force the module to check the state of the native BLE manager and trigger a BleManagerDidUpdateState event. +Resolves to a promise containing the current BleState. + +**Examples** + +```js +BleManager.checkState().then((state) => + console.log(`current BLE state = '${state}'.`) +); +``` + +--- + +### startNotification(peripheralId, serviceUUID, characteristicUUID) + +Start the notification on the specified characteristic, you need to call `retrieveServices` method before. + +Events will be send to `onDidUpdateValueForCharacteristic` when the peripheral notifies a new value for the characteristic. + +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. + +**Examples** + +```js +BleManager.startNotification( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +) + .then(() => { + // Success code + console.log("Notification started"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### stopNotification(peripheralId, serviceUUID, characteristicUUID) + +Stop the notification on the specified characteristic. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. + +--- + +### read(peripheralId, serviceUUID, characteristicUUID) + +Read the current value of the specified characteristic, you need to call `retrieveServices` method before. +Returns a `Promise` object that will resolves to an array of plain integers (`number[]`) representing a `ByteArray` structure. +That array can then be converted to a JS `ArrayBuffer` for example using `Buffer.from()` [thanks to this buffer module](https://github.com/feross/buffer). + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. + +**Examples** + +```js +BleManager.read( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +) + .then((readData) => { + // Success code + console.log("Read: " + readData); + + // https://github.com/feross/buffer + // https://nodejs.org/api/buffer.html#static-method-bufferfromarray + const buffer = Buffer.from(readData); + const sensorData = buffer.readUInt8(1, true); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### write(peripheralId, serviceUUID, characteristicUUID, data, maxByteSize) + +Write with response to the specified characteristic, you need to call `retrieveServices` method before. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. +- `data` - `number[]` - the data to write as a plain integer array representing a `ByteArray` structure. +- `maxByteSize` - `Integer` - specify the max byte size before splitting message, defaults to 20 bytes if not specified + +**Data preparation** + +To convert your data to a `number[]`, you should probably be manipulating a `Buffer` or anything representing a JS `ArrayBuffer`. +This will make sure you are converting from valid byte representations of your data first and not with [an integer outside the expected range](https://techtutorialsx.com/2019/10/27/node-js-converting-array-to-buffer/). + +You can create a buffer from files, numbers or strings easily (see examples bellow). + +```js +// https://github.com/feross/buffer +import { Buffer } from 'buffer'; + +// Creates a Buffer containing the bytes [0x01, 0x02, 0x03]. +const buffer = Buffer.from([1, 2, 3]); + +// Creates a Buffer containing the bytes [0x01, 0x01, 0x01, 0x01] – the entries +// are all truncated using `(value & 255)` to fit into the range 0–255. +const buffer = Buffer.from([257, 257.5, -255, '1']); + +// Creates a Buffer containing the UTF-8-encoded bytes for the string 'tést': +// [0x74, 0xc3, 0xa9, 0x73, 0x74] (in hexadecimal notation) +// [116, 195, 169, 115, 116] (in decimal notation) +const buffer = Buffer.from('tést'); +``` + +Feel free to use other packages or google how to convert into byte array if your data has other format. + +**Examples** + +```js +BleManager.write( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + // encode & extract raw `number[]`. + // Each number should be in the 0-255 range as it is converted from a valid byte. + buffer.toJSON().data +) + .then(() => { + // Success code + console.log("Write: " + data); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### writeWithoutResponse(peripheralId, serviceUUID, characteristicUUID, data, maxByteSize, queueSleepTime) + +Write without response to the specified characteristic, you need to call `retrieveServices` method before. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. +- `data` - `number[]` - the data to write as a plain integer array representing a `ByteArray` structure. (see `write()`). +- `maxByteSize` - `Integer` - (Optional) specify the max byte size +- `queueSleepTime` - `Integer` - (Optional) specify the wait time before each write if the data is greater than maxByteSize + +**Data preparation** + +If your data is not in `number[]` format check info fom the `write()` function example above. + +**Example** + +```js +BleManager.writeWithoutResponse( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + data +) + .then(() => { + // Success code + console.log("Wrote: " + data); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### readRSSI(peripheralId) + +Read the current value of the RSSI. +Returns a `Promise` object resolving with the updated RSSI value (`number`) if it succeeds. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. + +**Examples** + +```js +BleManager.readRSSI("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + .then((rssi) => { + // Success code + console.log("Current RSSI: " + rssi); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### readDescriptor(peripheralId, serviceId, characteristicId, descriptorId) + +Read the current value of the specified descriptor, you need to call `retrieveServices` method before. +Returns a `Promise` object that will resolves to an array of plain integers (`number[]`) representing a `ByteArray` structure. +That array can then be converted to a JS `ArrayBuffer` for example using `Buffer.from()` [thanks to this buffer module](https://github.com/feross/buffer). + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. +- `descriptorUUID` - `String` - the UUID of the descriptor. + +**Examples** + +```js +BleManager.readDescriptor( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXX" +) + .then((readData) => { + // Success code + console.log("Read: " + readData); + + // https://github.com/feross/buffer + // https://nodejs.org/api/buffer.html#static-method-bufferfromarray + const buffer = Buffer.from(readData); + const sensorData = buffer.readUInt8(1, true); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### writeDescriptor(peripheralId, serviceId, characteristicId, descriptorId, data) + +Write a value to the specified descriptor, you need to call `retrieveServices` method before. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. +- `descriptorUUID` - `String` - the UUID of the descriptor. +- `data` - `number[]` - the data to write as a plain integer array representing a `ByteArray` structure. + +**Examples** + +```js +BleManager.writeDescriptor( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXX", + [1, 2] +) + .then(() => { + // Success code + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### retrieveServices(peripheralId[, serviceUUIDs]) + +Retrieve the peripheral's services and characteristics. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUIDs` - `String[]` - [iOS only] only retrieve these services. + +**Examples** + +```js +BleManager.retrieveServices("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX").then( + (peripheralInfo) => { + // Success code + console.log("Peripheral info:", peripheralInfo); + } +); +``` + +--- + +### getConnectedPeripherals(serviceUUIDs) + +Return the connected peripherals. +Returns a `Promise` object. +> In Android, Peripherals "advertising" property can be not set! +> Will be available if peripheral was found through scan before connect. This matches to current Android Bluetooth design specification. + +**Arguments** + +- `serviceUUIDs` - `String[]` - [iOS only] Optional, only retrieve peripherals with these services. Ignored in Android. + +**Examples** + +```js +BleManager.getConnectedPeripherals([]).then((peripheralsArray) => { + // Success code + console.log("Connected peripherals: " + peripheralsArray.length); +}); +``` + +--- + +### getDiscoveredPeripherals() + +Return the discovered peripherals after a scan. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.getDiscoveredPeripherals().then((peripheralsArray) => { + // Success code + console.log("Discovered peripherals: " + peripheralsArray.length); +}); +``` + +--- + +### isPeripheralConnected(peripheralId, serviceUUIDs) + +Check whether a specific peripheral is connected and return `true` or `false`. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - The id/mac address of the peripheral. +- `serviceUUIDs` - `String[]` - [iOS only] Optional, only retrieve peripherals with these services. Ignored in Android. + +**Examples** + +```js +BleManager.isPeripheralConnected( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + [] +).then((isConnected) => { + if (isConnected) { + console.log("Peripheral is connected!"); + } else { + console.log("Peripheral is NOT connected!"); + } +}); +``` + +--- + +### isScanning() + +Checks whether the scan is in progress and return `true` or `false`. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.isScanning().then((isScanning) => { + if (isScanning) { + console.log("Is scanning!"); + } else { + console.log("Is NOT scanning!"); + } +}); +``` + +--- + +## Android-only + +APIs that require Android; many expose platform concepts like ScanSettings, bonding, or adapter state. + +### companionScan() [Android only, API 26+] + +Scan for companion devices. + +Rejects if the companion device manager is not supported on this device. + +The promise it will eventually resolve with either: + +1. peripheral if user selects one +2. null if user "cancels" (i.e. doesn't select anything) + +See `BleManager.supportsCompanion`. + +See: https://developer.android.com/develop/connectivity/bluetooth/companion-device-pairing + +**Arguments** + +- `serviceUUIDs` - `String[]` - List of service UUIDs to use as a filter +- `options` - `JSON` - Additional options + + - `single` - `String?` - Scan only for single peripheral. See Android's `AssociationRequest.Builder.setSingleDevice`. + +**Examples** + +```js +BleManager.companionScan([]).then(peripheral => { + console.log('Associated peripheral', peripheral); +}); +``` + +--- + +### enableBluetooth() [Android only] + +Create the ACTION_REQUEST_ENABLE to ask the user to activate the bluetooth. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.enableBluetooth() + .then(() => { + // Success code + console.log("The bluetooth is already enabled or the user confirm"); + }) + .catch((error) => { + // Failure code + console.log("The user refuse to enable bluetooth"); + }); +``` + +--- + +### supportsCompanion() [Android only] + +Check if current device supports the companion device manager. + +**Examples** + +```js +BleManager.supportsCompanion().then((isSupported) => { + if (isSupported) { + console.log("Companion device manager is supported!"); + } else { + console.log("Companion device manager is NOT supported!"); + } +}); +``` + +--- + +### startNotificationWithBuffer(peripheralId, serviceUUID, characteristicUUID, buffer) [Android only] + +Start the notification on the specified characteristic, you need to call `retrieveServices` method before. The buffer collect messages until the buffer of messages bytes reaches the limit defined with the `buffer` argument and then emit all the collected data. Useful to reduce the number of calls between the native and the react-native part in case of many messages. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `serviceUUID` - `String` - the UUID of the service. +- `characteristicUUID` - `String` - the UUID of the characteristic. +- `buffer` - `Integer` - the capacity of the buffer (bytes) stored before emitting the data for the characteristic. + +**Examples** + +```js +BleManager.startNotificationWithBuffer( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + 1234 +) + .then(() => { + // Success code + console.log("Notification started"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### requestConnectionPriority(peripheralId, connectionPriority) [Android only API 21+] + +Request a connection parameter update. +Returns a `Promise` object which fulfills with the status of the request. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `connectionPriority` - `Integer` - the connection priority to be requested, as follows: + - 0 - balanced priority connection + - 1 - high priority connection + - 2 - low power priority connection + +**Examples** + +```js +BleManager.requestConnectionPriority("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 1) + .then((status) => { + // Success code + console.log("Requested connection priority"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### requestMTU(peripheralId, mtu) [Android only API 21+] + +Request an MTU size used for a given connection. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. +- `mtu` - `Integer` - the MTU size to be requested in bytes. + +**Examples** + +```js +BleManager.requestMTU("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", 512) + .then((mtu) => { + // Success code + console.log("MTU size changed to " + mtu + " bytes"); + }) + .catch((error) => { + // Failure code + console.log(error); + }); +``` + +--- + +### refreshCache(peripheralId) [Android only] + +refreshes the peripheral's services and characteristics cache +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. + +**Examples** + +```js +BleManager.refreshCache("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") + .then((peripheralInfo) => { + // Success code + console.log("cache refreshed!"); + }) + .catch((error) => { + console.error(error); + }); +``` + +--- + +### getAssociatedPeripherals() [Android only, API 26+] + +Retrieve associated peripherals (from companion manager). + +--- + +### removeAssociatedPeripheral(peripheralId) [Android only, API 26+] + +Remove an associated peripheral. + +Rejects if no association is found. + +**Arguments** + +- `peripheralId` - `String` - Peripheral to remove + +--- + +### createBond(peripheralId,peripheralPin) [Android only] + +Start the bonding (pairing) process with the remote device. If you pass peripheralPin (optional), bonding will be auto (without manually entering the pin). +Returns a `Promise` object that will resolve if the bond is successfully created, otherwise it will be rejected with the appropriate error message. +> In Android, Ensure to make one bond request at a time. + +**Arguments** + +- `peripheralId` - `String` - The id/mac address of the peripheral. +- `peripheralPin` - `String` - Optional, will be used to auto-bond if possible. + +**Examples** + +```js +BleManager.createBond(peripheralId) + .then(() => { + console.log("createBond success or there is already an existing one"); + }) + .catch(() => { + console.log("fail to bond"); + }); +``` + +--- + +### removeBond(peripheralId) [Android only] + +Remove a paired device. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - The id/mac address of the peripheral. + +**Examples** + +```js +BleManager.removeBond(peripheralId) + .then(() => { + console.log("removeBond success"); + }) + .catch(() => { + console.log("fail to remove the bond"); + }); +``` + +--- + +### getBondedPeripherals() [Android only] + +Return the bonded peripherals. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.getBondedPeripherals([]).then((bondedPeripheralsArray) => { + // Each peripheral in returned array will have id and name properties + console.log("Bonded peripherals: " + bondedPeripheralsArray.length); +}); +``` + +--- + +### removePeripheral(peripheralId) [Android only] + +Removes a disconnected peripheral from the cached list. +It is useful if the device is turned off, because it will be re-discovered upon turning on again. +Returns a `Promise` object. + +**Arguments** + +- `peripheralId` - `String` - the id/mac address of the peripheral. + +--- + +### setName(name) [Android only] + +Create the request to set the name of the bluetooth adapter. (https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#setName(java.lang.String)) +Returns a `Promise` object. + +**Examples** + +```js +BleManager.setName("INNOVEIT_CENTRAL") + .then(() => { + // Success code + console.log("Name set successfully"); + }) + .catch((error) => { + // Failure code + console.log("Name could not be set"); + }); +``` + +--- + +## iOS-only + +APIs that surface CoreBluetooth limitations or platform-specific helpers. + +### getMaximumWriteValueLengthForWithoutResponse(peripheralId) [iOS only] + +Return the maximum value length for WriteWithoutResponse. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.getMaximumWriteValueLengthForWithoutResponse( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +).then((maxValue) => { + console.log("Maximum length for WriteWithoutResponse: " + maxValue); +}); +``` + +--- + +### getMaximumWriteValueLengthForWithResponse(peripheralId) [iOS only] + +Return the maximum value length for WriteWithResponse. +Returns a `Promise` object. + +**Examples** + +```js +BleManager.getMaximumWriteValueLengthForWithResponse( + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" +).then((maxValue) => { + console.log("Maximum length for WriteWithResponse: " + maxValue); +}); +``` diff --git a/docs/troubleshooting.markdown b/docs/troubleshooting.markdown new file mode 100644 index 0000000..0231b6e --- /dev/null +++ b/docs/troubleshooting.markdown @@ -0,0 +1,16 @@ +--- +layout: page +title: Troubleshooting +permalink: /troubleshooting/ +nav_order: 5 +--- + +# Troubleshooting + +- Remember to use the `start` method before anything. +- If you have problem with old devices try avoid to connect/read/write to a peripheral during scan. +- Android API >= 23 require the ACCESS_COARSE_LOCATION permission to scan for peripherals. React Native >= 0.33 natively support PermissionsAndroid like in the example. +- Android API >= 29 require the ACCESS_FINE_LOCATION permission to scan for peripherals. + React-Native 0.63.X started targeting Android API 29. +- Before write, read or start notification you need to call `retrieveServices` method +- Because location and bluetooth permissions are runtime permissions, you **must** request these permissions at runtime along with declaring them in your manifest. \ No newline at end of file diff --git a/docs/usage.markdown b/docs/usage.markdown new file mode 100644 index 0000000..87e5efb --- /dev/null +++ b/docs/usage.markdown @@ -0,0 +1,11 @@ +--- +layout: page +title: Usage +permalink: /usage/ +nav_order: 3 +has_children: true +--- + +# Usage + +The library must be initialized before use through the [start](../methods/#startoptions) method. This can be done at the launch of the application or just before the actual use of BLE. \ No newline at end of file diff --git a/example/.babelrc b/example/.babelrc deleted file mode 100644 index 12560f9..0000000 --- a/example/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": ["module:metro-react-native-babel-preset"], - "env": { - "production": { - "plugins": ["transform-remove-console"] - } - } -} diff --git a/example/.buckconfig b/example/.buckconfig deleted file mode 100644 index 934256c..0000000 --- a/example/.buckconfig +++ /dev/null @@ -1,6 +0,0 @@ - -[android] - target = Google Inc.:Google APIs:23 - -[maven_repositories] - central = https://repo1.maven.org/maven2 diff --git a/example/.bundle/config b/example/.bundle/config new file mode 100644 index 0000000..848943b --- /dev/null +++ b/example/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/example/.eslintrc.js b/example/.eslintrc.js index 40c6dcd..208cc36 100644 --- a/example/.eslintrc.js +++ b/example/.eslintrc.js @@ -1,4 +1,8 @@ module.exports = { root: true, - extends: '@react-native-community', + extends: ['universe/native'], + rules: { + // Ensures props and state inside functions are always up-to-date + 'react-hooks/exhaustive-deps': 'warn', + }, }; diff --git a/example/.flowconfig b/example/.flowconfig deleted file mode 100644 index b274ad1..0000000 --- a/example/.flowconfig +++ /dev/null @@ -1,73 +0,0 @@ -[ignore] -; We fork some components by platform -.*/*[.]android.js - -; Ignore "BUCK" generated dirs -/\.buckd/ - -; Ignore polyfills -node_modules/react-native/Libraries/polyfills/.* - -; These should not be required directly -; require from fbjs/lib instead: require('fbjs/lib/warning') -node_modules/warning/.* - -; Flow doesn't support platforms -.*/Libraries/Utilities/LoadingView.js - -[untyped] -.*/node_modules/@react-native-community/cli/.*/.* - -[include] - -[libs] -node_modules/react-native/interface.js -node_modules/react-native/flow/ - -[options] -emoji=true - -esproposal.optional_chaining=enable -esproposal.nullish_coalescing=enable - -module.file_ext=.js -module.file_ext=.json -module.file_ext=.ios.js - -munge_underscores=true - -module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' -module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' - -suppress_type=$FlowIssue -suppress_type=$FlowFixMe -suppress_type=$FlowFixMeProps -suppress_type=$FlowFixMeState - -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ -suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError - -[lints] -sketchy-null-number=warn -sketchy-null-mixed=warn -sketchy-number=warn -untyped-type-import=warn -nonstrict-import=warn -deprecated-type=warn -unsafe-getters-setters=warn -unnecessary-invariant=warn -signature-verification-failure=warn -deprecated-utility=error - -[strict] -deprecated-type -nonstrict-import -sketchy-null -unclear-type -unsafe-getters-setters -untyped-import -untyped-type-import - -[version] -^0.122.0 diff --git a/example/.gitattributes b/example/.gitattributes deleted file mode 100644 index d42ff18..0000000 --- a/example/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.pbxproj -text diff --git a/example/.gitignore b/example/.gitignore index ad572e6..685324c 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -20,6 +20,8 @@ DerivedData *.hmap *.ipa *.xcuserstate +ios/.xcode.env.local +ios/ # Android/IntelliJ # @@ -28,6 +30,11 @@ build/ .gradle local.properties *.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +android/ # node.js # @@ -35,12 +42,6 @@ node_modules/ npm-debug.log yarn-error.log -# BUCK -buck-out/ -\.buckd/ -*.keystore -!debug.keystore - # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the @@ -48,12 +49,22 @@ buck-out/ # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/ -*/fastlane/report.xml -*/fastlane/Preview.html -*/fastlane/screenshots +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output # Bundle artifact *.jsbundle -# CocoaPods +# Ruby / CocoaPods /ios/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# Expo +.expo/ + +# diff --git a/example/.node-version b/example/.node-version new file mode 100644 index 0000000..3c03207 --- /dev/null +++ b/example/.node-version @@ -0,0 +1 @@ +18 diff --git a/example/.prettierrc.js b/example/.prettierrc.js deleted file mode 100644 index 5c4de1a..0000000 --- a/example/.prettierrc.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - bracketSpacing: false, - jsxBracketSameLine: true, - singleQuote: true, - trailingComma: 'all', -}; diff --git a/example/.ruby-version b/example/.ruby-version new file mode 100644 index 0000000..49cdd66 --- /dev/null +++ b/example/.ruby-version @@ -0,0 +1 @@ +2.7.6 diff --git a/example/App.js b/example/App.js deleted file mode 100644 index fb797fe..0000000 --- a/example/App.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - * Sample BLE React Native App - * - * @format - * @flow strict-local - */ - -import React, { - useState, - useEffect, -} from 'react'; -import { - SafeAreaView, - StyleSheet, - ScrollView, - View, - Text, - StatusBar, - NativeModules, - NativeEventEmitter, - Button, - Platform, - PermissionsAndroid, - FlatList, - TouchableHighlight, -} from 'react-native'; - -import { - Colors, -} from 'react-native/Libraries/NewAppScreen'; - -import BleManager from '../BleManager'; -const BleManagerModule = NativeModules.BleManager; -const bleManagerEmitter = new NativeEventEmitter(BleManagerModule); - -const App = () => { - const [isScanning, setIsScanning] = useState(false); - const peripherals = new Map(); - const [list, setList] = useState([]); - - - const startScan = () => { - if (!isScanning) { - BleManager.scan([], 3, true).then((results) => { - console.log('Scanning...'); - setIsScanning(true); - }).catch(err => { - console.error(err); - }); - } - } - - const handleStopScan = () => { - console.log('Scan is stopped'); - setIsScanning(false); - } - - const handleDisconnectedPeripheral = (data) => { - let peripheral = peripherals.get(data.peripheral); - if (peripheral) { - peripheral.connected = false; - peripherals.set(peripheral.id, peripheral); - setList(Array.from(peripherals.values())); - } - console.log('Disconnected from ' + data.peripheral); - } - - const handleUpdateValueForCharacteristic = (data) => { - console.log('Received data from ' + data.peripheral + ' characteristic ' + data.characteristic, data.value); - } - - const retrieveConnected = () => { - BleManager.getConnectedPeripherals([]).then((results) => { - if (results.length == 0) { - console.log('No connected peripherals') - } - console.log(results); - for (var i = 0; i < results.length; i++) { - var peripheral = results[i]; - peripheral.connected = true; - peripherals.set(peripheral.id, peripheral); - setList(Array.from(peripherals.values())); - } - }); - } - - const handleDiscoverPeripheral = (peripheral) => { - console.log('Got ble peripheral', peripheral); - if (!peripheral.name) { - peripheral.name = 'NO NAME'; - } - peripherals.set(peripheral.id, peripheral); - setList(Array.from(peripherals.values())); - } - - const testPeripheral = (peripheral) => { - if (peripheral){ - if (peripheral.connected){ - BleManager.disconnect(peripheral.id); - }else{ - BleManager.connect(peripheral.id).then(() => { - let p = peripherals.get(peripheral.id); - if (p) { - p.connected = true; - peripherals.set(peripheral.id, p); - setList(Array.from(peripherals.values())); - } - console.log('Connected to ' + peripheral.id); - - - setTimeout(() => { - - /* Test read current RSSI value */ - BleManager.retrieveServices(peripheral.id).then((peripheralData) => { - console.log('Retrieved peripheral services', peripheralData); - - BleManager.readRSSI(peripheral.id).then((rssi) => { - console.log('Retrieved actual RSSI value', rssi); - let p = peripherals.get(peripheral.id); - if (p) { - p.rssi = rssi; - peripherals.set(peripheral.id, p); - setList(Array.from(peripherals.values())); - } - }); - }); - - // Test using bleno's pizza example - // https://github.com/sandeepmistry/bleno/tree/master/examples/pizza - /* - BleManager.retrieveServices(peripheral.id).then((peripheralInfo) => { - console.log(peripheralInfo); - var service = '13333333-3333-3333-3333-333333333337'; - var bakeCharacteristic = '13333333-3333-3333-3333-333333330003'; - var crustCharacteristic = '13333333-3333-3333-3333-333333330001'; - - setTimeout(() => { - BleManager.startNotification(peripheral.id, service, bakeCharacteristic).then(() => { - console.log('Started notification on ' + peripheral.id); - setTimeout(() => { - BleManager.write(peripheral.id, service, crustCharacteristic, [0]).then(() => { - console.log('Writed NORMAL crust'); - BleManager.write(peripheral.id, service, bakeCharacteristic, [1,95]).then(() => { - console.log('Writed 351 temperature, the pizza should be BAKED'); - - //var PizzaBakeResult = { - // HALF_BAKED: 0, - // BAKED: 1, - // CRISPY: 2, - // BURNT: 3, - // ON_FIRE: 4 - //}; - }); - }); - - }, 500); - }).catch((error) => { - console.log('Notification error', error); - }); - }, 200); - });*/ - - - - }, 900); - }).catch((error) => { - console.log('Connection error', error); - }); - } - } - - } - - useEffect(() => { - BleManager.start({showAlert: false}); - - bleManagerEmitter.addListener('BleManagerDiscoverPeripheral', handleDiscoverPeripheral); - bleManagerEmitter.addListener('BleManagerStopScan', handleStopScan ); - bleManagerEmitter.addListener('BleManagerDisconnectPeripheral', handleDisconnectedPeripheral ); - bleManagerEmitter.addListener('BleManagerDidUpdateValueForCharacteristic', handleUpdateValueForCharacteristic ); - - if (Platform.OS === 'android' && Platform.Version >= 23) { - PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION).then((result) => { - if (result) { - console.log("Permission is OK"); - } else { - PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION).then((result) => { - if (result) { - console.log("User accept"); - } else { - console.log("User refuse"); - } - }); - } - }); - } - - return (() => { - console.log('unmount'); - bleManagerEmitter.removeListener('BleManagerDiscoverPeripheral', handleDiscoverPeripheral); - bleManagerEmitter.removeListener('BleManagerStopScan', handleStopScan ); - bleManagerEmitter.removeListener('BleManagerDisconnectPeripheral', handleDisconnectedPeripheral ); - bleManagerEmitter.removeListener('BleManagerDidUpdateValueForCharacteristic', handleUpdateValueForCharacteristic ); - }) - }, []); - - const renderItem = (item) => { - const color = item.connected ? 'green' : '#fff'; - return ( - testPeripheral(item) }> - - {item.name} - RSSI: {item.rssi} - {item.id} - - - ); - } - - return ( - <> - - - - {global.HermesInternal == null ? null : ( - - Engine: Hermes - - )} - - - -