Set up Bluetooth in a mobile app
Declare and request the iOS and Android Bluetooth permissions, link bare-bluetooth's native addons, and start the worklet only after access is granted.
bare-bluetooth runs inside your app's Bare worklet, but the operating system grants Bluetooth access to the app, not to the worklet. So the host app has to declare what it uses, ask the user at runtime, and ship the native code that the module needs. This guide covers each step for iOS and Android, using two working hosts:
bare-mobile-doctor, an Expo app that usesreact-native-bare-kit. Build a Bluetooth chat for iOS and Android walks through its Bluetooth screen.- The test app of
bare-bluetooth-android, a native Kotlin app that uses Bare Kit directly.
On desktop there is less to do. See Desktop at the end.
For how the pieces fit together, read Bluetooth in Bare first.
On Android, bare-bluetooth works only inside an app that hosts Bare, such as a Bare Kit worklet. It calls the Android Bluetooth API through the app's Java VM, which a plain bare process doesn't have.
Declare the permissions
iOS
iOS terminates an app that touches Bluetooth without a usage description in its Info.plist. Add NSBluetoothAlwaysUsageDescription with a sentence that the user sees in the permission prompt. In an Expo app, it goes under ios.infoPlist in app.json:
"infoPlist": {
"NSCameraUsageDescription": "This app uses the camera to capture video content",
"NSMicrophoneUsageDescription": "This app uses the microphone to capture audio content",
"NSBluetoothAlwaysUsageDescription": "This app uses Bluetooth to discover nearby devices and exchange messages"
}iOS shows the prompt the first time the app creates a Bluetooth manager: the first new Central() or new Server(). If the user declines, the manager's state becomes 'unauthorized' instead of 'poweredOn'. Handle that BluetoothState in your stateChange listener rather than waiting forever.
Android
Android 12 (API level 31) and later splits Bluetooth access into three runtime permissions:
BLUETOOTH_SCANto discover devices.BLUETOOTH_CONNECTto connect to them.BLUETOOTH_ADVERTISEto be discoverable.
A native app declares them in AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />The android:usesPermissionFlags="neverForLocation" attribute states that the app never derives the user's location from scan results, which lets it scan without a location permission. Without that flag, Android also requires ACCESS_FINE_LOCATION for scanning.
An Expo app lists the permissions in app.json instead. bare-mobile-doctor lists:
- The three permissions listed earlier.
ACCESS_FINE_LOCATION, because it doesn't set the flag.- The legacy
BLUETOOTHandBLUETOOTH_ADMINpermissions.
It also raises the minimum SDK to 31 with minSdkVersion, so the app never runs on a release without the new permissions:
"permissions": [
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_ADVERTISE",
"android.permission.BLUETOOTH_CONNECT",
"android.permission.BLUETOOTH_SCAN"
] [
"expo-build-properties",
{
"android": {
"minSdkVersion": 31
},
"ios": {
"deploymentTarget": "16.4",
"enableSceneSupport": true
}
}
],The ios options in the same entry set the app's iOS deployment target and scene support. They aren't Bluetooth settings.
Request access before starting the worklet
On Android, declaring a permission isn't enough. The user has to grant it at runtime, and a Bluetooth call made without it fails. The Android binding surfaces that failure as a thrown Java exception. Both hosts ask first and start the worklet only once everything is granted, so the Bare code never runs without access.
In React Native, use PermissionsAndroid:
async function requestAndroidBluetoothPermissions() {
const permissions = [
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE,
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
].filter(Boolean)
const results = await PermissionsAndroid.requestMultiple(permissions)
const granted = PermissionsAndroid.RESULTS.GRANTED
return permissions.every((permission) => results[permission] === granted)
} if (Platform.OS === 'android') {
const granted = await requestAndroidBluetoothPermissions()
if (!granted) {
const message = 'Android Bluetooth permissions were not granted'
setError(message)
addLog(message)
return
}
}
const worklet = new Worklet()
workletRef.current = worklet
worklet.start('bluetooth.bundle', source, [name, String(logsEnabled)])In a native Kotlin activity, make the same check with checkSelfPermission() and requestPermissions():
private val permissions = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_ADVERTISE
) val missing = permissions.filter { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
if (missing.isEmpty()) run() else requestPermissions(missing.toTypedArray(), 0)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults.isEmpty() || grantResults.any { it != PackageManager.PERMISSION_GRANTED }) {
Log.e("bare-bluetooth-test", "Bluetooth permissions denied, not running the tests")
finish()
return
}
run()
}The activity's run() method starts the Bare Kit worklet. On iOS there is nothing to request up front: the system prompts on first use, as described in the iOS section.
Link the native addons
bare-bluetooth is JavaScript, but it loads native addons: bare-bluetooth-apple on iOS and bare-bluetooth-android on Android. You have to link them into the app. bare-link does this from the worklet's entry point.
On Android, bare-bluetooth-android also carries Java helper classes. bare-link writes them out as a *.classes.jar file next to the addon, and the app has to compile them in too.
bare-mobile-doctor links every worklet into the addon folders that react-native-bare-kit already picks up, with a short script that you run as npm run link:
// Links the native addons loaded by the worklets into react-native-bare-kit's
// addon directories, which its native projects already pick up.
import link from 'bare-link'
const worklets = [
'bare/bluetooth.js',
'bare/checksum.js',
'bare/hypercore/hypercore.mjs',
'bare/hyperdb/hyperdb.mjs',
'bare/ipc.js',
'bare/os.js',
'bare/rocksdb/index.mjs',
'bare/udx.js',
'bare/videocapture.js',
'bare/videoconverter.js'
]
const out = {
ios: 'node_modules/react-native-bare-kit/ios/addons',
android: 'node_modules/react-native-bare-kit/android/src/main/addons'
}
const preset = process.argv[2]
if (preset in out === false) {
throw new Error(`Usage: node scripts/link.mjs <${Object.keys(out).join('|')}>`)
}
for await (const _ of link(worklets, { preset, out: out[preset] })) {
// Nothing to do; the generator does the linking.
}The native test app does the same from Gradle:
- A
linktask runsbare-link. - A
packtask bundles the worklet. - Both tasks run before every build.
- The dependencies pull in the addons'
*.classes.jarfiles.
tasks.register("link", Exec) {
workingDir repo
commandLine([
"node_modules/.bin/bare-link",
"--host", "android-arm64",
"--out", "test/test-app/app/src/main/addons",
"test/test-app/test.js"
])
}
tasks.register("pack", Exec) {
workingDir repo
commandLine([
"node_modules/.bin/bare-pack",
"--host", "android-arm64",
"--linked",
"--defer", "bare-cov",
"--out", "test/test-app/app/src/main/assets/test.bundle",
"test/test-app/test.js"
])
}
preBuild.dependsOn link, pack
dependencies {
api fileTree(dir: "libs", include: ["bare-kit/classes.jar"])
api fileTree(dir: "src/main/addons", include: ["*.classes.jar"])
}Run the link step again whenever you reinstall dependencies or change an addon dependency.
Bundle the worklet
bare-pack packs the worklet into one bundle per app. Pass a --host for every platform and architecture you ship, and --linked so the bundle loads the addons that you linked in the previous section. bare-mobile-doctor builds its Bluetooth bundle with:
"gen:bluetooth": "npx bare-pack --out app/views/bluetooth/bluetooth.bundle.js bare/bluetooth.js --host ios-arm64 --host ios-arm64-simulator --host ios-x64-simulator --host android-arm --host android-arm64 --host android-ia32 --host android-x64 --linked",Bundle a Bare app covers bare-pack in more depth.
Desktop
The same bare-bluetooth code runs with plain bare on macOS and, from 0.6.3, on Linux:
- macOS uses CoreBluetooth, as on iOS. A packaged macOS app needs the same
NSBluetoothAlwaysUsageDescriptionkey in itsInfo.plist. If it runs in the App Sandbox, it also needs thecom.apple.security.device.bluetoothentitlement. A script that you run with plainbaretakes its permission from the terminal app, which needs Bluetooth access under System Settings, Privacy & Security. - Linux goes through the BlueZ daemon, so
bluetoothdhas to be running and the adapter powered on, for example withbluetoothctl power on. Linux has no system pairing prompt for the module to fall back on. If a peer asks to pair, your app gets apairRequestevent with aPairRequestto accept or reject. - Windows isn't supported.
require('bare-bluetooth')fails there.
Where to go next
- Build a Bluetooth chat for iOS and Android: the app these steps set up.
- Embed Bare in a React Native app: worklets and IPC in general.
- Handle app suspension: the radio and the worklet when the app goes to the background.
- Bluetooth in Bare: which package runs on which platform, and why.
Last updated on