unified-ble-manager 4.0.7 → 4.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/MIGRATION_4.0.md +1 -1
- package/README.md +27 -2
- package/RELEASE.md +43 -1
- package/SBOM.cdx.json +4 -4
- package/THIRD_PARTY_LICENSES.json +1 -1
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/BlePlxForegroundService.java +78 -17
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/AndroidConnectedDeviceForegroundServiceDriver.java +18 -0
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/BlePlxForegroundServiceRecoveryReceiver.java +50 -0
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceDriver.java +1 -0
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceLeaseRegistry.java +9 -0
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +110 -9
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolControlModule.java +33 -0
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/generated/NativeProtocolV2Schema.kt +23 -5
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +109 -3
- package/android/src/main/jni/UnifiedBleProtocolJsiBinding.cpp +26 -21
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/BlePlxForegroundServiceLifecycleTest.kt +61 -0
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceLeaseRegistryTest.kt +21 -0
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +304 -0
- package/docs/BONDING.md +20 -0
- package/docs/ELECTRON.md +1 -1
- package/docs/EXPO_PLUGIN.md +30 -3
- package/docs/GAPS.4.0.md +3 -1
- package/docs/GETTING_STARTED.md +8 -6
- package/docs/NODE.md +1 -1
- package/docs/PEERS.md +23 -0
- package/docs/PLATFORMS.md +72 -1
- package/docs/UNIFIED_BLE_4.0_IMPLEMENTATION_PLAN.md +12 -2
- package/docs/WEB.md +1 -1
- package/docs/generated/PLATFORM_SUPPORT.md +1 -1
- package/ios/Generated/NativeProtocolV2Schema.swift +20 -2
- package/ios/NativeProtocol/UnifiedBleProtocolAppleExecution.mm +22 -7
- package/ios/NativeProtocol/UnifiedBleProtocolAppleExecutionState.hpp +6 -4
- package/ios/UnifiedBleProtocolControl.mm +6 -0
- package/lib/commonjs/NativeUnifiedBleProtocolControl.js.map +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js +5 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/commonjs/backends/reactnative/react-native-android-peer-directory.js +115 -0
- package/lib/commonjs/backends/reactnative/react-native-android-peer-directory.js.map +1 -0
- package/lib/commonjs/backends/reactnative/react-native-android-provider.js +28 -1
- package/lib/commonjs/backends/reactnative/react-native-android-provider.js.map +1 -1
- package/lib/commonjs/backends/reactnative/react-native-android-security.js +1 -1
- package/lib/commonjs/backends/reactnative/react-native-android-security.js.map +1 -1
- package/lib/commonjs/expo.js +43 -1
- package/lib/commonjs/expo.js.map +1 -1
- package/lib/commonjs/implementation-version.js +1 -1
- package/lib/commonjs/native-protocol/generated/native-protocol-v2-schema.js +14 -9
- package/lib/commonjs/native-protocol/generated/native-protocol-v2-schema.js.map +1 -1
- package/lib/commonjs/native-protocol/rn-android-boundary.js +66 -8
- package/lib/commonjs/native-protocol/rn-android-boundary.js.map +1 -1
- package/lib/commonjs/native-protocol/rn-apple-boundary.js +8 -2
- package/lib/commonjs/native-protocol/rn-apple-boundary.js.map +1 -1
- package/lib/commonjs/native-protocol/v2-codec.js +25 -0
- package/lib/commonjs/native-protocol/v2-codec.js.map +1 -1
- package/lib/commonjs/public/capabilities.js +2 -6
- package/lib/commonjs/public/capabilities.js.map +1 -1
- package/lib/module/NativeUnifiedBleProtocolControl.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js +5 -1
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/module/backends/reactnative/react-native-android-peer-directory.js +110 -0
- package/lib/module/backends/reactnative/react-native-android-peer-directory.js.map +1 -0
- package/lib/module/backends/reactnative/react-native-android-provider.js +28 -1
- package/lib/module/backends/reactnative/react-native-android-provider.js.map +1 -1
- package/lib/module/backends/reactnative/react-native-android-security.js +1 -1
- package/lib/module/backends/reactnative/react-native-android-security.js.map +1 -1
- package/lib/module/expo.js +43 -1
- package/lib/module/expo.js.map +1 -1
- package/lib/module/implementation-version.js +1 -1
- package/lib/module/native-protocol/generated/native-protocol-v2-schema.js +13 -8
- package/lib/module/native-protocol/generated/native-protocol-v2-schema.js.map +1 -1
- package/lib/module/native-protocol/rn-android-boundary.js +66 -8
- package/lib/module/native-protocol/rn-android-boundary.js.map +1 -1
- package/lib/module/native-protocol/rn-apple-boundary.js +8 -2
- package/lib/module/native-protocol/rn-apple-boundary.js.map +1 -1
- package/lib/module/native-protocol/v2-codec.js +25 -0
- package/lib/module/native-protocol/v2-codec.js.map +1 -1
- package/lib/module/public/capabilities.js +2 -6
- package/lib/module/public/capabilities.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeUnifiedBleProtocolControl.d.ts +6 -0
- package/lib/typescript/commonjs/src/NativeUnifiedBleProtocolControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-boundary.d.ts +6 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-boundary.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-peer-directory.d.ts +28 -0
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-peer-directory.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +3 -2
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/expo.d.ts +5 -0
- package/lib/typescript/commonjs/src/expo.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
- package/lib/typescript/commonjs/src/native-protocol/generated/native-protocol-v2-schema.d.ts +5 -1
- package/lib/typescript/commonjs/src/native-protocol/generated/native-protocol-v2-schema.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts +13 -2
- package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/native-protocol/rn-apple-boundary.d.ts +5 -1
- package/lib/typescript/commonjs/src/native-protocol/rn-apple-boundary.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/public/capabilities.d.ts +1 -0
- package/lib/typescript/commonjs/src/public/capabilities.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeUnifiedBleProtocolControl.d.ts +6 -0
- package/lib/typescript/module/src/NativeUnifiedBleProtocolControl.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-boundary.d.ts +6 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-boundary.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/module/src/backends/reactnative/react-native-android-peer-directory.d.ts +28 -0
- package/lib/typescript/module/src/backends/reactnative/react-native-android-peer-directory.d.ts.map +1 -0
- package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +3 -2
- package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
- package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/module/src/expo.d.ts +5 -0
- package/lib/typescript/module/src/expo.d.ts.map +1 -1
- package/lib/typescript/module/src/implementation-version.d.ts +1 -1
- package/lib/typescript/module/src/native-protocol/generated/native-protocol-v2-schema.d.ts +5 -1
- package/lib/typescript/module/src/native-protocol/generated/native-protocol-v2-schema.d.ts.map +1 -1
- package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts +13 -2
- package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
- package/lib/typescript/module/src/native-protocol/rn-apple-boundary.d.ts +5 -1
- package/lib/typescript/module/src/native-protocol/rn-apple-boundary.d.ts.map +1 -1
- package/lib/typescript/module/src/public/capabilities.d.ts +1 -0
- package/lib/typescript/module/src/public/capabilities.d.ts.map +1 -1
- package/native/PREBUILDS.json +5 -5
- package/native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node +0 -0
- package/native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node +0 -0
- package/native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node +0 -0
- package/native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node +0 -0
- package/native/protocol/generated/NativeProtocolV2Schema.hpp +34 -9
- package/native/protocol/schema/native-protocol-v2-abi.json +18 -6
- package/native/protocol/schema/native-protocol-v2.json +21 -6
- package/native/protocol/src/NativeProtocolV2Codec.cpp +11 -3
- package/package.json +1 -1
- package/plugin/build/withBLEAndroidForegroundService.d.ts +1 -0
- package/plugin/build/withBLEAndroidForegroundService.js +36 -2
- package/src/NativeUnifiedBleProtocolControl.ts +7 -0
- package/src/backends/corebluetooth/corebluetooth-backend.ts +7 -3
- package/src/backends/corebluetooth/corebluetooth-boundary.ts +4 -1
- package/src/backends/reactnative/react-native-android-peer-directory.ts +153 -0
- package/src/backends/reactnative/react-native-android-provider.ts +37 -0
- package/src/backends/reactnative/react-native-android-security.ts +1 -1
- package/src/expo.ts +78 -5
- package/src/implementation-version.ts +1 -1
- package/src/native-protocol/generated/native-protocol-v2-schema.ts +27 -8
- package/src/native-protocol/rn-android-boundary.ts +92 -4
- package/src/native-protocol/rn-apple-boundary.ts +10 -2
- package/src/native-protocol/v2-codec.ts +27 -0
- package/src/public/capabilities.ts +3 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `unified-ble-manager` are documented here.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
No changes yet.
|
|
8
|
+
|
|
9
|
+
## [4.0.9] - 2026-08-29
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
- Correct the README release-truth note so it no longer describes an already
|
|
14
|
+
published version as merely being prepared. The wording now stays truthful
|
|
15
|
+
while a future release branch is ahead of npm and keeps the registry and
|
|
16
|
+
GitHub release as the publication authorities (#179).
|
|
17
|
+
|
|
18
|
+
## [4.0.8] - 2026-08-29
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
- **Apple runtime shutdown no longer retains a closed JavaScript attachment through its invoker.** Closing the native protocol now moves the `CallInvoker` out of shared state before scheduling runtime-thread sink cleanup, breaking the state/invoker/callback ownership cycle. iOS, macOS, and tvOS keep the same public behavior while clean teardown no longer leaves JSI functions alive past their runtime.
|
|
23
|
+
|
|
24
|
+
- **Public capability checks no longer hide implemented operations whose evidence is limited.** `manager.capabilities.supports(id)` and `require(id)` now agree with the core manager: both `supported` and invocable `limited` descriptors are usable, while `unavailable`, `unsupported`, and missing capabilities still fail closed. Callers that need full qualification can inspect `manager.capabilities.get(id).state` and its retained limitations. This fixes Android callers incorrectly discarding the bonded `when-available` reconnect path merely because its physical-radio evidence is still labelled `limited` (#174, PR #170).
|
|
25
|
+
|
|
26
|
+
- **Expo Android connected-device monitoring now has an app-controlled notification and explicit lifecycle recovery.** While an active background lease is held, `manager.background.updateNotification({ title, body? })` updates the existing UBM notification in place without acquiring or starting another service. The configured channel, icon, connected-device service type, ongoing state, session-intent policy, and host-app tap are preserved. `restart: 'while-session-intent-exists'` now also manages boot and package-replaced recovery, but only starts the service when native UBM session intent exists; it never scans or reconnects. Once a recovered service is promoted, a package-scoped `FOREGROUND_READY` signal lets an app-owned headless runtime resume without racing Android's background-service restrictions; normal acquisition resolves its existing caller without launching a redundant headless runtime. Background and restart remain absent/`never` by default, and unsupported hosts reject the operation truthfully (#177, PR #170).
|
|
27
|
+
|
|
28
|
+
- **React Native no longer closes a healthy notification subscription during a legitimate burst of native events.** Android and Apple still fail closed on an undrained native-to-JavaScript queue, but both bridges now retain up to 512 records / 1 MiB before declaring overflow. That bounded budget accommodates common catch-up transfers such as roughly 288 five-minute readings delivered in one 24-hour history response while preserving a visible `stream.overflow` terminal if JavaScript genuinely cannot keep up. The change applies only to the two React Native native-event ingress queues; other backends keep their existing buffering and capability truth (#175).
|
|
29
|
+
|
|
30
|
+
- **React Native Android now exposes the system bonded peer directory.** `manager.peers.bonded()` reads the ABI-6 Android bond table, returns deterministic version-1 system-scoped references, and `manager.peers.resolve()` rechecks that a saved reference remains bonded before reconnecting. `manager.connect(peer, { intent: 'when-available' })` reaches Android's queued auto-connect path. Bonded metadata does not imply reachability; Android reports unknown reachability and preserves `permission.denied` instead of returning an empty list. Other backends keep their truthful unsupported boundaries, and Web origin-authorized devices remain distinct from bonded peers.
|
|
31
|
+
|
|
32
|
+
- **React Native Android now honours `PairOptions.transport: 'le'` instead of discarding it.** The public security layer validated and forwarded the selector, but the React Native Android backend dropped it before the native command and always called parameterless `createBond()`. Explicit LE pairing now crosses native-protocol ABI 5 and invokes Android's transport-selecting bond operation with `BluetoothDevice.TRANSPORT_LE`; `'auto'` remains the platform default. Unsupported or rejected explicit selection fails closed rather than silently changing the request. Because Android API 36 cannot publicly query bond state by transport, only an LE-only device's existing bond satisfies an explicit-LE `already-paired` result; dual-mode, classic, and unknown devices retry the directed operation instead of risking a false success from a BR/EDR-only bond. Reflection-wrapped permission failures retain `permissionDenied`. BlueZ and WinRT already pair through BLE-only device objects, while Apple and Web keep generic pairing unsupported; those platform semantics are unchanged.
|
|
33
|
+
|
|
34
|
+
- **Every GATT notification was dropped on both React Native backends, and characteristic and descriptor reads with them.** A notification's payload is carried as a binary reference, and the protocol requires that reference to name the operation the event belongs to — the codec compares the two for equality (`requireBinaryCorrelation`). Both bindings stamped the subscribe's correlation on the event while minting the payload under a correlation of their own (`"notification:<subscription>:<ordinal>"` on Android, `"apple-notification:..."` on Apple), a combination that can never validate. Every notification was therefore refused inside our own codec before reaching a caller, so a subscription delivered nothing at all while the radio received the peer perfectly well. The read paths decorated their correlation the same way (`"read:<epoch>:<nonce>"`, `"apple-read:<nonce>"`) and failed the same check on the result record. All four sites now mint the payload under the owning operation's nonce, as the write path always did.
|
|
35
|
+
|
|
36
|
+
Sharing one correlation across a subscription's notifications is safe: `OwnedBinaryPayloadStore` keys retained payloads by a freshly generated owner token and releases by that token, treating the correlation as metadata it cross-checks rather than as a unique key.
|
|
37
|
+
|
|
38
|
+
The deterministic layer could not catch this, which is why it shipped: the React Native test doubles minted their own notification correlations and omitted the operation correlation entirely, so they modelled a record the native codec refuses to deliver, and the suite stayed green against a shape no device could ever produce. `ReactNativeAndroidProtocolBoundary` now enforces the same rule the native codec enforces, the doubles emit the shape the bindings actually emit, and the codec's own tests pin both the notification event and the read result. Live-device evidence showed the platform receiving a peer's reply chunks while the application received none (#168).
|
|
39
|
+
|
|
5
40
|
## [4.0.7] - 2026-08-26
|
|
6
41
|
|
|
7
42
|
### Changed behaviour
|
|
@@ -62,7 +97,7 @@ The first release cut against a live peripheral on Android as well as Linux. Mos
|
|
|
62
97
|
|
|
63
98
|
## [4.0.5] - 2026-08-25
|
|
64
99
|
|
|
65
|
-
First release cut against a live peripheral. Every fix below came from driving a real
|
|
100
|
+
First release cut against a live peripheral. Every fix below came from driving a real BLE device from an Android phone and a Linux/BlueZ host, and several are defects no unit test had reason to catch. Does not retag `v4.0.4`.
|
|
66
101
|
|
|
67
102
|
### Fixes
|
|
68
103
|
|
package/MIGRATION_4.0.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Migrating from react-native-ble-plx
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This source targets `4.0.9`. The `4.0.0` release is a new package and a new contract. It is **not a source-compatible rename**. There is no `new BleManager()` facade, no Base64 characteristic values, and no public transaction IDs.
|
|
6
6
|
|
|
7
7
|
This page is for a React Native app that already uses `react-native-ble-plx`. Web, Electron, Node, and Tauri are new hosts — use those pages after you understand the RN rewrite.
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
It is an evolution of `react-native-ble-plx`, rewritten as a **cross-platform unified product**. One bytes-first BLE model and lifecycle semantics across hosts, with host-specific construction and ownership. The root package never picks a radio for you, and it will not quietly fall back to a simulator or a different backend.
|
|
8
8
|
|
|
9
|
-
Install `unified-ble-manager` from npm
|
|
9
|
+
Install `unified-ble-manager` from npm. The registry and provenance attached to
|
|
10
|
+
the tag-driven release are the authority for the current `latest` version. The
|
|
11
|
+
root import does not pick a radio. Package SemVer and backend support labels are
|
|
12
|
+
independent: each radio backend keeps its evidence-derived label. See
|
|
13
|
+
[`docs/PLATFORMS.md`](docs/PLATFORMS.md).
|
|
14
|
+
|
|
15
|
+
This source tree is versioned `4.0.9`. Install the exact version shown in the npm
|
|
16
|
+
registry. During release preparation, the version in `package.json` can be ahead
|
|
17
|
+
of npm until the matching tag-driven workflow publishes it; the registry and
|
|
18
|
+
GitHub release remain authoritative.
|
|
19
|
+
|
|
20
|
+
> **4.0 development note:** The 4.0 line is the real-application proving ground
|
|
21
|
+
> for a simpler, stronger 4.1. Develop carefully against it: pin the version you
|
|
22
|
+
> validate, read the changelog when upgrading, inspect capability limitations,
|
|
23
|
+
> and report real-device behavior. Missing hardware evidence remains visible;
|
|
24
|
+
> it does not make an implemented operation unusable.
|
|
10
25
|
|
|
11
26
|
> Sponsored by [Imagi Explain](https://imagiexplain.com) — researched, narrated whiteboard explainers from a prompt, a PDF, or your notes.
|
|
12
27
|
|
|
@@ -85,6 +100,12 @@ const manager = await createReactNativeBleManager({
|
|
|
85
100
|
|
|
86
101
|
On Android 12+ the app must request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` itself. The library does not call `PermissionsAndroid`.
|
|
87
102
|
|
|
103
|
+
On Android, `manager.peers.bonded()` lists paired system peers and
|
|
104
|
+
`manager.peers.resolve(reference)` rechecks a saved reference before
|
|
105
|
+
`manager.connect(peer, { intent: 'when-available' })`; paired does not mean
|
|
106
|
+
reachable. See [`docs/PEERS.md`](docs/PEERS.md) for the persistence and error
|
|
107
|
+
semantics.
|
|
108
|
+
|
|
88
109
|
### Expo plugin
|
|
89
110
|
|
|
90
111
|
Use an Expo development build, never Expo Go. Plugin options live in
|
|
@@ -209,7 +230,11 @@ Web Bluetooth replaces the scan with `ble.choose(...)` from a user gesture. Taur
|
|
|
209
230
|
|
|
210
231
|
Controls report the truth of the instantiated host backend, including
|
|
211
232
|
`supported`, `limited`, `unavailable`, or `unsupported`; host family alone is
|
|
212
|
-
not evidence of support.
|
|
233
|
+
not evidence of support. `manager.capabilities.supports(id)` answers whether
|
|
234
|
+
the operation is implemented and invocable, so it returns `true` for both
|
|
235
|
+
`supported` and `limited`. Use `manager.capabilities.get(id)` when application
|
|
236
|
+
policy needs to distinguish full qualification from a named limitation.
|
|
237
|
+
Readiness is unsupported until the backend advertises
|
|
213
238
|
the readiness capability, and a readiness event does not prove a later payload
|
|
214
239
|
was retained.
|
|
215
240
|
|
package/RELEASE.md
CHANGED
|
@@ -81,7 +81,7 @@ Active `4.0.0-rc.*` release-train candidates publish to npm `latest` so a bare `
|
|
|
81
81
|
|
|
82
82
|
On release day, set `release_candidate` to the exact candidate required by the
|
|
83
83
|
release plan. RC2, RC3, RC4, `4.0.0-rc.4.1`, and RC5 are already immutable
|
|
84
|
-
once tagged. Stable `4.0.0
|
|
84
|
+
once tagged. Stable `4.0.0` through `4.0.8` are immutable. `4.0.9` is the current train head.
|
|
85
85
|
|
|
86
86
|
```sh
|
|
87
87
|
release_candidate=4.0.0-rc.N
|
|
@@ -133,6 +133,48 @@ The `v4.0.3` tag is immutable published history. Do not recreate or move it.
|
|
|
133
133
|
git tag -a v4.0.3 -m "v4.0.3"
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
## Releasing 4.0.9
|
|
137
|
+
|
|
138
|
+
The `v4.0.9` tag must identify the exact current `main` commit after canonical
|
|
139
|
+
CI passes. Do not tag this release branch directly.
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
git fetch origin --tags
|
|
143
|
+
git checkout main
|
|
144
|
+
git pull --ff-only origin main
|
|
145
|
+
|
|
146
|
+
test "$(git branch --show-current)" = "main"
|
|
147
|
+
test "$(node -p "require('./package.json').version")" = "4.0.9"
|
|
148
|
+
git diff --exit-code
|
|
149
|
+
git diff --cached --exit-code
|
|
150
|
+
|
|
151
|
+
git tag -a v4.0.9 -m "v4.0.9"
|
|
152
|
+
git push origin v4.0.9
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Before tagging, confirm release-note extraction finds `## [4.0.9]`.
|
|
156
|
+
|
|
157
|
+
## Releasing 4.0.8
|
|
158
|
+
|
|
159
|
+
The `v4.0.8` tag must identify the exact current `main` commit after canonical
|
|
160
|
+
CI passes. Do not tag this feature branch or `release/4.0.8` directly.
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
git fetch origin --tags
|
|
164
|
+
git checkout main
|
|
165
|
+
git pull --ff-only origin main
|
|
166
|
+
|
|
167
|
+
test "$(git branch --show-current)" = "main"
|
|
168
|
+
test "$(node -p "require('./package.json').version")" = "4.0.8"
|
|
169
|
+
git diff --exit-code
|
|
170
|
+
git diff --cached --exit-code
|
|
171
|
+
|
|
172
|
+
git tag -a v4.0.8 -m "v4.0.8"
|
|
173
|
+
git push origin v4.0.8
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Before tagging, confirm release-note extraction finds `## [4.0.8]`.
|
|
177
|
+
|
|
136
178
|
## Releasing 4.0.7
|
|
137
179
|
|
|
138
180
|
Same shape as 4.0.6. The release workflow verifies that the tag points at the
|
package/SBOM.cdx.json
CHANGED
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "library",
|
|
9
|
-
"bom-ref": "pkg:npm/unified-ble-manager@4.0.
|
|
9
|
+
"bom-ref": "pkg:npm/unified-ble-manager@4.0.9",
|
|
10
10
|
"name": "unified-ble-manager",
|
|
11
|
-
"version": "4.0.
|
|
11
|
+
"version": "4.0.9",
|
|
12
12
|
"licenses": [
|
|
13
13
|
{
|
|
14
14
|
"expression": "Apache-2.0"
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
|
-
"purl": "pkg:npm/unified-ble-manager@4.0.
|
|
17
|
+
"purl": "pkg:npm/unified-ble-manager@4.0.9"
|
|
18
18
|
},
|
|
19
19
|
"properties": [
|
|
20
20
|
{
|
|
@@ -560,7 +560,7 @@
|
|
|
560
560
|
"dependsOn": []
|
|
561
561
|
},
|
|
562
562
|
{
|
|
563
|
-
"ref": "pkg:npm/unified-ble-manager@4.0.
|
|
563
|
+
"ref": "pkg:npm/unified-ble-manager@4.0.9",
|
|
564
564
|
"dependsOn": [
|
|
565
565
|
"pkg:npm/%40babel/runtime@7.29.7",
|
|
566
566
|
"pkg:npm/%40tauri-apps/api@2.11.1",
|
package/android/src/main/java/com/sfourdrinier/unifiedblemanager/BlePlxForegroundService.java
CHANGED
|
@@ -3,6 +3,7 @@ package com.sfourdrinier.unifiedblemanager;
|
|
|
3
3
|
import android.app.Notification;
|
|
4
4
|
import android.app.NotificationChannel;
|
|
5
5
|
import android.app.NotificationManager;
|
|
6
|
+
import android.app.PendingIntent;
|
|
6
7
|
import android.app.Service;
|
|
7
8
|
import android.content.Context;
|
|
8
9
|
import android.content.Intent;
|
|
@@ -26,6 +27,8 @@ import java.util.Map;
|
|
|
26
27
|
/** Explicit connected-device foreground lease. It performs no scan or reconnect work. */
|
|
27
28
|
public final class BlePlxForegroundService extends Service {
|
|
28
29
|
public static final String ACTION_START = "com.sfourdrinier.unifiedblemanager.background.START";
|
|
30
|
+
public static final String ACTION_FOREGROUND_READY =
|
|
31
|
+
"com.sfourdrinier.unifiedblemanager.background.FOREGROUND_READY";
|
|
29
32
|
public static final String EXTRA_ACK = "backgroundStartAcknowledgement";
|
|
30
33
|
public static final int ACK_STARTED = 1;
|
|
31
34
|
public static final int ACK_FAILED = 2;
|
|
@@ -37,6 +40,8 @@ public final class BlePlxForegroundService extends Service {
|
|
|
37
40
|
private static final String EXTRA_BODY = "body";
|
|
38
41
|
private static final String EXTRA_ICON_NAME = "iconName";
|
|
39
42
|
private static final String EXTRA_RESTART_STICKY = "restartSticky";
|
|
43
|
+
private static volatile BlePlxForegroundService activeService;
|
|
44
|
+
private volatile ForegroundServiceNotificationConfiguration activeConfiguration;
|
|
40
45
|
|
|
41
46
|
public static Intent startIntent(
|
|
42
47
|
Context context,
|
|
@@ -75,28 +80,15 @@ public final class BlePlxForegroundService extends Service {
|
|
|
75
80
|
return START_NOT_STICKY;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
final String title = configuration.getTitle();
|
|
81
|
-
final String body = configuration.getBody();
|
|
83
|
+
activeService = this;
|
|
84
|
+
activeConfiguration = configuration;
|
|
82
85
|
if (!getSharedPreferences("unified-ble-manager", MODE_PRIVATE)
|
|
83
86
|
.edit()
|
|
84
87
|
.putBoolean(SESSION_INTENT_PREFERENCE, configuration.restartWhileSessionIntentExists())
|
|
85
88
|
.commit()) {
|
|
86
89
|
throw new IllegalStateException("Android could not persist the foreground-service session intent.");
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
final Notification.Builder builder =
|
|
90
|
-
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
91
|
-
? new Notification.Builder(this, channelId)
|
|
92
|
-
: new Notification.Builder(this);
|
|
93
|
-
builder
|
|
94
|
-
.setSmallIcon(iconResource(this, configuration.getIconName()))
|
|
95
|
-
.setContentTitle(title)
|
|
96
|
-
.setOngoing(true)
|
|
97
|
-
.setCategory(Notification.CATEGORY_SERVICE);
|
|
98
|
-
if (body != null) builder.setContentText(body);
|
|
99
|
-
final Notification notification = builder.build();
|
|
91
|
+
final Notification notification = buildNotification(configuration);
|
|
100
92
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
101
93
|
startForeground(
|
|
102
94
|
ForegroundServiceNotificationConfiguration.NOTIFICATION_ID,
|
|
@@ -106,6 +98,13 @@ public final class BlePlxForegroundService extends Service {
|
|
|
106
98
|
startForeground(ForegroundServiceNotificationConfiguration.NOTIFICATION_ID, notification);
|
|
107
99
|
}
|
|
108
100
|
acknowledge(intent, ACK_STARTED, null);
|
|
101
|
+
// A normal acquisition already has a live JS caller waiting on EXTRA_ACK;
|
|
102
|
+
// starting a second headless runtime before that lease commits is both
|
|
103
|
+
// redundant and racy. Only recovery starts (boot, package replacement,
|
|
104
|
+
// or START_STICKY recreation) need the app-owned wake signal.
|
|
105
|
+
if (intent == null || !intent.hasExtra(EXTRA_ACK)) {
|
|
106
|
+
sendBroadcast(new Intent(ACTION_FOREGROUND_READY).setPackage(getPackageName()));
|
|
107
|
+
}
|
|
109
108
|
return configuration.restartWhileSessionIntentExists() ? START_STICKY : START_NOT_STICKY;
|
|
110
109
|
} catch (RuntimeException error) {
|
|
111
110
|
acknowledge(intent, ACK_FAILED, error.getMessage());
|
|
@@ -117,16 +116,50 @@ public final class BlePlxForegroundService extends Service {
|
|
|
117
116
|
new IllegalStateException("Android could not clear the foreground-service session intent."));
|
|
118
117
|
}
|
|
119
118
|
stopSelf();
|
|
119
|
+
activeService = null;
|
|
120
|
+
activeConfiguration = null;
|
|
120
121
|
return START_NOT_STICKY;
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
@Override
|
|
125
|
-
public void onDestroy() {
|
|
126
|
+
public synchronized void onDestroy() {
|
|
127
|
+
activeService = null;
|
|
128
|
+
activeConfiguration = null;
|
|
126
129
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
127
130
|
super.onDestroy();
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
public static void updateNotification(String title, String body) {
|
|
134
|
+
final BlePlxForegroundService service = activeService;
|
|
135
|
+
if (service == null || service.activeConfiguration == null) {
|
|
136
|
+
throw new com.sfourdrinier.unifiedblemanager.background.ForegroundServiceControlException(
|
|
137
|
+
"foregroundServiceNotRunning",
|
|
138
|
+
"The connected-device foreground service is not running; acquire a background lease first.");
|
|
139
|
+
}
|
|
140
|
+
synchronized (service) {
|
|
141
|
+
final ForegroundServiceNotificationConfiguration current = service.activeConfiguration;
|
|
142
|
+
if (current == null) {
|
|
143
|
+
throw new com.sfourdrinier.unifiedblemanager.background.ForegroundServiceControlException(
|
|
144
|
+
"foregroundServiceNotRunning", "The connected-device foreground service is not running; acquire a background lease first.");
|
|
145
|
+
}
|
|
146
|
+
service.activeConfiguration = ForegroundServiceNotificationConfiguration.fromValues(
|
|
147
|
+
current.getChannelId(),
|
|
148
|
+
current.getChannelName(),
|
|
149
|
+
title,
|
|
150
|
+
body,
|
|
151
|
+
current.getIconName(),
|
|
152
|
+
current.restartWhileSessionIntentExists());
|
|
153
|
+
final NotificationManager manager = service.getSystemService(NotificationManager.class);
|
|
154
|
+
if (manager == null) {
|
|
155
|
+
throw new com.sfourdrinier.unifiedblemanager.background.ForegroundServiceControlException(
|
|
156
|
+
"foregroundServiceNotRunning", "Android notification service is unavailable.");
|
|
157
|
+
}
|
|
158
|
+
manager.notify(ForegroundServiceNotificationConfiguration.NOTIFICATION_ID,
|
|
159
|
+
service.buildNotification(service.activeConfiguration));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
@Nullable
|
|
131
164
|
@Override
|
|
132
165
|
public IBinder onBind(Intent intent) {
|
|
@@ -141,6 +174,34 @@ public final class BlePlxForegroundService extends Service {
|
|
|
141
174
|
new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW));
|
|
142
175
|
}
|
|
143
176
|
|
|
177
|
+
private Notification buildNotification(ForegroundServiceNotificationConfiguration configuration) {
|
|
178
|
+
ensureChannel(configuration.getChannelId(), configuration.getChannelName());
|
|
179
|
+
final Notification.Builder builder =
|
|
180
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
181
|
+
? new Notification.Builder(this, configuration.getChannelId())
|
|
182
|
+
: new Notification.Builder(this);
|
|
183
|
+
final Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
|
184
|
+
if (launchIntent == null) {
|
|
185
|
+
throw new com.sfourdrinier.unifiedblemanager.background.ForegroundServiceControlException(
|
|
186
|
+
"foregroundServiceNotConfigured", "The host app has no launchable activity for the notification tap.");
|
|
187
|
+
}
|
|
188
|
+
final PendingIntent contentIntent = PendingIntent.getActivity(
|
|
189
|
+
this,
|
|
190
|
+
ForegroundServiceNotificationConfiguration.NOTIFICATION_ID,
|
|
191
|
+
launchIntent,
|
|
192
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
193
|
+
? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
|
194
|
+
: PendingIntent.FLAG_UPDATE_CURRENT);
|
|
195
|
+
builder
|
|
196
|
+
.setSmallIcon(iconResource(this, configuration.getIconName()))
|
|
197
|
+
.setContentTitle(configuration.getTitle())
|
|
198
|
+
.setContentIntent(contentIntent)
|
|
199
|
+
.setOngoing(true)
|
|
200
|
+
.setCategory(Notification.CATEGORY_SERVICE);
|
|
201
|
+
if (configuration.getBody() != null) builder.setContentText(configuration.getBody());
|
|
202
|
+
return builder.build();
|
|
203
|
+
}
|
|
204
|
+
|
|
144
205
|
private static String required(Intent intent, String name) {
|
|
145
206
|
final String value = intent.getStringExtra(name);
|
|
146
207
|
if (value == null || value.isEmpty()) {
|
|
@@ -32,6 +32,11 @@ public final class AndroidConnectedDeviceForegroundServiceDriver
|
|
|
32
32
|
|
|
33
33
|
@Override
|
|
34
34
|
public void start(String reason) {
|
|
35
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
36
|
+
throw new ForegroundServiceControlException(
|
|
37
|
+
"foregroundServiceMainThreadUnavailable",
|
|
38
|
+
"Android foreground-service acquisition cannot wait for promotion on the main thread.");
|
|
39
|
+
}
|
|
35
40
|
final ForegroundServiceNotificationConfiguration configuration = configuration();
|
|
36
41
|
requireRuntimePermissions();
|
|
37
42
|
final CountDownLatch acknowledgement = new CountDownLatch(1);
|
|
@@ -116,6 +121,19 @@ public final class AndroidConnectedDeviceForegroundServiceDriver
|
|
|
116
121
|
}
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
@Override
|
|
125
|
+
public void update(String title, String body) {
|
|
126
|
+
try {
|
|
127
|
+
BlePlxForegroundService.updateNotification(title, body);
|
|
128
|
+
} catch (RuntimeException error) {
|
|
129
|
+
if (error instanceof ForegroundServiceControlException) throw error;
|
|
130
|
+
throw new ForegroundServiceControlException(
|
|
131
|
+
"foregroundServiceNotificationUpdateFailed",
|
|
132
|
+
"Android could not update the connected-device foreground-service notification; retry while the lease is active.",
|
|
133
|
+
error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
private ForegroundServiceNotificationConfiguration configuration() {
|
|
120
138
|
try {
|
|
121
139
|
final ApplicationInfo application = context.getPackageManager().getApplicationInfo(
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package com.sfourdrinier.unifiedblemanager.background;
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.os.Build;
|
|
7
|
+
import android.util.Log;
|
|
8
|
+
|
|
9
|
+
import com.sfourdrinier.unifiedblemanager.BlePlxForegroundService;
|
|
10
|
+
|
|
11
|
+
/** Restores only the configured foreground service; it never scans or reconnects. */
|
|
12
|
+
public final class BlePlxForegroundServiceRecoveryReceiver extends BroadcastReceiver {
|
|
13
|
+
private static final String TAG = "UnifiedBleRecovery";
|
|
14
|
+
|
|
15
|
+
@Override
|
|
16
|
+
public void onReceive(Context context, Intent intent) {
|
|
17
|
+
if (intent == null || (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
|
|
18
|
+
&& !Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()))) return;
|
|
19
|
+
if (!context.getSharedPreferences("unified-ble-manager", Context.MODE_PRIVATE)
|
|
20
|
+
.getBoolean(BlePlxForegroundService.SESSION_INTENT_PREFERENCE, false)) return;
|
|
21
|
+
try {
|
|
22
|
+
final ForegroundServiceNotificationConfiguration configuration = configuration(context);
|
|
23
|
+
if (!configuration.restartWhileSessionIntentExists()) return;
|
|
24
|
+
final Intent start = BlePlxForegroundService.startIntent(context, configuration);
|
|
25
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(start);
|
|
26
|
+
else context.startService(start);
|
|
27
|
+
} catch (RuntimeException error) {
|
|
28
|
+
Log.e(TAG, "Connected-device foreground-service recovery failed", error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static ForegroundServiceNotificationConfiguration configuration(Context context) {
|
|
33
|
+
try {
|
|
34
|
+
final android.content.pm.ApplicationInfo application =
|
|
35
|
+
context.getPackageManager().getApplicationInfo(
|
|
36
|
+
context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA);
|
|
37
|
+
final java.util.Map<String, String> metadata = new java.util.HashMap<>();
|
|
38
|
+
if (application.metaData != null) {
|
|
39
|
+
for (String key : application.metaData.keySet()) {
|
|
40
|
+
final Object value = application.metaData.get(key);
|
|
41
|
+
if (value instanceof String) metadata.put(key, (String) value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ForegroundServiceNotificationConfiguration.fromMetadata(metadata);
|
|
45
|
+
} catch (android.content.pm.PackageManager.NameNotFoundException error) {
|
|
46
|
+
throw new ForegroundServiceControlException(
|
|
47
|
+
"foregroundServiceNotConfigured", "Managed foreground-service metadata is unavailable", error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -44,6 +44,15 @@ public final class ConnectedDeviceForegroundServiceLeaseRegistry {
|
|
|
44
44
|
leases.clear();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
public synchronized void update(String leaseId, String title, String body) {
|
|
48
|
+
if (!leases.contains(leaseId)) {
|
|
49
|
+
throw new ForegroundServiceControlException(
|
|
50
|
+
"invalidBackgroundLease",
|
|
51
|
+
"The connected-device background lease is stale or already released.");
|
|
52
|
+
}
|
|
53
|
+
driver.update(title, body);
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
public synchronized int activeLeaseCount() {
|
|
48
57
|
return leases.size();
|
|
49
58
|
}
|