unified-ble-manager 4.0.6 → 4.0.8

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.
Files changed (253) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/MIGRATION_4.0.md +1 -1
  3. package/README.md +25 -2
  4. package/RELEASE.md +53 -1
  5. package/SBOM.cdx.json +4 -4
  6. package/THIRD_PARTY_LICENSES.json +1 -1
  7. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/BlePlxForegroundService.java +78 -17
  8. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/AndroidConnectedDeviceForegroundServiceDriver.java +18 -0
  9. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/BlePlxForegroundServiceRecoveryReceiver.java +50 -0
  10. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceDriver.java +1 -0
  11. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceLeaseRegistry.java +9 -0
  12. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +110 -9
  13. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolControlModule.java +33 -0
  14. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/generated/NativeProtocolV2Schema.kt +23 -5
  15. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +109 -3
  16. package/android/src/main/jni/UnifiedBleProtocolJsiBinding.cpp +26 -21
  17. package/android/src/test/java/com/sfourdrinier/unifiedblemanager/BlePlxForegroundServiceLifecycleTest.kt +61 -0
  18. package/android/src/test/java/com/sfourdrinier/unifiedblemanager/background/ConnectedDeviceForegroundServiceLeaseRegistryTest.kt +21 -0
  19. package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +304 -0
  20. package/docs/BONDING.md +126 -4
  21. package/docs/ELECTRON.md +1 -1
  22. package/docs/EXPO_PLUGIN.md +30 -3
  23. package/docs/GAPS.4.0.md +3 -1
  24. package/docs/GETTING_STARTED.md +8 -6
  25. package/docs/NODE.md +1 -1
  26. package/docs/PEERS.md +23 -0
  27. package/docs/PLATFORMS.md +72 -1
  28. package/docs/UNIFIED_BLE_4.0_IMPLEMENTATION_PLAN.md +12 -2
  29. package/docs/WEB.md +1 -1
  30. package/docs/generated/PLATFORM_SUPPORT.md +1 -1
  31. package/ios/Generated/NativeProtocolV2Schema.swift +20 -2
  32. package/ios/NativeProtocol/UnifiedBleProtocolAppleExecution.mm +22 -7
  33. package/ios/NativeProtocol/UnifiedBleProtocolAppleExecutionState.hpp +6 -4
  34. package/ios/UnifiedBleProtocolControl.mm +6 -0
  35. package/lib/commonjs/NativeUnifiedBleProtocolControl.js.map +1 -1
  36. package/lib/commonjs/backend-contract/capabilities.js +11 -0
  37. package/lib/commonjs/backend-contract/capabilities.js.map +1 -1
  38. package/lib/commonjs/backend-contract/security.js +54 -0
  39. package/lib/commonjs/backend-contract/security.js.map +1 -1
  40. package/lib/commonjs/backends/bluez/bluez-backend-provider.js +2 -1
  41. package/lib/commonjs/backends/bluez/bluez-backend-provider.js.map +1 -1
  42. package/lib/commonjs/backends/bluez/bluez-backend-runtime.js +1 -0
  43. package/lib/commonjs/backends/bluez/bluez-backend-runtime.js.map +1 -1
  44. package/lib/commonjs/backends/bluez/bluez-backend.js +31 -20
  45. package/lib/commonjs/backends/bluez/bluez-backend.js.map +1 -1
  46. package/lib/commonjs/backends/bluez/bluez-pairing-generation-capability.js +92 -0
  47. package/lib/commonjs/backends/bluez/bluez-pairing-generation-capability.js.map +1 -0
  48. package/lib/commonjs/backends/bluez/bluez-pairing-generation.js +193 -0
  49. package/lib/commonjs/backends/bluez/bluez-pairing-generation.js.map +1 -0
  50. package/lib/commonjs/backends/bluez/bluez-security.js +31 -9
  51. package/lib/commonjs/backends/bluez/bluez-security.js.map +1 -1
  52. package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js +5 -1
  53. package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
  54. package/lib/commonjs/backends/reactnative/react-native-android-peer-directory.js +115 -0
  55. package/lib/commonjs/backends/reactnative/react-native-android-peer-directory.js.map +1 -0
  56. package/lib/commonjs/backends/reactnative/react-native-android-provider.js +28 -1
  57. package/lib/commonjs/backends/reactnative/react-native-android-provider.js.map +1 -1
  58. package/lib/commonjs/backends/reactnative/react-native-android-security.js +22 -4
  59. package/lib/commonjs/backends/reactnative/react-native-android-security.js.map +1 -1
  60. package/lib/commonjs/backends/winrt/winrt-security.js +16 -10
  61. package/lib/commonjs/backends/winrt/winrt-security.js.map +1 -1
  62. package/lib/commonjs/expo.js +43 -1
  63. package/lib/commonjs/expo.js.map +1 -1
  64. package/lib/commonjs/implementation-version.js +1 -1
  65. package/lib/commonjs/native-protocol/generated/native-protocol-v2-schema.js +14 -9
  66. package/lib/commonjs/native-protocol/generated/native-protocol-v2-schema.js.map +1 -1
  67. package/lib/commonjs/native-protocol/rn-android-boundary.js +66 -8
  68. package/lib/commonjs/native-protocol/rn-android-boundary.js.map +1 -1
  69. package/lib/commonjs/native-protocol/rn-apple-boundary.js +8 -2
  70. package/lib/commonjs/native-protocol/rn-apple-boundary.js.map +1 -1
  71. package/lib/commonjs/native-protocol/v2-codec.js +25 -0
  72. package/lib/commonjs/native-protocol/v2-codec.js.map +1 -1
  73. package/lib/commonjs/node-bluez.js +5 -2
  74. package/lib/commonjs/node-bluez.js.map +1 -1
  75. package/lib/commonjs/public/capabilities.js +2 -6
  76. package/lib/commonjs/public/capabilities.js.map +1 -1
  77. package/lib/commonjs/public/security.js +13 -1
  78. package/lib/commonjs/public/security.js.map +1 -1
  79. package/lib/commonjs/testing/deterministic/deterministic-security.js +8 -3
  80. package/lib/commonjs/testing/deterministic/deterministic-security.js.map +1 -1
  81. package/lib/module/NativeUnifiedBleProtocolControl.js.map +1 -1
  82. package/lib/module/backend-contract/capabilities.js +11 -0
  83. package/lib/module/backend-contract/capabilities.js.map +1 -1
  84. package/lib/module/backend-contract/security.js +53 -1
  85. package/lib/module/backend-contract/security.js.map +1 -1
  86. package/lib/module/backends/bluez/bluez-backend-provider.js +2 -1
  87. package/lib/module/backends/bluez/bluez-backend-provider.js.map +1 -1
  88. package/lib/module/backends/bluez/bluez-backend-runtime.js +1 -0
  89. package/lib/module/backends/bluez/bluez-backend-runtime.js.map +1 -1
  90. package/lib/module/backends/bluez/bluez-backend.js +31 -20
  91. package/lib/module/backends/bluez/bluez-backend.js.map +1 -1
  92. package/lib/module/backends/bluez/bluez-pairing-generation-capability.js +87 -0
  93. package/lib/module/backends/bluez/bluez-pairing-generation-capability.js.map +1 -0
  94. package/lib/module/backends/bluez/bluez-pairing-generation.js +188 -0
  95. package/lib/module/backends/bluez/bluez-pairing-generation.js.map +1 -0
  96. package/lib/module/backends/bluez/bluez-security.js +31 -9
  97. package/lib/module/backends/bluez/bluez-security.js.map +1 -1
  98. package/lib/module/backends/corebluetooth/corebluetooth-backend.js +5 -1
  99. package/lib/module/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
  100. package/lib/module/backends/reactnative/react-native-android-peer-directory.js +110 -0
  101. package/lib/module/backends/reactnative/react-native-android-peer-directory.js.map +1 -0
  102. package/lib/module/backends/reactnative/react-native-android-provider.js +28 -1
  103. package/lib/module/backends/reactnative/react-native-android-provider.js.map +1 -1
  104. package/lib/module/backends/reactnative/react-native-android-security.js +22 -4
  105. package/lib/module/backends/reactnative/react-native-android-security.js.map +1 -1
  106. package/lib/module/backends/winrt/winrt-security.js +16 -10
  107. package/lib/module/backends/winrt/winrt-security.js.map +1 -1
  108. package/lib/module/expo.js +43 -1
  109. package/lib/module/expo.js.map +1 -1
  110. package/lib/module/implementation-version.js +1 -1
  111. package/lib/module/native-protocol/generated/native-protocol-v2-schema.js +13 -8
  112. package/lib/module/native-protocol/generated/native-protocol-v2-schema.js.map +1 -1
  113. package/lib/module/native-protocol/rn-android-boundary.js +66 -8
  114. package/lib/module/native-protocol/rn-android-boundary.js.map +1 -1
  115. package/lib/module/native-protocol/rn-apple-boundary.js +8 -2
  116. package/lib/module/native-protocol/rn-apple-boundary.js.map +1 -1
  117. package/lib/module/native-protocol/v2-codec.js +25 -0
  118. package/lib/module/native-protocol/v2-codec.js.map +1 -1
  119. package/lib/module/node-bluez.js +5 -2
  120. package/lib/module/node-bluez.js.map +1 -1
  121. package/lib/module/public/capabilities.js +2 -6
  122. package/lib/module/public/capabilities.js.map +1 -1
  123. package/lib/module/public/security.js +13 -1
  124. package/lib/module/public/security.js.map +1 -1
  125. package/lib/module/testing/deterministic/deterministic-security.js +8 -3
  126. package/lib/module/testing/deterministic/deterministic-security.js.map +1 -1
  127. package/lib/typescript/commonjs/src/NativeUnifiedBleProtocolControl.d.ts +6 -0
  128. package/lib/typescript/commonjs/src/NativeUnifiedBleProtocolControl.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/src/backend-contract/capabilities.d.ts +11 -0
  130. package/lib/typescript/commonjs/src/backend-contract/capabilities.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/src/backend-contract/security.d.ts +38 -1
  132. package/lib/typescript/commonjs/src/backend-contract/security.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +13 -1
  134. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-runtime.d.ts +8 -0
  136. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-runtime.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend.d.ts +5 -1
  138. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/src/backends/bluez/bluez-pairing-generation-capability.d.ts +16 -0
  140. package/lib/typescript/commonjs/src/backends/bluez/bluez-pairing-generation-capability.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/src/backends/bluez/bluez-pairing-generation.d.ts +41 -0
  142. package/lib/typescript/commonjs/src/backends/bluez/bluez-pairing-generation.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/src/backends/bluez/bluez-security.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-boundary.d.ts +6 -1
  146. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-boundary.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  148. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-peer-directory.d.ts +28 -0
  149. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-peer-directory.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +3 -2
  151. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-security.d.ts +5 -0
  153. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-security.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  155. package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
  156. package/lib/typescript/commonjs/src/backends/winrt/winrt-security.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/src/expo.d.ts +5 -0
  158. package/lib/typescript/commonjs/src/expo.d.ts.map +1 -1
  159. package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
  160. package/lib/typescript/commonjs/src/native-protocol/generated/native-protocol-v2-schema.d.ts +5 -1
  161. package/lib/typescript/commonjs/src/native-protocol/generated/native-protocol-v2-schema.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts +13 -2
  163. package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/src/native-protocol/rn-apple-boundary.d.ts +5 -1
  165. package/lib/typescript/commonjs/src/native-protocol/rn-apple-boundary.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/src/node-bluez.d.ts +14 -0
  167. package/lib/typescript/commonjs/src/node-bluez.d.ts.map +1 -1
  168. package/lib/typescript/commonjs/src/public/capabilities.d.ts +1 -0
  169. package/lib/typescript/commonjs/src/public/capabilities.d.ts.map +1 -1
  170. package/lib/typescript/commonjs/src/testing/deterministic/deterministic-security.d.ts.map +1 -1
  171. package/lib/typescript/module/src/NativeUnifiedBleProtocolControl.d.ts +6 -0
  172. package/lib/typescript/module/src/NativeUnifiedBleProtocolControl.d.ts.map +1 -1
  173. package/lib/typescript/module/src/backend-contract/capabilities.d.ts +11 -0
  174. package/lib/typescript/module/src/backend-contract/capabilities.d.ts.map +1 -1
  175. package/lib/typescript/module/src/backend-contract/security.d.ts +38 -1
  176. package/lib/typescript/module/src/backend-contract/security.d.ts.map +1 -1
  177. package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +13 -1
  178. package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts.map +1 -1
  179. package/lib/typescript/module/src/backends/bluez/bluez-backend-runtime.d.ts +8 -0
  180. package/lib/typescript/module/src/backends/bluez/bluez-backend-runtime.d.ts.map +1 -1
  181. package/lib/typescript/module/src/backends/bluez/bluez-backend.d.ts +5 -1
  182. package/lib/typescript/module/src/backends/bluez/bluez-backend.d.ts.map +1 -1
  183. package/lib/typescript/module/src/backends/bluez/bluez-pairing-generation-capability.d.ts +16 -0
  184. package/lib/typescript/module/src/backends/bluez/bluez-pairing-generation-capability.d.ts.map +1 -0
  185. package/lib/typescript/module/src/backends/bluez/bluez-pairing-generation.d.ts +41 -0
  186. package/lib/typescript/module/src/backends/bluez/bluez-pairing-generation.d.ts.map +1 -0
  187. package/lib/typescript/module/src/backends/bluez/bluez-security.d.ts.map +1 -1
  188. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
  189. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-boundary.d.ts +6 -1
  190. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-boundary.d.ts.map +1 -1
  191. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  192. package/lib/typescript/module/src/backends/reactnative/react-native-android-peer-directory.d.ts +28 -0
  193. package/lib/typescript/module/src/backends/reactnative/react-native-android-peer-directory.d.ts.map +1 -0
  194. package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +3 -2
  195. package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts.map +1 -1
  196. package/lib/typescript/module/src/backends/reactnative/react-native-android-security.d.ts +5 -0
  197. package/lib/typescript/module/src/backends/reactnative/react-native-android-security.d.ts.map +1 -1
  198. package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  199. package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
  200. package/lib/typescript/module/src/backends/winrt/winrt-security.d.ts.map +1 -1
  201. package/lib/typescript/module/src/expo.d.ts +5 -0
  202. package/lib/typescript/module/src/expo.d.ts.map +1 -1
  203. package/lib/typescript/module/src/implementation-version.d.ts +1 -1
  204. package/lib/typescript/module/src/native-protocol/generated/native-protocol-v2-schema.d.ts +5 -1
  205. package/lib/typescript/module/src/native-protocol/generated/native-protocol-v2-schema.d.ts.map +1 -1
  206. package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts +13 -2
  207. package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
  208. package/lib/typescript/module/src/native-protocol/rn-apple-boundary.d.ts +5 -1
  209. package/lib/typescript/module/src/native-protocol/rn-apple-boundary.d.ts.map +1 -1
  210. package/lib/typescript/module/src/node-bluez.d.ts +14 -0
  211. package/lib/typescript/module/src/node-bluez.d.ts.map +1 -1
  212. package/lib/typescript/module/src/public/capabilities.d.ts +1 -0
  213. package/lib/typescript/module/src/public/capabilities.d.ts.map +1 -1
  214. package/lib/typescript/module/src/testing/deterministic/deterministic-security.d.ts.map +1 -1
  215. package/native/PREBUILDS.json +5 -5
  216. package/native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node +0 -0
  217. package/native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node +0 -0
  218. package/native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node +0 -0
  219. package/native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node +0 -0
  220. package/native/protocol/generated/NativeProtocolV2Schema.hpp +34 -9
  221. package/native/protocol/schema/native-protocol-v2-abi.json +18 -6
  222. package/native/protocol/schema/native-protocol-v2.json +21 -6
  223. package/native/protocol/src/NativeProtocolV2Codec.cpp +11 -3
  224. package/native/tauri/src/btleplug_dispatcher.rs +171 -16
  225. package/native/tauri/src/capabilities.rs +2 -1
  226. package/package.json +1 -1
  227. package/plugin/build/withBLEAndroidForegroundService.d.ts +1 -0
  228. package/plugin/build/withBLEAndroidForegroundService.js +36 -2
  229. package/src/NativeUnifiedBleProtocolControl.ts +7 -0
  230. package/src/backend-contract/capabilities.ts +11 -0
  231. package/src/backend-contract/security.ts +51 -1
  232. package/src/backends/bluez/bluez-backend-provider.ts +14 -1
  233. package/src/backends/bluez/bluez-backend-runtime.ts +9 -0
  234. package/src/backends/bluez/bluez-backend.ts +45 -27
  235. package/src/backends/bluez/bluez-pairing-generation-capability.ts +100 -0
  236. package/src/backends/bluez/bluez-pairing-generation.ts +220 -0
  237. package/src/backends/bluez/bluez-security.ts +87 -45
  238. package/src/backends/corebluetooth/corebluetooth-backend.ts +7 -3
  239. package/src/backends/corebluetooth/corebluetooth-boundary.ts +4 -1
  240. package/src/backends/reactnative/react-native-android-peer-directory.ts +153 -0
  241. package/src/backends/reactnative/react-native-android-provider.ts +37 -0
  242. package/src/backends/reactnative/react-native-android-security.ts +20 -2
  243. package/src/backends/winrt/winrt-security.ts +14 -3
  244. package/src/expo.ts +78 -5
  245. package/src/implementation-version.ts +1 -1
  246. package/src/native-protocol/generated/native-protocol-v2-schema.ts +27 -8
  247. package/src/native-protocol/rn-android-boundary.ts +92 -4
  248. package/src/native-protocol/rn-apple-boundary.ts +10 -2
  249. package/src/native-protocol/v2-codec.ts +27 -0
  250. package/src/node-bluez.ts +21 -3
  251. package/src/public/capabilities.ts +3 -6
  252. package/src/public/security.ts +10 -1
  253. package/src/testing/deterministic/deterministic-security.ts +8 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
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.8] - 2026-08-29
10
+
11
+ ### Fixes
12
+
13
+ - **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.
14
+
15
+ - **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).
16
+
17
+ - **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).
18
+
19
+ - **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).
20
+
21
+ - **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.
22
+
23
+ - **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.
24
+
25
+ - **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.
26
+
27
+ 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.
28
+
29
+ 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).
30
+
31
+ ## [4.0.7] - 2026-08-26
32
+
33
+ ### Changed behaviour
34
+
35
+ - `cancelPairing()` reports what the cancellation **achieved**, not what it requested, and can no longer contradict the pairing it cancelled. Every backend answered `'cancelled'` unconditionally the moment the cancel was dispatched, so a cancellation that lost the race told the caller no bond exists while one did — and a caller who believes that never looks again. It now reads the in-flight pairing's own result rather than forming a second opinion, which is what makes the two calls incapable of disagreeing: there is one source of truth and the cancellation reads it. `SecurityCancelPairingResult` gains the words to say so — `'paired'` when the bond completed before the cancellation arrived, and `'rejected'` (carrying the peer's reason) when the peer refused, because claiming credit for stopping something that stopped itself is the same substitution with the arrow reversed. A pairing that *fails* is not given an invented outcome: `cancelPairing()` rejects with the error the pairing rejected with. `'paired'` deliberately matches `SecurityPairResult`'s `'paired'` — a bond exists as a result of this operation — and is not `'already-paired'`, which in that type means the peer was bonded *before* the call; one word, one meaning, in both types. Applies to BlueZ, Android, WinRT **and** the deterministic `/testing` backend, since a mock that answers differently from every real radio lets a consumer's suite pass against a contract no device honours. WinRT additionally short-circuited *upstream* of the shared mapper — when its dispatcher reported the operation already terminal, which is exactly the lost race, it answered `'not-pairing'`, contradicting its own `pair()` and making that word mean both "there was nothing to stop" and "it was already over" (#159).
36
+
37
+ ### Additions
38
+
39
+ - **`PairOptions.secureConnections` can now actually select the LE pairing generation on BlueZ — behind a host-supplied privileged operation.** Some peripherals accept only LE Legacy and terminate the link on a Secure Connections pairing request; 4.0.6 gave the contract a way to say so but no backend that could honour it. `org.bluez.Adapter1` (BlueZ 5.85) exposes no Secure Connections property and `Device1.Pair` takes no parameters — the setting lives behind the kernel management socket's Set Secure Connections command, which requires **`CAP_NET_ADMIN`**.
40
+
41
+ This package never acquires that privilege: it opens no management socket, shells out to nothing, and does not assume it is root. A library that silently escalates hands an application capabilities its author did not choose and cannot audit. The **host** supplies the operation via `BluezBackendProviderOptions.pairingGeneration`, which makes the escalation visible in the application that opted into it. Omit it and `'require'`/`'disallow'` keep failing closed exactly as in 4.0.6 — the default posture is unchanged.
42
+
43
+ Three properties of the kernel setting a host must accept, documented in `docs/BONDING.md` rather than discovered: it is **adapter-wide**, not per-pairing, so every pairing on that controller uses the selected generation while it is held; it **outlives the process**, because the kernel keeps it until something sets it back; and a **failed restore never changes the pairing's outcome** — a bond that was created is reported as created, with the restore failure reported separately, because leaving a controller in LE Legacy and telling a caller they are not bonded are both serious and are different facts. Concurrent *directed* pairings on one adapter are serialised so they cannot corrupt each other's restore value; an undirected (`'prefer'`) pairing does not take that lock and uses whichever generation is held, which `docs/BONDING.md` states beside the option.
44
+
45
+ `security:pairing-generation` joins the capability catalog and is reported by the instantiated backend at runtime: `unsupported` with the platform reason when no operation was supplied, `limited` when one was — so two BlueZ backends on one machine legitimately answer differently. Evidence is deterministic tests only; this is **not** physical-radio proof, and the label says so (#144).
46
+
47
+ ### Fixes
48
+
49
+ - A Tauri connect that succeeds physically but is then denied admission no longer strands the peer. Both compensation branches — the caller vanished, and the caller's lease went stale — removed the peer reservation *before* attempting the disconnect and then discarded its result with `.ok()`. If that disconnect failed or hung while the peripheral stayed connected, the result was a connected peer with no owner and no handle: nothing could reach it, nothing could retry it, and the caller was told only that admission was denied. Compensation now follows the rule the explicit disconnect already follows — a bounded wait, a fresh state reading, and a D-Bus error naming a vanished device object read as evidence of release rather than as a failed question — and surrenders the reservation only when the platform proves the link is down. Genuine indeterminacy keeps it so a retry can reclaim the peer, and the admission error carries what compensation could not undo, which is the difference between "try again" and "a peer is stranded" (#146).
50
+
51
+ ### Known limitations
52
+
53
+ - `pair()`'s abort path still reports `'cancelled'` without knowing whether the daemon bonded anyway. Learning the truth means waiting for the radio, and waiting risks a hang that a wedged daemon would inflict on the caller — which the suite explicitly forbids. Resolving it needs a vocabulary that can express "cancellation requested, bond state not yet known"; `state()` and `watch()` remain authoritative meanwhile (#157).
54
+
5
55
  ## [4.0.6] - 2026-08-26
6
56
 
7
57
  The first release cut against a live peripheral on Android as well as Linux. Most of it is defects that only real hardware surfaced, several of them boundaries that discarded evidence and so presented a specific fault as silence. Does not retag `v4.0.5`.
@@ -38,7 +88,7 @@ The first release cut against a live peripheral on Android as well as Linux. Mos
38
88
 
39
89
  ## [4.0.5] - 2026-08-25
40
90
 
41
- First release cut against a live peripheral. Every fix below came from driving a real CGM 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`.
91
+ 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`.
42
92
 
43
93
  ### Fixes
44
94
 
package/MIGRATION_4.0.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Migrating from react-native-ble-plx
4
4
 
5
- The current package is `4.0.6`. 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.
5
+ This source targets `4.0.8`. 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,20 @@
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 (`4.0.6` on `latest`). The root import does not pick a radio. Package SemVer and backend support labels are independent: each radio backend stays Experimental until live-radio evidence says otherwise. See [`docs/PLATFORMS.md`](docs/PLATFORMS.md).
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 preparing `4.0.8`; that version is installable only after it
16
+ appears in the npm registry.
17
+
18
+ > **4.0 development note:** The 4.0 line is the real-application proving ground
19
+ > for a simpler, stronger 4.1. Develop carefully against it: pin the version you
20
+ > validate, read the changelog when upgrading, inspect capability limitations,
21
+ > and report real-device behavior. Missing hardware evidence remains visible;
22
+ > it does not make an implemented operation unusable.
10
23
 
11
24
  > Sponsored by [Imagi Explain](https://imagiexplain.com) — researched, narrated whiteboard explainers from a prompt, a PDF, or your notes.
12
25
 
@@ -85,6 +98,12 @@ const manager = await createReactNativeBleManager({
85
98
 
86
99
  On Android 12+ the app must request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` itself. The library does not call `PermissionsAndroid`.
87
100
 
101
+ On Android, `manager.peers.bonded()` lists paired system peers and
102
+ `manager.peers.resolve(reference)` rechecks a saved reference before
103
+ `manager.connect(peer, { intent: 'when-available' })`; paired does not mean
104
+ reachable. See [`docs/PEERS.md`](docs/PEERS.md) for the persistence and error
105
+ semantics.
106
+
88
107
  ### Expo plugin
89
108
 
90
109
  Use an Expo development build, never Expo Go. Plugin options live in
@@ -209,7 +228,11 @@ Web Bluetooth replaces the scan with `ble.choose(...)` from a user gesture. Taur
209
228
 
210
229
  Controls report the truth of the instantiated host backend, including
211
230
  `supported`, `limited`, `unavailable`, or `unsupported`; host family alone is
212
- not evidence of support. Readiness is unsupported until the backend advertises
231
+ not evidence of support. `manager.capabilities.supports(id)` answers whether
232
+ the operation is implemented and invocable, so it returns `true` for both
233
+ `supported` and `limited`. Use `manager.capabilities.get(id)` when application
234
+ policy needs to distinguish full qualification from a named limitation.
235
+ Readiness is unsupported until the backend advertises
213
236
  the readiness capability, and a readiness event does not prove a later payload
214
237
  was retained.
215
238
 
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`, `4.0.1`, `4.0.2`, `4.0.3`, `4.0.4`, and `4.0.5` are immutable. `4.0.6` is the current train head.
84
+ once tagged. Stable `4.0.0` through `4.0.7` are immutable. `4.0.8` is the current train head.
85
85
 
86
86
  ```sh
87
87
  release_candidate=4.0.0-rc.N
@@ -133,6 +133,58 @@ 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.8
137
+
138
+ The `v4.0.8` tag must identify the exact current `main` commit after canonical
139
+ CI passes. Do not tag this feature branch or `release/4.0.8` 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.8"
148
+ git diff --exit-code
149
+ git diff --cached --exit-code
150
+
151
+ git tag -a v4.0.8 -m "v4.0.8"
152
+ git push origin v4.0.8
153
+ ```
154
+
155
+ Before tagging, confirm release-note extraction finds `## [4.0.8]`.
156
+
157
+ ## Releasing 4.0.7
158
+
159
+ Same shape as 4.0.6. The release workflow verifies that the tag points at the
160
+ exact current `main` commit before publication; do not create it from a side
161
+ branch or an older commit. Do not retag any immutable version.
162
+
163
+ ```sh
164
+ git fetch origin --tags
165
+ git checkout main
166
+ git pull --ff-only origin main
167
+
168
+ test "$(git branch --show-current)" = "main"
169
+ test "$(node -p "require('./package.json').version")" = "4.0.7"
170
+ git diff --exit-code
171
+ git diff --cached --exit-code
172
+
173
+ git tag -a v4.0.7 -m "v4.0.7"
174
+ git push origin v4.0.7
175
+ ```
176
+
177
+ Do not push another commit to `main` between the final verification and the tag
178
+ push.
179
+
180
+ Before tagging, confirm the release-notes extraction finds the entry — the
181
+ workflow's awk matches `^## \[4.0.7\]`, and a heading left as `[Unreleased]`
182
+ publishes a stub instead of the changelog:
183
+
184
+ ```sh
185
+ awk -v ver=4.0.7 '$0 ~ ("^## \\[" ver "\\]") {p=1;next} p && $0 ~ /^## \[/ {exit} p {print}' CHANGELOG.md
186
+ ```
187
+
136
188
  ## Releasing 4.0.6
137
189
 
138
190
  The source version is prepared on `main` before the tag. The release workflow verifies that every initial release tag points at the exact current `main` commit before publication; do not create that tag from a side branch or an older commit. Do not retag immutable `v4.0.0`, `v4.0.1`, `v4.0.2`, or `v4.0.3`.
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.6",
9
+ "bom-ref": "pkg:npm/unified-ble-manager@4.0.8",
10
10
  "name": "unified-ble-manager",
11
- "version": "4.0.6",
11
+ "version": "4.0.8",
12
12
  "licenses": [
13
13
  {
14
14
  "expression": "Apache-2.0"
15
15
  }
16
16
  ],
17
- "purl": "pkg:npm/unified-ble-manager@4.0.6"
17
+ "purl": "pkg:npm/unified-ble-manager@4.0.8"
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.6",
563
+ "ref": "pkg:npm/unified-ble-manager@4.0.8",
564
564
  "dependsOn": [
565
565
  "pkg:npm/%40babel/runtime@7.29.7",
566
566
  "pkg:npm/%40tauri-apps/api@2.11.1",
@@ -4,7 +4,7 @@
4
4
  "schemaVersion": "1.0.0",
5
5
  "package": {
6
6
  "name": "unified-ble-manager",
7
- "version": "4.0.6"
7
+ "version": "4.0.8"
8
8
  },
9
9
  "source": {
10
10
  "method": "pnpm-lock production graph with installed-manifest license audit",
@@ -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
- final String channelId = configuration.getChannelId();
79
- final String channelName = configuration.getChannelName();
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
- ensureChannel(channelId, channelName);
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
+ }
@@ -3,4 +3,5 @@ package com.sfourdrinier.unifiedblemanager.background;
3
3
  public interface ConnectedDeviceForegroundServiceDriver {
4
4
  void start(String reason);
5
5
  void stop();
6
+ void update(String title, String body);
6
7
  }
@@ -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
  }