unified-ble-manager 4.0.13 → 4.0.16
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 +41 -0
- package/MIGRATION_4.0.md +1 -1
- package/README.md +1 -1
- package/RELEASE.md +42 -8
- package/SBOM.cdx.json +5 -5
- package/THIRD_PARTY_LICENSES.json +2 -2
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +79 -7
- package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +67 -3
- package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +170 -1
- package/docs/ELECTRON.md +1 -1
- package/docs/EXPO_PLUGIN.md +1 -1
- package/docs/GAPS.4.0.md +1 -1
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/NODE.md +1 -1
- package/docs/WEB.md +1 -1
- package/docs/generated/PLATFORM_SUPPORT.md +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
- package/lib/commonjs/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js +41 -6
- package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-late-connect-cleanup.js +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-late-connect-cleanup.js.map +1 -1
- package/lib/commonjs/backends/corebluetooth/corebluetooth-operation-lifecycle.js +17 -3
- package/lib/commonjs/backends/corebluetooth/corebluetooth-operation-lifecycle.js.map +1 -1
- package/lib/commonjs/backends/winrt/winrt-backend.js +52 -10
- package/lib/commonjs/backends/winrt/winrt-backend.js.map +1 -1
- package/lib/commonjs/backends/winrt/winrt-handles.js +1 -1
- package/lib/commonjs/backends/winrt/winrt-handles.js.map +1 -1
- package/lib/commonjs/implementation-version.js +1 -1
- package/lib/commonjs/native-protocol/rn-android-boundary.js +55 -3
- package/lib/commonjs/native-protocol/rn-android-boundary.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
- package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js +41 -6
- package/lib/module/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-late-connect-cleanup.js +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-late-connect-cleanup.js.map +1 -1
- package/lib/module/backends/corebluetooth/corebluetooth-operation-lifecycle.js +17 -3
- package/lib/module/backends/corebluetooth/corebluetooth-operation-lifecycle.js.map +1 -1
- package/lib/module/backends/winrt/winrt-backend.js +52 -10
- package/lib/module/backends/winrt/winrt-backend.js.map +1 -1
- package/lib/module/backends/winrt/winrt-handles.js +1 -1
- package/lib/module/backends/winrt/winrt-handles.js.map +1 -1
- package/lib/module/implementation-version.js +1 -1
- package/lib/module/native-protocol/rn-android-boundary.js +55 -3
- package/lib/module/native-protocol/rn-android-boundary.js.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-adapter-loss-cleanup.d.ts +1 -0
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-operation-lifecycle.d.ts +1 -1
- package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-operation-lifecycle.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +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-backend.d.ts +3 -0
- package/lib/typescript/commonjs/src/backends/winrt/winrt-backend.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
- package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts +8 -0
- package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.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-adapter-loss-cleanup.d.ts +1 -0
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-operation-lifecycle.d.ts +1 -1
- package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-operation-lifecycle.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +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-backend.d.ts +3 -0
- package/lib/typescript/module/src/backends/winrt/winrt-backend.d.ts.map +1 -1
- package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
- package/lib/typescript/module/src/implementation-version.d.ts +1 -1
- package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts +8 -0
- package/lib/typescript/module/src/native-protocol/rn-android-boundary.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/package.json +1 -1
- package/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts +4 -0
- package/src/backends/corebluetooth/corebluetooth-backend.ts +45 -6
- package/src/backends/corebluetooth/corebluetooth-late-connect-cleanup.ts +1 -1
- package/src/backends/corebluetooth/corebluetooth-operation-lifecycle.ts +18 -3
- package/src/backends/winrt/winrt-backend.ts +57 -10
- package/src/backends/winrt/winrt-handles.ts +1 -1
- package/src/implementation-version.ts +1 -1
- package/src/native-protocol/rn-android-boundary.ts +70 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,47 @@ All notable changes to `unified-ble-manager` are documented here.
|
|
|
6
6
|
|
|
7
7
|
No changes yet.
|
|
8
8
|
|
|
9
|
+
## [4.0.16] - 2026-09-03
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- Normalize an Android CCCD operation that terminates with GATT status 19 as
|
|
14
|
+
`connection.lost` even when it arrives before the native `connectionLost`
|
|
15
|
+
event. Other CCCD failures remain `platform.failure` and retain their native
|
|
16
|
+
GATT status.
|
|
17
|
+
- Cancel a dispatched React Native Android `when-available` connection when
|
|
18
|
+
its caller aborts, releasing the native pending connection so a later
|
|
19
|
+
same-peer retry is admitted without affecting distinct-peer connects.
|
|
20
|
+
|
|
21
|
+
## [4.0.15] - 2026-09-02
|
|
22
|
+
|
|
23
|
+
### Fixes
|
|
24
|
+
|
|
25
|
+
- Silence exactly one expected late React Native Android terminal when a peer
|
|
26
|
+
is lost while an operation such as GATT discovery is still settling. The
|
|
27
|
+
boundary records only operations it explicitly rejected for that peer,
|
|
28
|
+
bounds and lifecycle-scopes those records, and continues to report unknown
|
|
29
|
+
or duplicate terminals as protocol diagnostics.
|
|
30
|
+
|
|
31
|
+
## [4.0.14] - 2026-09-02
|
|
32
|
+
|
|
33
|
+
### Fixes
|
|
34
|
+
|
|
35
|
+
- Release Android GATT ownership immediately when Bluetooth powers off or
|
|
36
|
+
resets instead of waiting for a `STATE_DISCONNECTED` callback that Android
|
|
37
|
+
may never deliver. Failed native closes remain retained and retry when the
|
|
38
|
+
adapter returns, protocol routes are invalidated before adapter-state
|
|
39
|
+
publication, and an ordinary disconnect timeout now also publishes the
|
|
40
|
+
missing terminal state before cleanup. A cleanup unsubscribe after adapter
|
|
41
|
+
loss is idempotent when native ownership is already gone.
|
|
42
|
+
- Terminalize adapter-loss connections exactly once with the typed `adapter`
|
|
43
|
+
reason when a native CoreBluetooth-style or WinRT disconnect completes
|
|
44
|
+
without a platform loss callback. Delayed native callbacks are ignored, and
|
|
45
|
+
WinRT connection paths retain the attachment that created them.
|
|
46
|
+
- Treat an operation that is still settling after adapter loss as expected
|
|
47
|
+
quarantine rather than a cleanup error. Cleanup still waits and retries, but
|
|
48
|
+
only an actual native release failure produces an error diagnostic.
|
|
49
|
+
|
|
9
50
|
## [4.0.13] - 2026-09-01
|
|
10
51
|
|
|
11
52
|
### Fixes
|
package/MIGRATION_4.0.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Migrating from react-native-ble-plx
|
|
4
4
|
|
|
5
|
-
This source targets `4.0.
|
|
5
|
+
This source targets `4.0.16`. 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
|
@@ -18,7 +18,7 @@ root import does not pick a radio. Package SemVer and backend support labels are
|
|
|
18
18
|
independent: each radio backend keeps its evidence-derived label. See
|
|
19
19
|
[`docs/PLATFORMS.md`](docs/PLATFORMS.md).
|
|
20
20
|
|
|
21
|
-
This source tree is versioned `4.0.
|
|
21
|
+
This source tree is versioned `4.0.16`. Install the exact version shown in the npm
|
|
22
22
|
registry. During release preparation, the version in `package.json` can be ahead
|
|
23
23
|
of npm until the matching tag-driven workflow publishes it; the registry and
|
|
24
24
|
GitHub release remain authoritative.
|
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` through `4.0.
|
|
84
|
+
once tagged. Stable `4.0.0` through `4.0.15` are immutable. `4.0.16` is the current train head.
|
|
85
85
|
|
|
86
86
|
```sh
|
|
87
87
|
release_candidate=4.0.0-rc.N
|
|
@@ -133,9 +133,30 @@ 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.
|
|
136
|
+
## Releasing 4.0.16
|
|
137
|
+
|
|
138
|
+
The `v4.0.16` tag must identify the exact current `main` commit after canonical
|
|
139
|
+
CI passes. Do not tag the 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.16"
|
|
148
|
+
git diff --exit-code
|
|
149
|
+
git diff --cached --exit-code
|
|
150
|
+
|
|
151
|
+
git tag -a v4.0.16 -m "v4.0.16"
|
|
152
|
+
git push origin v4.0.16
|
|
153
|
+
```
|
|
137
154
|
|
|
138
|
-
|
|
155
|
+
Before tagging, confirm release-note extraction finds `## [4.0.16]`.
|
|
156
|
+
|
|
157
|
+
## Releasing 4.0.15
|
|
158
|
+
|
|
159
|
+
The `v4.0.15` tag must identify the exact current `main` commit after canonical
|
|
139
160
|
CI passes. Do not tag this feature branch directly.
|
|
140
161
|
|
|
141
162
|
```sh
|
|
@@ -144,15 +165,28 @@ git checkout main
|
|
|
144
165
|
git pull --ff-only origin main
|
|
145
166
|
|
|
146
167
|
test "$(git branch --show-current)" = "main"
|
|
147
|
-
test "$(node -p "require('./package.json').version")" = "4.0.
|
|
168
|
+
test "$(node -p "require('./package.json').version")" = "4.0.15"
|
|
148
169
|
git diff --exit-code
|
|
149
170
|
git diff --cached --exit-code
|
|
150
171
|
|
|
151
|
-
git tag -a v4.0.
|
|
152
|
-
git push origin v4.0.
|
|
172
|
+
git tag -a v4.0.15 -m "v4.0.15"
|
|
173
|
+
git push origin v4.0.15
|
|
153
174
|
```
|
|
154
175
|
|
|
155
|
-
Before tagging, confirm release-note extraction finds `## [4.0.
|
|
176
|
+
Before tagging, confirm release-note extraction finds `## [4.0.15]`.
|
|
177
|
+
|
|
178
|
+
## Releasing 4.0.14
|
|
179
|
+
|
|
180
|
+
The `v4.0.14` tag is immutable history, but its publish workflow was cancelled
|
|
181
|
+
before the npm version check and publish steps. Do not recreate or move it.
|
|
182
|
+
|
|
183
|
+
## Releasing 4.0.13
|
|
184
|
+
|
|
185
|
+
The `v4.0.13` tag is immutable published history. Do not recreate or move it.
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
git tag -a v4.0.13 -m "v4.0.13"
|
|
189
|
+
```
|
|
156
190
|
|
|
157
191
|
## Releasing 4.0.12
|
|
158
192
|
|
|
@@ -327,7 +361,7 @@ a green publish job and a package a consumer can actually install are not the
|
|
|
327
361
|
same claim.
|
|
328
362
|
|
|
329
363
|
```sh
|
|
330
|
-
version=4.0.
|
|
364
|
+
version=4.0.16
|
|
331
365
|
|
|
332
366
|
npm view "unified-ble-manager@$version" version
|
|
333
367
|
npm view unified-ble-manager dist-tags --json
|
package/SBOM.cdx.json
CHANGED
|
@@ -6,20 +6,20 @@
|
|
|
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.16",
|
|
10
10
|
"name": "unified-ble-manager",
|
|
11
|
-
"version": "4.0.
|
|
11
|
+
"version": "4.0.16",
|
|
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.16"
|
|
18
18
|
},
|
|
19
19
|
"properties": [
|
|
20
20
|
{
|
|
21
21
|
"name": "unified-ble-manager:pnpm-lock-sha256",
|
|
22
|
-
"value": "
|
|
22
|
+
"value": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "unified-ble-manager:dependency-scope",
|
|
@@ -537,7 +537,7 @@
|
|
|
537
537
|
"dependsOn": []
|
|
538
538
|
},
|
|
539
539
|
{
|
|
540
|
-
"ref": "pkg:npm/unified-ble-manager@4.0.
|
|
540
|
+
"ref": "pkg:npm/unified-ble-manager@4.0.16",
|
|
541
541
|
"dependsOn": [
|
|
542
542
|
"pkg:npm/%40babel/runtime@7.29.7",
|
|
543
543
|
"pkg:npm/node-addon-api@8.9.0",
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"schemaVersion": "1.0.0",
|
|
5
5
|
"package": {
|
|
6
6
|
"name": "unified-ble-manager",
|
|
7
|
-
"version": "4.0.
|
|
7
|
+
"version": "4.0.16"
|
|
8
8
|
},
|
|
9
9
|
"source": {
|
|
10
10
|
"method": "pnpm-lock production graph with installed-manifest license audit",
|
|
11
11
|
"lockfile": "pnpm-lock.yaml",
|
|
12
|
-
"lockfileSha256": "
|
|
12
|
+
"lockfileSha256": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
|
|
13
13
|
},
|
|
14
14
|
"reviewedOverrides": [
|
|
15
15
|
{
|
|
@@ -11,6 +11,7 @@ import com.sfourdrinier.unifiedblemanager.protocol.generated.ConnectionIntents
|
|
|
11
11
|
import com.sfourdrinier.unifiedblemanager.protocol.generated.RecordKind
|
|
12
12
|
import com.sfourdrinier.unifiedblemanager.radio.OwnedAndroidGattRadio
|
|
13
13
|
import com.sfourdrinier.unifiedblemanager.radio.OwnedRadioTeardownFailure
|
|
14
|
+
import com.sfourdrinier.unifiedblemanager.radio.AndroidGattOperationFailure
|
|
14
15
|
import com.sfourdrinier.unifiedblemanager.radio.BondedPeerSnapshot
|
|
15
16
|
import com.sfourdrinier.unifiedblemanager.radio.nextUuidOccurrence
|
|
16
17
|
import java.util.UUID
|
|
@@ -38,7 +39,8 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
38
39
|
private var attachmentRecord: ProtocolWireRecord? = null
|
|
39
40
|
|
|
40
41
|
init {
|
|
41
|
-
radio.onAdapterState = {
|
|
42
|
+
radio.onAdapterState = { adapterState ->
|
|
43
|
+
clearGattProtocolOwnershipForAdapterLoss(adapterState)
|
|
42
44
|
emitCurrentAdapterState()
|
|
43
45
|
}
|
|
44
46
|
radio.onCleanupFailure = { failure ->
|
|
@@ -140,6 +142,33 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
private fun clearGattProtocolOwnershipForAdapterLoss(adapterState: String) {
|
|
146
|
+
val state = radio.currentProtocolAdapterState()
|
|
147
|
+
if (state.availability == "available" && state.authorization == "granted" && state.power == "on") return
|
|
148
|
+
val failure = when (adapterState) {
|
|
149
|
+
"PoweredOff" -> AdapterLossConnectFailure(
|
|
150
|
+
"adapterPoweredOff",
|
|
151
|
+
"Android Bluetooth was powered off while the connection was pending"
|
|
152
|
+
)
|
|
153
|
+
"Resetting" -> AdapterLossConnectFailure(
|
|
154
|
+
"adapterResetting",
|
|
155
|
+
"Android Bluetooth reset while the connection was pending"
|
|
156
|
+
)
|
|
157
|
+
else -> AdapterLossConnectFailure(
|
|
158
|
+
"adapterUnavailable",
|
|
159
|
+
"Android Bluetooth became unavailable while the connection was pending"
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
pendingConnects.entries.toList().forEach { entry ->
|
|
163
|
+
if (pendingConnects.remove(entry.key, entry.value)) {
|
|
164
|
+
emitFailure(entry.value, failure.code, failure.message)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
establishedConnections.clear()
|
|
168
|
+
activeDatabases.clear()
|
|
169
|
+
activeSubscriptions.clear()
|
|
170
|
+
}
|
|
171
|
+
|
|
143
172
|
fun emitCurrentAdapterState() {
|
|
144
173
|
val state = radio.currentProtocolAdapterState()
|
|
145
174
|
val fields = mutableMapOf<Int, ProtocolWireValue>(
|
|
@@ -564,6 +593,11 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
564
593
|
}
|
|
565
594
|
|
|
566
595
|
private fun subscribe(command: ProtocolWireRecord, enable: Boolean) {
|
|
596
|
+
val subscriptionId = command.requiredString(7)
|
|
597
|
+
if (!enable && !activeSubscriptions.containsKey(subscriptionId)) {
|
|
598
|
+
emitSuccess(command, "unsubscribed")
|
|
599
|
+
return
|
|
600
|
+
}
|
|
567
601
|
val endpoint = characteristicEndpoint(command.requiredRecord(4))
|
|
568
602
|
val radioOperationId = radio.setNotifyExact(
|
|
569
603
|
endpoint.deviceId,
|
|
@@ -597,7 +631,6 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
597
631
|
}
|
|
598
632
|
return@fold
|
|
599
633
|
}
|
|
600
|
-
val subscriptionId = command.requiredString(7)
|
|
601
634
|
if (enable) {
|
|
602
635
|
activeSubscriptions[subscriptionId] = SubscriptionRoute(
|
|
603
636
|
subscriptionId,
|
|
@@ -609,7 +642,9 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
609
642
|
}
|
|
610
643
|
emitSuccess(command, if (enable) "subscribed" else "unsubscribed")
|
|
611
644
|
},
|
|
612
|
-
onFailure = { error ->
|
|
645
|
+
onFailure = { error ->
|
|
646
|
+
emitGattOperationFailure(command, "subscriptionFailed", error, "Android CCCD operation failed")
|
|
647
|
+
}
|
|
613
648
|
)
|
|
614
649
|
}
|
|
615
650
|
radioOperationIds[operationKey(command)] = radioOperationId
|
|
@@ -824,21 +859,44 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
824
859
|
radioOperationIds.remove(operationKey(command))
|
|
825
860
|
}
|
|
826
861
|
|
|
827
|
-
private fun
|
|
862
|
+
private fun emitGattOperationFailure(
|
|
863
|
+
command: ProtocolWireRecord,
|
|
864
|
+
fallbackCode: String,
|
|
865
|
+
error: Throwable,
|
|
866
|
+
fallbackMessage: String
|
|
867
|
+
) {
|
|
868
|
+
emitFailure(
|
|
869
|
+
command,
|
|
870
|
+
androidGattOperationFailureCode(error, fallbackCode),
|
|
871
|
+
error.message ?: fallbackMessage,
|
|
872
|
+
androidGattOperationFailureStatus(error)
|
|
873
|
+
)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private fun emitFailure(
|
|
877
|
+
command: ProtocolWireRecord,
|
|
878
|
+
code: String,
|
|
879
|
+
message: String,
|
|
880
|
+
androidGattStatus: Int? = null
|
|
881
|
+
) {
|
|
828
882
|
val bondedPeerCommand = command.requiredString(3) == "enumerateBondedPeers"
|
|
829
883
|
if (bondedPeerCommand) {
|
|
830
884
|
// Keep failure/cancellation/teardown mutually exclusive with a late success.
|
|
831
885
|
if (!claimExactPendingCommand(pendingCommands, operationKey(command), command)) return
|
|
832
886
|
} else if (!isPending(command)) return
|
|
833
|
-
val
|
|
834
|
-
RecordKind.ERROR,
|
|
835
|
-
mapOf(
|
|
887
|
+
val errorFields = mutableMapOf<Int, ProtocolWireValue>(
|
|
836
888
|
1 to ProtocolWireValue.StringValue(code),
|
|
837
889
|
2 to ProtocolWireValue.StringValue("android"),
|
|
838
890
|
3 to ProtocolWireValue.StringValue(command.requiredString(3)),
|
|
839
891
|
4 to ProtocolWireValue.StringValue("notRetryable"),
|
|
840
892
|
7 to ProtocolWireValue.StringValue(message)
|
|
841
893
|
)
|
|
894
|
+
androidGattStatus?.let { status ->
|
|
895
|
+
errorFields[8] = ProtocolWireValue.SignedIntegerValue(status.toLong())
|
|
896
|
+
}
|
|
897
|
+
val error = ProtocolWireRecord(
|
|
898
|
+
RecordKind.ERROR,
|
|
899
|
+
errorFields
|
|
842
900
|
)
|
|
843
901
|
val result = ProtocolWireRecord(
|
|
844
902
|
RecordKind.RESULT,
|
|
@@ -1132,6 +1190,11 @@ class UnifiedBleProtocolAndroidDispatcher(
|
|
|
1132
1190
|
}
|
|
1133
1191
|
}
|
|
1134
1192
|
|
|
1193
|
+
private data class AdapterLossConnectFailure(
|
|
1194
|
+
val code: String,
|
|
1195
|
+
val message: String
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1135
1198
|
private fun requirePhyAvailable() {
|
|
1136
1199
|
check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1137
1200
|
"Android PHY requires API 26"
|
|
@@ -1158,6 +1221,7 @@ internal fun connectionLostEvent(
|
|
|
1158
1221
|
8 to ProtocolWireValue.SignedIntegerValue(status.toLong())
|
|
1159
1222
|
)
|
|
1160
1223
|
)
|
|
1224
|
+
|
|
1161
1225
|
return ProtocolWireRecord(
|
|
1162
1226
|
RecordKind.EVENT,
|
|
1163
1227
|
mapOf(
|
|
@@ -1173,6 +1237,14 @@ internal fun connectionLostEvent(
|
|
|
1173
1237
|
)
|
|
1174
1238
|
}
|
|
1175
1239
|
|
|
1240
|
+
internal fun androidGattOperationFailureCode(error: Throwable, fallbackCode: String): String {
|
|
1241
|
+
val gattFailure = error as? AndroidGattOperationFailure
|
|
1242
|
+
return if (gattFailure?.isLinkLoss == true) "connectionLost" else fallbackCode
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
internal fun androidGattOperationFailureStatus(error: Throwable): Int? =
|
|
1246
|
+
(error as? AndroidGattOperationFailure)?.gattStatus
|
|
1247
|
+
|
|
1176
1248
|
internal fun databaseChangedEvent(
|
|
1177
1249
|
nativeHandle: Long,
|
|
1178
1250
|
database: ProtocolWireRecord,
|
package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt
CHANGED
|
@@ -109,6 +109,27 @@ internal class OwnedAndroidSubscriptionOwnership<K> {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Preserves the Android GATT callback status when an asynchronous operation
|
|
114
|
+
* terminates. Status 19 means that the peer terminated the link; keeping that
|
|
115
|
+
* distinction typed prevents a CCCD race from being flattened to a generic
|
|
116
|
+
* platform failure before the later connection-state callback arrives.
|
|
117
|
+
*/
|
|
118
|
+
internal class AndroidGattOperationFailure(
|
|
119
|
+
val operation: String,
|
|
120
|
+
val gattStatus: Int
|
|
121
|
+
) : IllegalStateException("$operation status=$gattStatus") {
|
|
122
|
+
val isLinkLoss: Boolean
|
|
123
|
+
get() = gattStatus == ANDROID_GATT_LINK_LOSS_STATUS
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
internal const val ANDROID_GATT_LINK_LOSS_STATUS = 19
|
|
127
|
+
|
|
128
|
+
internal fun classifyAndroidGattOperationFailure(
|
|
129
|
+
operation: String,
|
|
130
|
+
gattStatus: Int
|
|
131
|
+
): AndroidGattOperationFailure = AndroidGattOperationFailure(operation, gattStatus)
|
|
132
|
+
|
|
112
133
|
/** Runtime adapter state derived from Android hardware and granted permissions. */
|
|
113
134
|
internal data class OwnedRadioAdapterProtocolState(
|
|
114
135
|
val availability: String,
|
|
@@ -150,6 +171,11 @@ internal fun bondedPeerAdapterReadiness(
|
|
|
150
171
|
}
|
|
151
172
|
}
|
|
152
173
|
|
|
174
|
+
internal fun requiresImmediateGattTeardownOnAdapterState(state: Int?): Boolean =
|
|
175
|
+
state == BluetoothAdapter.STATE_OFF ||
|
|
176
|
+
state == BluetoothAdapter.STATE_TURNING_OFF ||
|
|
177
|
+
state == BluetoothAdapter.STATE_TURNING_ON
|
|
178
|
+
|
|
153
179
|
/** Normalize the platform snapshot before it crosses the Android protocol boundary. */
|
|
154
180
|
internal fun normalizeBondedPeerSnapshots(
|
|
155
181
|
peers: Iterable<BondedPeerSnapshot>
|
|
@@ -416,6 +442,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
416
442
|
override fun onReceive(ctx: Context?, intent: Intent?) {
|
|
417
443
|
if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
|
|
418
444
|
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
|
|
445
|
+
handleAdapterStateTransition(state)
|
|
419
446
|
onAdapterState?.invoke(mapAdapterState(state))
|
|
420
447
|
}
|
|
421
448
|
}
|
|
@@ -430,6 +457,23 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
430
457
|
adapterStateReceiver = receiver
|
|
431
458
|
}
|
|
432
459
|
|
|
460
|
+
private fun handleAdapterStateTransition(state: Int) {
|
|
461
|
+
if (state == BluetoothAdapter.STATE_ON) {
|
|
462
|
+
pendingGattTeardowns.keys.toList().forEach { key -> retryGattTeardown(key) }
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (!requiresImmediateGattTeardownOnAdapterState(state)) return
|
|
466
|
+
|
|
467
|
+
pendingReconnect.clear()
|
|
468
|
+
(gatts.keys + pendingGattTeardowns.keys).toSet().forEach { key ->
|
|
469
|
+
failPendingForDevice(key, "adapter unavailable")
|
|
470
|
+
val owner = pendingGattTeardowns[key]
|
|
471
|
+
val gatt = gatts[key] ?: owner?.gatt ?: return@forEach
|
|
472
|
+
val teardownFailure = completeGattTeardown(key, gatt)
|
|
473
|
+
pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
433
477
|
internal fun unregisterAdapterStateReceiver(): OwnedRadioTeardownFailure? {
|
|
434
478
|
val receiver = adapterStateReceiver ?: return null
|
|
435
479
|
try {
|
|
@@ -838,6 +882,18 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
838
882
|
onComplete(failure)
|
|
839
883
|
return failure
|
|
840
884
|
}
|
|
885
|
+
if (pendingGattTeardowns[key] != null) {
|
|
886
|
+
val teardownFailure = retryGattTeardown(key)
|
|
887
|
+
val failure = cleanupFailure ?: teardownFailure
|
|
888
|
+
onComplete(failure)
|
|
889
|
+
return failure
|
|
890
|
+
}
|
|
891
|
+
if (requiresImmediateGattTeardownOnAdapterState(adapter?.state)) {
|
|
892
|
+
val teardownFailure = completeGattTeardown(key, g)
|
|
893
|
+
val failure = cleanupFailure ?: teardownFailure
|
|
894
|
+
onComplete(failure)
|
|
895
|
+
return failure
|
|
896
|
+
}
|
|
841
897
|
// R3-F003: only disconnect(); close() waits for STATE_DISCONNECTED (or safety timeout).
|
|
842
898
|
pendingDisconnectCallbacks[key] = onComplete
|
|
843
899
|
scheduleSafeClose(key, g)
|
|
@@ -923,6 +979,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
923
979
|
Runnable {
|
|
924
980
|
if (gatts[key] === gatt) {
|
|
925
981
|
OwnedAndroidLog.e("GATT close safety timeout for $key (DISCONNECTED never arrived)")
|
|
982
|
+
dispatchConnectionState(key, false, BluetoothGatt.GATT_FAILURE)
|
|
926
983
|
failPendingForDevice(key, "disconnected timeout")
|
|
927
984
|
val teardownFailure = completeGattTeardown(key, gatt)
|
|
928
985
|
pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
|
|
@@ -2443,7 +2500,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2443
2500
|
} else {
|
|
2444
2501
|
completeExactUnit(
|
|
2445
2502
|
pending,
|
|
2446
|
-
Result.failure(
|
|
2503
|
+
Result.failure(classifyAndroidGattOperationFailure("descriptor-write", status))
|
|
2447
2504
|
)
|
|
2448
2505
|
}
|
|
2449
2506
|
return
|
|
@@ -2458,7 +2515,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2458
2515
|
} else {
|
|
2459
2516
|
completeExactUnit(
|
|
2460
2517
|
pending,
|
|
2461
|
-
Result.failure(
|
|
2518
|
+
Result.failure(classifyAndroidGattOperationFailure("cccd-write", status))
|
|
2462
2519
|
)
|
|
2463
2520
|
}
|
|
2464
2521
|
return
|
|
@@ -2479,7 +2536,14 @@ class OwnedAndroidGattRadio(private val context: Context) {
|
|
|
2479
2536
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
|
2480
2537
|
cb?.invoke(Result.success(Unit))
|
|
2481
2538
|
} else {
|
|
2482
|
-
cb?.invoke(
|
|
2539
|
+
cb?.invoke(
|
|
2540
|
+
Result.failure(
|
|
2541
|
+
classifyAndroidGattOperationFailure(
|
|
2542
|
+
if (matchedKey?.startsWith("cccd:") == true) "cccd-write" else "descriptor-write",
|
|
2543
|
+
status
|
|
2544
|
+
)
|
|
2545
|
+
)
|
|
2546
|
+
)
|
|
2483
2547
|
}
|
|
2484
2548
|
}
|
|
2485
2549
|
|