@onekeyfe/hd-transport-react-native 1.2.3-alpha.7 → 1.2.3-alpha.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/dist/BleManager.d.ts +6 -5
- package/dist/BleManager.d.ts.map +1 -1
- package/dist/bleKeyMissing.d.ts +7 -0
- package/dist/bleKeyMissing.d.ts.map +1 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -23
- package/package.json +6 -6
- package/src/BleManager.ts +78 -38
- package/src/__tests__/androidKeyMissing.test.ts +307 -0
- package/src/__tests__/enumerate.test.ts +8 -0
- package/src/__tests__/protocolV2Link.test.ts +8 -0
- package/src/bleKeyMissing.ts +106 -0
- package/src/index.ts +82 -8
package/dist/BleManager.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Peripheral } from '@onekeyfe/react-native-ble-utils';
|
|
2
2
|
export declare const getConnectedDeviceIds: (serviceUuids: string[]) => Promise<Peripheral[]>;
|
|
3
3
|
export declare const getBondedDevices: () => Promise<Peripheral[]>;
|
|
4
|
-
export declare const pairDevice: (macAddress: string) => Promise<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export declare const pairDevice: (macAddress: string) => Promise<import("@onekeyfe/react-native-ble-utils").PairDeviceResult>;
|
|
5
|
+
export declare const SYSTEM_BONDING_RESTART_WINDOW_MS = 1500;
|
|
6
|
+
export type DeviceBondStateOptions = {
|
|
7
|
+
systemInitiated?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const onDeviceBondState: (bleMacAddress: string, signal?: AbortSignal, options?: DeviceBondStateOptions) => Promise<Peripheral | undefined>;
|
|
9
10
|
//# sourceMappingURL=BleManager.d.ts.map
|
package/dist/BleManager.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BleManager.d.ts","sourceRoot":"","sources":["../src/BleManager.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"BleManager.d.ts","sourceRoot":"","sources":["../src/BleManager.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAsBnE,eAAO,MAAM,qBAAqB,iBAAkB,MAAM,EAAE,0BACZ,CAAC;AAEjD,eAAO,MAAM,gBAAgB,6BAAwC,CAAC;AAEtE,eAAO,MAAM,UAAU,eAAgB,MAAM,yEAAoC,CAAC;AAMlF,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAAG;IAMnC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAkCF,eAAO,MAAM,iBAAiB,kBACb,MAAM,WACZ,WAAW,YACV,sBAAsB,KAC/B,QAAQ,UAAU,GAAG,SAAS,CA0E7B,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const isBleKeyMissingSupported: () => boolean;
|
|
2
|
+
export declare const startBleKeyMissingTracking: () => boolean;
|
|
3
|
+
export declare const stopBleKeyMissingTracking: () => void;
|
|
4
|
+
export declare const hasBleKeyMissingSince: (deviceId: string, sinceMs: number) => boolean;
|
|
5
|
+
export declare const onBleKeyMissing: (deviceId: string, callback: () => void) => (() => void);
|
|
6
|
+
export declare const waitForBleKeyMissing: (deviceId: string, sinceMs: number, timeoutMs: number) => Promise<boolean>;
|
|
7
|
+
//# sourceMappingURL=bleKeyMissing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bleKeyMissing.d.ts","sourceRoot":"","sources":["../src/bleKeyMissing.ts"],"names":[],"mappings":"AAwCA,eAAO,MAAM,wBAAwB,eAA2C,CAAC;AAGjF,eAAO,MAAM,0BAA0B,QAAO,OAY7C,CAAC;AAEF,eAAO,MAAM,yBAAyB,YAKrC,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,KAAG,OAGzE,CAAC;AAGF,eAAO,MAAM,eAAe,aAAc,MAAM,YAAY,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CASnF,CAAC;AAOF,eAAO,MAAM,oBAAoB,aACrB,MAAM,WACP,MAAM,aACJ,MAAM,KAChB,QAAQ,OAAO,CAejB,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,14 @@ declare const getFirmwareUploadWriteRetryType: (error: unknown) => FirmwareUploa
|
|
|
62
62
|
*/
|
|
63
63
|
declare const BLE_WRITE_PACKET_TIMEOUT_MS = 10000;
|
|
64
64
|
declare const BLE_NATIVE_TEARDOWN_TIMEOUT_MS = 3000;
|
|
65
|
+
/**
|
|
66
|
+
* Android reports a lost bond (ACTION_KEY_MISSING) while it encrypts a new link, so only
|
|
67
|
+
* a failure this soon after the link started can be explained by it. Later disconnects,
|
|
68
|
+
* such as a firmware-update reboot, must keep their own meaning.
|
|
69
|
+
*/
|
|
70
|
+
declare const ANDROID_KEY_MISSING_LINK_WINDOW_MS = 10000;
|
|
71
|
+
/** The broadcast and the GATT disconnect it explains travel separately; either can land first. */
|
|
72
|
+
declare const ANDROID_KEY_MISSING_GRACE_MS = 500;
|
|
65
73
|
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
66
74
|
declare const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
67
75
|
type ProtocolV2BleTuning = {
|
|
@@ -155,6 +163,8 @@ declare class ReactNativeBleTransport {
|
|
|
155
163
|
* fails with ATT 5/15. Remember it so detectProtocol fails immediately.
|
|
156
164
|
*/
|
|
157
165
|
private staleBondErrors;
|
|
166
|
+
/** When the current Android link attempt began; bounds which key-missing signals apply. */
|
|
167
|
+
private androidLinkStartedAt;
|
|
158
168
|
/** Strict or previously confirmed V2 target while acquire installs notifications. */
|
|
159
169
|
private acquiringProtocolV2;
|
|
160
170
|
private protocolV2Assemblers;
|
|
@@ -200,6 +210,12 @@ declare class ReactNativeBleTransport {
|
|
|
200
210
|
private releaseNative;
|
|
201
211
|
post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
|
|
202
212
|
call(uuid: string, name: string, data: Record<string, unknown>, options?: TransportCallOptions): Promise<transport.MessageFromOneKey>;
|
|
213
|
+
/**
|
|
214
|
+
* Android 16+ keeps a bond the device no longer holds keys for. The device drops the link
|
|
215
|
+
* and the only structured evidence is ACTION_KEY_MISSING, so a link loss right after
|
|
216
|
+
* connect is re-read as an invalid bond when that signal belongs to the same attempt.
|
|
217
|
+
*/
|
|
218
|
+
private resolveAndroidBondInvalid;
|
|
203
219
|
private callProtocolV1;
|
|
204
220
|
stop(): Promise<void>;
|
|
205
221
|
disconnect(session: string): Promise<void>;
|
|
@@ -279,4 +295,4 @@ declare class ReactNativeBleTransport {
|
|
|
279
295
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
280
296
|
}
|
|
281
297
|
|
|
282
|
-
export { ANDROID_LINK_DROP_QUIET_MS, ANDROID_MTU_EXCHANGE_TIMEOUT_MS, BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_GATT_SETUP_TIMEOUT_MS, BLE_NATIVE_TEARDOWN_TIMEOUT_MS, BLE_SETUP_WEDGED_MESSAGE, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, PROTOCOL_REPROBE_FALLBACK_ATTEMPTS, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
|
298
|
+
export { ANDROID_KEY_MISSING_GRACE_MS, ANDROID_KEY_MISSING_LINK_WINDOW_MS, ANDROID_LINK_DROP_QUIET_MS, ANDROID_MTU_EXCHANGE_TIMEOUT_MS, BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD, BLE_CONNECT_TIMEOUT_MS, BLE_GATT_SETUP_TIMEOUT_MS, BLE_NATIVE_TEARDOWN_TIMEOUT_MS, BLE_SETUP_WEDGED_MESSAGE, BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, PROTOCOL_REPROBE_FALLBACK_ATTEMPTS, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAGL,UAAU,IAAI,aAAa,EAG5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAChB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAI1B,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,76 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
46
46
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
const lastKeyMissingAt = new Map();
|
|
50
|
+
const listeners = new Set();
|
|
51
|
+
let unsubscribeNative;
|
|
52
|
+
const normalizeDeviceId = (deviceId) => deviceId.toLowerCase();
|
|
53
|
+
const getCapableBleUtils = () => {
|
|
54
|
+
if (reactNative.Platform.OS !== 'android')
|
|
55
|
+
return undefined;
|
|
56
|
+
const bleUtils = BleUtils__default["default"];
|
|
57
|
+
if (typeof bleUtils.supportsDeviceKeyMissing !== 'function' ||
|
|
58
|
+
typeof bleUtils.onDeviceKeyMissing !== 'function') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return bleUtils.supportsDeviceKeyMissing() ? bleUtils : undefined;
|
|
62
|
+
};
|
|
63
|
+
const isBleKeyMissingSupported = () => getCapableBleUtils() !== undefined;
|
|
64
|
+
const startBleKeyMissingTracking = () => {
|
|
65
|
+
if (unsubscribeNative)
|
|
66
|
+
return true;
|
|
67
|
+
const bleUtils = getCapableBleUtils();
|
|
68
|
+
if (!(bleUtils === null || bleUtils === void 0 ? void 0 : bleUtils.onDeviceKeyMissing))
|
|
69
|
+
return false;
|
|
70
|
+
unsubscribeNative = bleUtils.onDeviceKeyMissing(event => {
|
|
71
|
+
if (typeof (event === null || event === void 0 ? void 0 : event.id) !== 'string')
|
|
72
|
+
return;
|
|
73
|
+
const deviceId = normalizeDeviceId(event.id);
|
|
74
|
+
lastKeyMissingAt.set(deviceId, Date.now());
|
|
75
|
+
listeners.forEach(listener => listener(deviceId));
|
|
76
|
+
});
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
const stopBleKeyMissingTracking = () => {
|
|
80
|
+
unsubscribeNative === null || unsubscribeNative === void 0 ? void 0 : unsubscribeNative();
|
|
81
|
+
unsubscribeNative = undefined;
|
|
82
|
+
lastKeyMissingAt.clear();
|
|
83
|
+
listeners.clear();
|
|
84
|
+
};
|
|
85
|
+
const hasBleKeyMissingSince = (deviceId, sinceMs) => {
|
|
86
|
+
const at = lastKeyMissingAt.get(normalizeDeviceId(deviceId));
|
|
87
|
+
return at !== undefined && at >= sinceMs;
|
|
88
|
+
};
|
|
89
|
+
const onBleKeyMissing = (deviceId, callback) => {
|
|
90
|
+
const target = normalizeDeviceId(deviceId);
|
|
91
|
+
const listener = id => {
|
|
92
|
+
if (id === target)
|
|
93
|
+
callback();
|
|
94
|
+
};
|
|
95
|
+
listeners.add(listener);
|
|
96
|
+
return () => {
|
|
97
|
+
listeners.delete(listener);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const waitForBleKeyMissing = (deviceId, sinceMs, timeoutMs) => {
|
|
101
|
+
if (!startBleKeyMissingTracking())
|
|
102
|
+
return Promise.resolve(false);
|
|
103
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs))
|
|
104
|
+
return Promise.resolve(true);
|
|
105
|
+
return new Promise(resolve => {
|
|
106
|
+
const settle = (found) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
cleanup();
|
|
109
|
+
resolve(found);
|
|
110
|
+
};
|
|
111
|
+
const cleanup = onBleKeyMissing(deviceId, () => {
|
|
112
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs))
|
|
113
|
+
settle(true);
|
|
114
|
+
});
|
|
115
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
49
119
|
let activeLogger;
|
|
50
120
|
const setBleLogger = (logger) => {
|
|
51
121
|
activeLogger = logger;
|
|
@@ -70,17 +140,35 @@ const bondFailureReasons = {
|
|
|
70
140
|
};
|
|
71
141
|
const getConnectedDeviceIds = (serviceUuids) => BleUtils__default["default"].getConnectedPeripherals(serviceUuids);
|
|
72
142
|
const pairDevice = (macAddress) => BleUtils__default["default"].pairDevice(macAddress);
|
|
73
|
-
const
|
|
143
|
+
const SYSTEM_BONDING_RESTART_WINDOW_MS = 1500;
|
|
144
|
+
const createBondFailureError = (bondState) => {
|
|
145
|
+
var _a;
|
|
146
|
+
const nativeReason = 'reason' in bondState && typeof bondState.reason === 'number' ? bondState.reason : undefined;
|
|
147
|
+
const reason = nativeReason === undefined ? 'unknown' : (_a = bondFailureReasons[nativeReason]) !== null && _a !== void 0 ? _a : 'unknown';
|
|
148
|
+
const params = Object.assign({ phase: 'bond', reason }, (nativeReason === undefined ? {} : { nativeReason }));
|
|
149
|
+
if (reason === 'timeout') {
|
|
150
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', params);
|
|
151
|
+
}
|
|
152
|
+
if (reason === 'canceled') {
|
|
153
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceBondedCanceled, 'Bluetooth pairing canceled', params);
|
|
154
|
+
}
|
|
155
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing failed', params);
|
|
156
|
+
};
|
|
157
|
+
const onDeviceBondState = (bleMacAddress, signal, options) => new Promise((resolve, reject) => {
|
|
74
158
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
75
159
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected));
|
|
76
160
|
return;
|
|
77
161
|
}
|
|
162
|
+
let pendingFailure;
|
|
78
163
|
const cleanup = () => {
|
|
79
164
|
if (timeout) {
|
|
80
165
|
clearTimeout(timeout);
|
|
81
166
|
}
|
|
167
|
+
if (pendingFailure)
|
|
168
|
+
clearTimeout(pendingFailure);
|
|
82
169
|
if (cleanupListener)
|
|
83
170
|
cleanupListener();
|
|
171
|
+
cleanupKeyMissing === null || cleanupKeyMissing === void 0 ? void 0 : cleanupKeyMissing();
|
|
84
172
|
signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
|
|
85
173
|
};
|
|
86
174
|
const onAbort = () => {
|
|
@@ -95,35 +183,44 @@ const onDeviceBondState = (bleMacAddress, signal) => new Promise((resolve, rejec
|
|
|
95
183
|
reason: 'timeout',
|
|
96
184
|
}));
|
|
97
185
|
}, 60 * 1000);
|
|
186
|
+
const cleanupKeyMissing = startBleKeyMissingTracking()
|
|
187
|
+
? onBleKeyMissing(bleMacAddress, () => {
|
|
188
|
+
cleanup();
|
|
189
|
+
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
|
|
190
|
+
phase: 'bond',
|
|
191
|
+
reason: 'key_missing',
|
|
192
|
+
}));
|
|
193
|
+
})
|
|
194
|
+
: undefined;
|
|
98
195
|
const cleanupListener = BleUtils__default["default"].onDeviceBondState(peripheral => {
|
|
99
|
-
var _a
|
|
196
|
+
var _a;
|
|
100
197
|
if (((_a = peripheral.id) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== bleMacAddress.toLowerCase()) {
|
|
101
198
|
return;
|
|
102
199
|
}
|
|
103
200
|
const { bondState } = peripheral;
|
|
104
201
|
const hasBonded = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_BONDED';
|
|
105
202
|
const hasFailed = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_NONE';
|
|
203
|
+
const hasRestarted = bondState.preState === 'BOND_NONE' && bondState.state === 'BOND_BONDING';
|
|
106
204
|
Logger.debug('onDeviceBondState bondState:', bondState);
|
|
107
205
|
if (hasBonded) {
|
|
108
206
|
cleanup();
|
|
109
207
|
resolve(peripheral);
|
|
110
208
|
}
|
|
209
|
+
else if (hasRestarted && pendingFailure) {
|
|
210
|
+
clearTimeout(pendingFailure);
|
|
211
|
+
pendingFailure = undefined;
|
|
212
|
+
}
|
|
213
|
+
else if (hasFailed && (options === null || options === void 0 ? void 0 : options.systemInitiated)) {
|
|
214
|
+
if (pendingFailure)
|
|
215
|
+
clearTimeout(pendingFailure);
|
|
216
|
+
pendingFailure = setTimeout(() => {
|
|
217
|
+
cleanup();
|
|
218
|
+
reject(createBondFailureError(bondState));
|
|
219
|
+
}, SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
220
|
+
}
|
|
111
221
|
else if (hasFailed) {
|
|
112
222
|
cleanup();
|
|
113
|
-
|
|
114
|
-
? bondState.reason
|
|
115
|
-
: undefined;
|
|
116
|
-
const reason = nativeReason === undefined ? 'unknown' : (_b = bondFailureReasons[nativeReason]) !== null && _b !== void 0 ? _b : 'unknown';
|
|
117
|
-
const params = Object.assign({ phase: 'bond', reason }, (nativeReason === undefined ? {} : { nativeReason }));
|
|
118
|
-
if (reason === 'timeout') {
|
|
119
|
-
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing timed out', params));
|
|
120
|
-
}
|
|
121
|
-
else if (reason === 'canceled') {
|
|
122
|
-
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceBondedCanceled, 'Bluetooth pairing canceled', params));
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'Bluetooth pairing failed', params));
|
|
126
|
-
}
|
|
223
|
+
reject(createBondFailureError(bondState));
|
|
127
224
|
}
|
|
128
225
|
});
|
|
129
226
|
});
|
|
@@ -386,6 +483,16 @@ const shouldRethrowProtocolProbeError = (error) => {
|
|
|
386
483
|
code === hdShared.HardwareErrorCode.BleCharacteristicNotifyChangeFailure ||
|
|
387
484
|
code === hdShared.HardwareErrorCode.BleWriteCharacteristicError);
|
|
388
485
|
};
|
|
486
|
+
const ANDROID_KEY_MISSING_LINK_WINDOW_MS = 10000;
|
|
487
|
+
const ANDROID_KEY_MISSING_GRACE_MS = 500;
|
|
488
|
+
const isAndroidLinkLossError = (error) => {
|
|
489
|
+
const code = error === null || error === void 0 ? void 0 : error.errorCode;
|
|
490
|
+
return (isNativeBleDisconnectError(error) ||
|
|
491
|
+
code === hdShared.HardwareErrorCode.BleDeviceNotBonded ||
|
|
492
|
+
code === hdShared.HardwareErrorCode.BleDeviceDisconnected ||
|
|
493
|
+
code === hdShared.HardwareErrorCode.BleCharacteristicNotifyError ||
|
|
494
|
+
code === hdShared.HardwareErrorCode.BleConnectedError);
|
|
495
|
+
};
|
|
389
496
|
const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
390
497
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
391
498
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
@@ -583,6 +690,7 @@ class ReactNativeBleTransport {
|
|
|
583
690
|
this.silentDetections = new Map();
|
|
584
691
|
this.androidGattCacheRefreshes = new Set();
|
|
585
692
|
this.staleBondErrors = new Map();
|
|
693
|
+
this.androidLinkStartedAt = new Map();
|
|
586
694
|
this.acquiringProtocolV2 = new Set();
|
|
587
695
|
this.protocolV2Assemblers = new Map();
|
|
588
696
|
this.protocolV2FrameQueues = new Map();
|
|
@@ -1016,7 +1124,14 @@ class ReactNativeBleTransport {
|
|
|
1016
1124
|
if (!uuid) {
|
|
1017
1125
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleRequiredUUID);
|
|
1018
1126
|
}
|
|
1019
|
-
return this.runLifecycleOperation(uuid, () => this
|
|
1127
|
+
return this.runLifecycleOperation(uuid, () => __awaiter(this, void 0, void 0, function* () {
|
|
1128
|
+
try {
|
|
1129
|
+
return yield this.acquireUnlocked(input);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
throw yield this.resolveAndroidBondInvalid(uuid, error);
|
|
1133
|
+
}
|
|
1134
|
+
}));
|
|
1020
1135
|
});
|
|
1021
1136
|
}
|
|
1022
1137
|
acquireUnlocked(input) {
|
|
@@ -1098,10 +1213,14 @@ class ReactNativeBleTransport {
|
|
|
1098
1213
|
if (this.stopped)
|
|
1099
1214
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
1100
1215
|
if (reactNative.Platform.OS === 'android') {
|
|
1216
|
+
startBleKeyMissingTracking();
|
|
1217
|
+
this.androidLinkStartedAt.delete(uuid);
|
|
1101
1218
|
try {
|
|
1102
1219
|
const bondState = yield pairDevice(uuid);
|
|
1103
1220
|
if (bondState.bonding) {
|
|
1104
|
-
yield onDeviceBondState(uuid, this.bondAbortController.signal
|
|
1221
|
+
yield onDeviceBondState(uuid, this.bondAbortController.signal, {
|
|
1222
|
+
systemInitiated: bondState.initiated === false,
|
|
1223
|
+
});
|
|
1105
1224
|
}
|
|
1106
1225
|
else if (!bondState.bonded) {
|
|
1107
1226
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
@@ -1113,6 +1232,7 @@ class ReactNativeBleTransport {
|
|
|
1113
1232
|
}));
|
|
1114
1233
|
throw error;
|
|
1115
1234
|
}
|
|
1235
|
+
this.androidLinkStartedAt.set(uuid, Date.now());
|
|
1116
1236
|
}
|
|
1117
1237
|
if (this.stopped)
|
|
1118
1238
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
@@ -1510,14 +1630,39 @@ class ReactNativeBleTransport {
|
|
|
1510
1630
|
if (!protocol) {
|
|
1511
1631
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol has not been detected for ${uuid}`);
|
|
1512
1632
|
}
|
|
1513
|
-
if (protocol === 'V2') {
|
|
1514
|
-
return this.callProtocolV2(uuid, name, data, options);
|
|
1515
|
-
}
|
|
1516
1633
|
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1517
|
-
if (this.runPromise && !forceRun) {
|
|
1634
|
+
if (protocol !== 'V2' && this.runPromise && !forceRun) {
|
|
1518
1635
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportCallInProgress);
|
|
1519
1636
|
}
|
|
1520
|
-
|
|
1637
|
+
try {
|
|
1638
|
+
return protocol === 'V2'
|
|
1639
|
+
? yield this.callProtocolV2(uuid, name, data, options)
|
|
1640
|
+
: yield this.callProtocolV1(uuid, name, data, options);
|
|
1641
|
+
}
|
|
1642
|
+
catch (error) {
|
|
1643
|
+
throw yield this.resolveAndroidBondInvalid(uuid, error);
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
resolveAndroidBondInvalid(uuid, error) {
|
|
1648
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1649
|
+
if (reactNative.Platform.OS !== 'android' || !isBleKeyMissingSupported())
|
|
1650
|
+
return error;
|
|
1651
|
+
if (hdShared.isBleStaleBondHardwareError(error) || !isAndroidLinkLossError(error))
|
|
1652
|
+
return error;
|
|
1653
|
+
const linkStartedAt = this.androidLinkStartedAt.get(uuid);
|
|
1654
|
+
if (linkStartedAt === undefined ||
|
|
1655
|
+
Date.now() - linkStartedAt > ANDROID_KEY_MISSING_LINK_WINDOW_MS) {
|
|
1656
|
+
return error;
|
|
1657
|
+
}
|
|
1658
|
+
if (!(yield waitForBleKeyMissing(uuid, linkStartedAt, ANDROID_KEY_MISSING_GRACE_MS))) {
|
|
1659
|
+
return error;
|
|
1660
|
+
}
|
|
1661
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Android key missing, bond is invalid:', uuid);
|
|
1662
|
+
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, undefined, {
|
|
1663
|
+
phase: 'connect',
|
|
1664
|
+
reason: 'key_missing',
|
|
1665
|
+
});
|
|
1521
1666
|
});
|
|
1522
1667
|
}
|
|
1523
1668
|
callProtocolV1(uuid, name, data, options) {
|
|
@@ -1733,6 +1878,8 @@ class ReactNativeBleTransport {
|
|
|
1733
1878
|
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1734
1879
|
this.androidPriorityResetTimers.clear();
|
|
1735
1880
|
this.androidHighPriorityDevices.clear();
|
|
1881
|
+
this.androidLinkStartedAt.clear();
|
|
1882
|
+
stopBleKeyMissingTracking();
|
|
1736
1883
|
const error = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceDisconnected);
|
|
1737
1884
|
(_a = this.runPromise) === null || _a === void 0 ? void 0 : _a.reject(error);
|
|
1738
1885
|
this.runPromise = null;
|
|
@@ -2766,6 +2913,8 @@ class ReactNativeBleTransport {
|
|
|
2766
2913
|
}
|
|
2767
2914
|
}
|
|
2768
2915
|
|
|
2916
|
+
exports.ANDROID_KEY_MISSING_GRACE_MS = ANDROID_KEY_MISSING_GRACE_MS;
|
|
2917
|
+
exports.ANDROID_KEY_MISSING_LINK_WINDOW_MS = ANDROID_KEY_MISSING_LINK_WINDOW_MS;
|
|
2769
2918
|
exports.ANDROID_LINK_DROP_QUIET_MS = ANDROID_LINK_DROP_QUIET_MS;
|
|
2770
2919
|
exports.ANDROID_MTU_EXCHANGE_TIMEOUT_MS = ANDROID_MTU_EXCHANGE_TIMEOUT_MS;
|
|
2771
2920
|
exports.BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.3-alpha.
|
|
3
|
+
"version": "1.2.3-alpha.9",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.3-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.3-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.3-alpha.
|
|
26
|
-
"@onekeyfe/react-native-ble-utils": "0.1.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.3-alpha.9",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.3-alpha.9",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.3-alpha.9",
|
|
26
|
+
"@onekeyfe/react-native-ble-utils": "0.1.8",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "0fc494d4b87cf3dcc4eb1fbe3910a3cbc914e5f5"
|
|
30
30
|
}
|
package/src/BleManager.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
2
2
|
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
3
3
|
|
|
4
|
+
import { onBleKeyMissing, startBleKeyMissingTracking } from './bleKeyMissing';
|
|
4
5
|
import { bleLogger } from './logger';
|
|
5
6
|
|
|
6
7
|
import type { Peripheral } from '@onekeyfe/react-native-ble-utils';
|
|
@@ -32,20 +33,71 @@ export const getBondedDevices = () => BleUtils.getBondedPeripherals();
|
|
|
32
33
|
|
|
33
34
|
export const pairDevice = (macAddress: string) => BleUtils.pairDevice(macAddress);
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Android replaces a bond as BONDING -> NONE, then NONE -> BONDING within milliseconds.
|
|
38
|
+
* The window covers slow broadcast delivery before the first half counts as a failure.
|
|
39
|
+
*/
|
|
40
|
+
export const SYSTEM_BONDING_RESTART_WINDOW_MS = 1500;
|
|
41
|
+
|
|
42
|
+
export type DeviceBondStateOptions = {
|
|
43
|
+
/**
|
|
44
|
+
* The system, not this transport, started the bonding in progress. Android 16+ re-pairs
|
|
45
|
+
* by itself after it detects a lost bond, and a successful re-pair replaces the old bond
|
|
46
|
+
* instead of going straight to BONDED.
|
|
47
|
+
*/
|
|
48
|
+
systemInitiated?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const createBondFailureError = (bondState: Peripheral['bondState']) => {
|
|
52
|
+
const nativeReason =
|
|
53
|
+
'reason' in bondState && typeof bondState.reason === 'number' ? bondState.reason : undefined;
|
|
54
|
+
const reason =
|
|
55
|
+
nativeReason === undefined ? 'unknown' : bondFailureReasons[nativeReason] ?? 'unknown';
|
|
56
|
+
const params = {
|
|
57
|
+
phase: 'bond',
|
|
58
|
+
reason,
|
|
59
|
+
...(nativeReason === undefined ? {} : { nativeReason }),
|
|
60
|
+
};
|
|
61
|
+
if (reason === 'timeout') {
|
|
62
|
+
// Connection timeouts are retried by Core; pairing requires a new user attempt.
|
|
63
|
+
return ERRORS.TypedError(
|
|
64
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
65
|
+
'Bluetooth pairing timed out',
|
|
66
|
+
params
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (reason === 'canceled') {
|
|
70
|
+
return ERRORS.TypedError(
|
|
71
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
72
|
+
'Bluetooth pairing canceled',
|
|
73
|
+
params
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return ERRORS.TypedError(
|
|
77
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
78
|
+
'Bluetooth pairing failed',
|
|
79
|
+
params
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
35
83
|
export const onDeviceBondState = (
|
|
36
84
|
bleMacAddress: string,
|
|
37
|
-
signal?: AbortSignal
|
|
85
|
+
signal?: AbortSignal,
|
|
86
|
+
options?: DeviceBondStateOptions
|
|
38
87
|
): Promise<Peripheral | undefined> =>
|
|
39
88
|
new Promise((resolve, reject) => {
|
|
40
89
|
if (signal?.aborted) {
|
|
41
90
|
reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected));
|
|
42
91
|
return;
|
|
43
92
|
}
|
|
93
|
+
let pendingFailure: ReturnType<typeof setTimeout> | undefined;
|
|
44
94
|
const cleanup = () => {
|
|
45
95
|
if (timeout) {
|
|
46
96
|
clearTimeout(timeout);
|
|
47
97
|
}
|
|
98
|
+
if (pendingFailure) clearTimeout(pendingFailure);
|
|
48
99
|
if (cleanupListener) cleanupListener();
|
|
100
|
+
cleanupKeyMissing?.();
|
|
49
101
|
signal?.removeEventListener('abort', onAbort);
|
|
50
102
|
};
|
|
51
103
|
const onAbort = () => {
|
|
@@ -65,6 +117,20 @@ export const onDeviceBondState = (
|
|
|
65
117
|
);
|
|
66
118
|
}, 60 * 1000);
|
|
67
119
|
|
|
120
|
+
// A failed system re-pair restores the old bond, so it also ends in BONDED. Key
|
|
121
|
+
// missing is what tells it apart from a bond that now works.
|
|
122
|
+
const cleanupKeyMissing = startBleKeyMissingTracking()
|
|
123
|
+
? onBleKeyMissing(bleMacAddress, () => {
|
|
124
|
+
cleanup();
|
|
125
|
+
reject(
|
|
126
|
+
ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
|
|
127
|
+
phase: 'bond',
|
|
128
|
+
reason: 'key_missing',
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
})
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
68
134
|
const cleanupListener = BleUtils.onDeviceBondState(peripheral => {
|
|
69
135
|
if (peripheral.id?.toLowerCase() !== bleMacAddress.toLowerCase()) {
|
|
70
136
|
return;
|
|
@@ -73,49 +139,23 @@ export const onDeviceBondState = (
|
|
|
73
139
|
|
|
74
140
|
const hasBonded = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_BONDED';
|
|
75
141
|
const hasFailed = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_NONE';
|
|
142
|
+
const hasRestarted = bondState.preState === 'BOND_NONE' && bondState.state === 'BOND_BONDING';
|
|
76
143
|
Logger.debug('onDeviceBondState bondState:', bondState);
|
|
77
144
|
if (hasBonded) {
|
|
78
145
|
cleanup();
|
|
79
146
|
resolve(peripheral);
|
|
147
|
+
} else if (hasRestarted && pendingFailure) {
|
|
148
|
+
clearTimeout(pendingFailure);
|
|
149
|
+
pendingFailure = undefined;
|
|
150
|
+
} else if (hasFailed && options?.systemInitiated) {
|
|
151
|
+
if (pendingFailure) clearTimeout(pendingFailure);
|
|
152
|
+
pendingFailure = setTimeout(() => {
|
|
153
|
+
cleanup();
|
|
154
|
+
reject(createBondFailureError(bondState));
|
|
155
|
+
}, SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
80
156
|
} else if (hasFailed) {
|
|
81
157
|
cleanup();
|
|
82
|
-
|
|
83
|
-
'reason' in bondState && typeof bondState.reason === 'number'
|
|
84
|
-
? bondState.reason
|
|
85
|
-
: undefined;
|
|
86
|
-
const reason =
|
|
87
|
-
nativeReason === undefined ? 'unknown' : bondFailureReasons[nativeReason] ?? 'unknown';
|
|
88
|
-
const params = {
|
|
89
|
-
phase: 'bond',
|
|
90
|
-
reason,
|
|
91
|
-
...(nativeReason === undefined ? {} : { nativeReason }),
|
|
92
|
-
};
|
|
93
|
-
if (reason === 'timeout') {
|
|
94
|
-
// Connection timeouts are retried by Core; pairing requires a new user attempt.
|
|
95
|
-
reject(
|
|
96
|
-
ERRORS.TypedError(
|
|
97
|
-
HardwareErrorCode.BleDeviceNotBonded,
|
|
98
|
-
'Bluetooth pairing timed out',
|
|
99
|
-
params
|
|
100
|
-
)
|
|
101
|
-
);
|
|
102
|
-
} else if (reason === 'canceled') {
|
|
103
|
-
reject(
|
|
104
|
-
ERRORS.TypedError(
|
|
105
|
-
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
106
|
-
'Bluetooth pairing canceled',
|
|
107
|
-
params
|
|
108
|
-
)
|
|
109
|
-
);
|
|
110
|
-
} else {
|
|
111
|
-
reject(
|
|
112
|
-
ERRORS.TypedError(
|
|
113
|
-
HardwareErrorCode.BleDeviceNotBonded,
|
|
114
|
-
'Bluetooth pairing failed',
|
|
115
|
-
params
|
|
116
|
-
)
|
|
117
|
-
);
|
|
118
|
-
}
|
|
158
|
+
reject(createBondFailureError(bondState));
|
|
119
159
|
}
|
|
120
160
|
});
|
|
121
161
|
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import EventEmitter from 'events';
|
|
2
|
+
|
|
3
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
4
|
+
|
|
5
|
+
import ReactNativeBleTransport, {
|
|
6
|
+
ANDROID_KEY_MISSING_GRACE_MS,
|
|
7
|
+
ANDROID_KEY_MISSING_LINK_WINDOW_MS,
|
|
8
|
+
} from '../index';
|
|
9
|
+
import { SYSTEM_BONDING_RESTART_WINDOW_MS, onDeviceBondState } from '../BleManager';
|
|
10
|
+
import { stopBleKeyMissingTracking } from '../bleKeyMissing';
|
|
11
|
+
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
12
|
+
|
|
13
|
+
let mockKeyMissingSupported = true;
|
|
14
|
+
let mockEmitKeyMissing: ((event: { id?: string }) => void) | undefined;
|
|
15
|
+
let mockEmitBondState: ((peripheral: unknown) => void) | undefined;
|
|
16
|
+
|
|
17
|
+
jest.mock(
|
|
18
|
+
'react-native',
|
|
19
|
+
() => ({
|
|
20
|
+
Platform: { OS: 'android', select: (spec: Record<string, unknown>) => spec.android },
|
|
21
|
+
PermissionsAndroid: {
|
|
22
|
+
PERMISSIONS: {},
|
|
23
|
+
RESULTS: {},
|
|
24
|
+
request: jest.fn(),
|
|
25
|
+
requestMultiple: jest.fn(),
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
{ virtual: true }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
32
|
+
BleATTErrorCode: { InvalidHandle: 1 },
|
|
33
|
+
BleError: Error,
|
|
34
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
35
|
+
BleManager: jest.fn(),
|
|
36
|
+
ScanMode: { LowLatency: 2 },
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
40
|
+
__esModule: true,
|
|
41
|
+
default: {
|
|
42
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
43
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
44
|
+
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
45
|
+
supportsDeviceKeyMissing: jest.fn(() => mockKeyMissingSupported),
|
|
46
|
+
onDeviceKeyMissing: jest.fn((callback: (event: { id?: string }) => void) => {
|
|
47
|
+
mockEmitKeyMissing = callback;
|
|
48
|
+
return () => {
|
|
49
|
+
mockEmitKeyMissing = undefined;
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
onDeviceBondState: jest.fn((callback: (peripheral: unknown) => void) => {
|
|
53
|
+
mockEmitBondState = callback;
|
|
54
|
+
return () => {
|
|
55
|
+
mockEmitBondState = undefined;
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const UUID = 'CC:06:1C:FD:48:E9';
|
|
62
|
+
|
|
63
|
+
const flush = () =>
|
|
64
|
+
new Promise(resolve => {
|
|
65
|
+
setImmediate(resolve);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const advance = async (ms: number) => {
|
|
69
|
+
jest.advanceTimersByTime(ms);
|
|
70
|
+
await flush();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// What ble-plx reports when the device drops the link while the first write is queued.
|
|
74
|
+
const peerTerminated = () =>
|
|
75
|
+
Promise.reject(
|
|
76
|
+
Object.assign(new Error(`Device ${UUID} was disconnected`), {
|
|
77
|
+
errorCode: 201,
|
|
78
|
+
attErrorCode: 19,
|
|
79
|
+
reason: 'Disconnected with status 19 (GATT_CONN_TERMINATE_PEER_USER)',
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
function createConnectedTransport(linkAgeMs = 0) {
|
|
84
|
+
const t = new ReactNativeBleTransport({});
|
|
85
|
+
t.configure(protocolV1Schema);
|
|
86
|
+
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
87
|
+
(t as any).getCachedTransport = () => ({
|
|
88
|
+
writeCharacteristic: { writeWithoutResponse: jest.fn(peerTerminated) },
|
|
89
|
+
});
|
|
90
|
+
(t as any).androidLinkStartedAt.set(UUID, Date.now() - linkAgeMs);
|
|
91
|
+
return t;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const settle = (promise: Promise<unknown>) => {
|
|
95
|
+
const outcome: { error?: { errorCode?: unknown }; done: boolean } = { done: false };
|
|
96
|
+
promise.then(
|
|
97
|
+
() => {
|
|
98
|
+
outcome.done = true;
|
|
99
|
+
},
|
|
100
|
+
error => {
|
|
101
|
+
outcome.error = error;
|
|
102
|
+
outcome.done = true;
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
return outcome;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
describe('Android key missing', () => {
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
111
|
+
mockKeyMissingSupported = true;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
stopBleKeyMissingTracking();
|
|
116
|
+
jest.clearAllTimers();
|
|
117
|
+
jest.useRealTimers();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('link dropped right after connect', () => {
|
|
121
|
+
test('reports an invalid bond when the system says the keys are missing', async () => {
|
|
122
|
+
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
123
|
+
await flush();
|
|
124
|
+
// The broadcast is delivered separately and can trail the GATT disconnect.
|
|
125
|
+
mockEmitKeyMissing?.({ id: UUID.toLowerCase() });
|
|
126
|
+
await flush();
|
|
127
|
+
|
|
128
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('keeps the original error when no key missing follows', async () => {
|
|
132
|
+
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
133
|
+
await flush();
|
|
134
|
+
expect(outcome.done).toBe(false);
|
|
135
|
+
|
|
136
|
+
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
137
|
+
|
|
138
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('ignores key missing reported for an earlier link', async () => {
|
|
142
|
+
const transport = createConnectedTransport();
|
|
143
|
+
const earlier = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
144
|
+
await flush();
|
|
145
|
+
mockEmitKeyMissing?.({ id: UUID });
|
|
146
|
+
await flush();
|
|
147
|
+
expect(earlier.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
148
|
+
|
|
149
|
+
// The user re-paired; a later link that drops is an ordinary disconnect.
|
|
150
|
+
await advance(1000);
|
|
151
|
+
(transport as any).androidLinkStartedAt.set(UUID, Date.now());
|
|
152
|
+
const later = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
153
|
+
await flush();
|
|
154
|
+
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
155
|
+
|
|
156
|
+
expect(later.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('ignores key missing for another device', async () => {
|
|
160
|
+
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
161
|
+
await flush();
|
|
162
|
+
mockEmitKeyMissing?.({ id: 'E9:B9:F4:DD:DE:CD' });
|
|
163
|
+
await advance(ANDROID_KEY_MISSING_GRACE_MS);
|
|
164
|
+
|
|
165
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('does not wait on a link that has been up for a while', async () => {
|
|
169
|
+
const transport = createConnectedTransport(ANDROID_KEY_MISSING_LINK_WINDOW_MS + 1);
|
|
170
|
+
const outcome = settle(transport.call(UUID, 'GetFeatures', {}));
|
|
171
|
+
await flush();
|
|
172
|
+
|
|
173
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('does not wait when the OS or native build never reports key missing', async () => {
|
|
177
|
+
mockKeyMissingSupported = false;
|
|
178
|
+
const outcome = settle(createConnectedTransport().call(UUID, 'GetFeatures', {}));
|
|
179
|
+
await flush();
|
|
180
|
+
|
|
181
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('acquire over a bond the device no longer holds', () => {
|
|
186
|
+
// Android still reports BONDED, so connect, MTU and GATT setup all succeed and the
|
|
187
|
+
// protocol probe is the first operation to meet the dropped link.
|
|
188
|
+
function createStaleBondHarness() {
|
|
189
|
+
const writeCharacteristic = {
|
|
190
|
+
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
191
|
+
isWritableWithoutResponse: true,
|
|
192
|
+
writeWithoutResponse: jest.fn(peerTerminated),
|
|
193
|
+
};
|
|
194
|
+
const notifyCharacteristic = {
|
|
195
|
+
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
196
|
+
isNotifiable: true,
|
|
197
|
+
monitor: jest.fn(() => ({ remove: jest.fn() })),
|
|
198
|
+
};
|
|
199
|
+
const device = {
|
|
200
|
+
id: UUID,
|
|
201
|
+
name: 'Pro 2 48E9',
|
|
202
|
+
localName: 'Pro 2 48E9',
|
|
203
|
+
mtu: 247,
|
|
204
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
205
|
+
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
206
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
207
|
+
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
208
|
+
characteristicsForService: jest.fn(() =>
|
|
209
|
+
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
210
|
+
),
|
|
211
|
+
services: jest.fn(() => Promise.resolve([])),
|
|
212
|
+
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
213
|
+
};
|
|
214
|
+
const transport = new ReactNativeBleTransport({});
|
|
215
|
+
(transport as any).blePlxManager = {
|
|
216
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
217
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
218
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
219
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
220
|
+
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
221
|
+
setImmediate(() => listener('PoweredOn'));
|
|
222
|
+
return { remove: jest.fn() };
|
|
223
|
+
}),
|
|
224
|
+
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
225
|
+
};
|
|
226
|
+
transport.init(
|
|
227
|
+
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
228
|
+
new EventEmitter()
|
|
229
|
+
);
|
|
230
|
+
transport.configure(protocolV1Schema);
|
|
231
|
+
return { transport, write: writeCharacteristic.writeWithoutResponse };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function acquireUntilSettled(keyMissing: boolean) {
|
|
235
|
+
const { transport, write } = createStaleBondHarness();
|
|
236
|
+
const outcome = settle(transport.acquire({ uuid: UUID }));
|
|
237
|
+
for (let i = 0; i < 100 && !outcome.done; i += 1) {
|
|
238
|
+
// eslint-disable-next-line no-await-in-loop
|
|
239
|
+
await advance(100);
|
|
240
|
+
// The broadcast follows the dropped link, which the probe write is first to meet.
|
|
241
|
+
if (keyMissing && write.mock.calls.length > 0) mockEmitKeyMissing?.({ id: UUID });
|
|
242
|
+
}
|
|
243
|
+
expect(write).toHaveBeenCalled();
|
|
244
|
+
return JSON.parse(JSON.stringify(ERRORS.serializeError({ error: outcome.error })));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
test('ends as an invalid bond that survives the Core response path', async () => {
|
|
248
|
+
await expect(acquireUntilSettled(true)).resolves.toEqual({
|
|
249
|
+
code: HardwareErrorCode.BleBondInvalid,
|
|
250
|
+
error: expect.stringContaining('Forget the device'),
|
|
251
|
+
params: { phase: 'connect', reason: 'key_missing' },
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('keeps the legacy error when the system reports nothing', async () => {
|
|
256
|
+
await expect(acquireUntilSettled(false)).resolves.toMatchObject({
|
|
257
|
+
code: HardwareErrorCode.BleDeviceNotBonded,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('waiting for bonding', () => {
|
|
263
|
+
const bondState = (preState: string, state: string) =>
|
|
264
|
+
mockEmitBondState?.({ id: UUID, bondState: { preState, state } });
|
|
265
|
+
|
|
266
|
+
test('fails as an invalid bond when the system re-pair ends in key missing', async () => {
|
|
267
|
+
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
268
|
+
mockEmitKeyMissing?.({ id: UUID });
|
|
269
|
+
await flush();
|
|
270
|
+
|
|
271
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleBondInvalid);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('follows a system re-pair that replaces the bond', async () => {
|
|
275
|
+
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
276
|
+
bondState('BOND_BONDING', 'BOND_NONE');
|
|
277
|
+
bondState('BOND_NONE', 'BOND_BONDING');
|
|
278
|
+
await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
279
|
+
expect(outcome.done).toBe(false);
|
|
280
|
+
|
|
281
|
+
bondState('BOND_BONDING', 'BOND_BONDED');
|
|
282
|
+
await flush();
|
|
283
|
+
|
|
284
|
+
expect(outcome.done).toBe(true);
|
|
285
|
+
expect(outcome.error).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('fails a system re-pair that ends without a bond', async () => {
|
|
289
|
+
const outcome = settle(onDeviceBondState(UUID, undefined, { systemInitiated: true }));
|
|
290
|
+
bondState('BOND_BONDING', 'BOND_NONE');
|
|
291
|
+
await flush();
|
|
292
|
+
expect(outcome.done).toBe(false);
|
|
293
|
+
|
|
294
|
+
await advance(SYSTEM_BONDING_RESTART_WINDOW_MS);
|
|
295
|
+
|
|
296
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('fails a bonding this transport started as soon as it ends without a bond', async () => {
|
|
300
|
+
const outcome = settle(onDeviceBondState(UUID));
|
|
301
|
+
bondState('BOND_BONDING', 'BOND_NONE');
|
|
302
|
+
await flush();
|
|
303
|
+
|
|
304
|
+
expect(outcome.error?.errorCode).toBe(HardwareErrorCode.BleDeviceNotBonded);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -25,6 +25,14 @@ jest.mock('../BleManager', () => ({
|
|
|
25
25
|
pairDevice: jest.fn(),
|
|
26
26
|
}));
|
|
27
27
|
|
|
28
|
+
// The native facade is replaced wholesale above; key missing is covered by its own suite.
|
|
29
|
+
jest.mock('../bleKeyMissing', () => ({
|
|
30
|
+
isBleKeyMissingSupported: jest.fn(() => false),
|
|
31
|
+
startBleKeyMissingTracking: jest.fn(() => false),
|
|
32
|
+
stopBleKeyMissingTracking: jest.fn(),
|
|
33
|
+
waitForBleKeyMissing: jest.fn(() => Promise.resolve(false)),
|
|
34
|
+
}));
|
|
35
|
+
|
|
28
36
|
jest.mock('../subscribeBleOn', () => ({
|
|
29
37
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
30
38
|
}));
|
|
@@ -45,6 +45,14 @@ jest.mock('../BleManager', () => ({
|
|
|
45
45
|
pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })),
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
// The native facade is replaced wholesale above; key missing is covered by its own suite.
|
|
49
|
+
jest.mock('../bleKeyMissing', () => ({
|
|
50
|
+
isBleKeyMissingSupported: jest.fn(() => false),
|
|
51
|
+
startBleKeyMissingTracking: jest.fn(() => false),
|
|
52
|
+
stopBleKeyMissingTracking: jest.fn(),
|
|
53
|
+
waitForBleKeyMissing: jest.fn(() => Promise.resolve(false)),
|
|
54
|
+
}));
|
|
55
|
+
|
|
48
56
|
jest.mock('../subscribeBleOn', () => ({
|
|
49
57
|
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
50
58
|
}));
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import BleUtils from '@onekeyfe/react-native-ble-utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Android 16+ keeps its side of a bond the device no longer has keys for, and reports it
|
|
6
|
+
* with ACTION_KEY_MISSING instead of a GATT status. The device then drops the link, so
|
|
7
|
+
* without this signal a wiped device is indistinguishable from any other disconnect.
|
|
8
|
+
*
|
|
9
|
+
* The signal only describes the link that was being encrypted when it fired. Callers must
|
|
10
|
+
* bound it to their own connection attempt with `sinceMs`; reusing an older one would turn
|
|
11
|
+
* an ordinary disconnect (a firmware-update reboot, for example) into a terminal bond error.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// An app can still resolve a react-native-ble-utils older than 0.1.8, and the JS bundle can
|
|
15
|
+
// run on a native build that predates the event, so neither method is assumed to exist.
|
|
16
|
+
type KeyMissingCapableBleUtils = Partial<
|
|
17
|
+
Pick<typeof BleUtils, 'supportsDeviceKeyMissing' | 'onDeviceKeyMissing'>
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
type KeyMissingListener = (deviceId: string) => void;
|
|
21
|
+
|
|
22
|
+
const lastKeyMissingAt = new Map<string, number>();
|
|
23
|
+
const listeners = new Set<KeyMissingListener>();
|
|
24
|
+
let unsubscribeNative: (() => void) | undefined;
|
|
25
|
+
|
|
26
|
+
const normalizeDeviceId = (deviceId: string) => deviceId.toLowerCase();
|
|
27
|
+
|
|
28
|
+
const getCapableBleUtils = (): KeyMissingCapableBleUtils | undefined => {
|
|
29
|
+
if (Platform.OS !== 'android') return undefined;
|
|
30
|
+
const bleUtils: KeyMissingCapableBleUtils = BleUtils;
|
|
31
|
+
if (
|
|
32
|
+
typeof bleUtils.supportsDeviceKeyMissing !== 'function' ||
|
|
33
|
+
typeof bleUtils.onDeviceKeyMissing !== 'function'
|
|
34
|
+
) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return bleUtils.supportsDeviceKeyMissing() ? bleUtils : undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** False means the event can never fire here, so nothing should wait for it. */
|
|
41
|
+
export const isBleKeyMissingSupported = () => getCapableBleUtils() !== undefined;
|
|
42
|
+
|
|
43
|
+
/** Idempotent. Returns whether the signal is available on this OS and native build. */
|
|
44
|
+
export const startBleKeyMissingTracking = (): boolean => {
|
|
45
|
+
if (unsubscribeNative) return true;
|
|
46
|
+
const bleUtils = getCapableBleUtils();
|
|
47
|
+
if (!bleUtils?.onDeviceKeyMissing) return false;
|
|
48
|
+
|
|
49
|
+
unsubscribeNative = bleUtils.onDeviceKeyMissing(event => {
|
|
50
|
+
if (typeof event?.id !== 'string') return;
|
|
51
|
+
const deviceId = normalizeDeviceId(event.id);
|
|
52
|
+
lastKeyMissingAt.set(deviceId, Date.now());
|
|
53
|
+
listeners.forEach(listener => listener(deviceId));
|
|
54
|
+
});
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const stopBleKeyMissingTracking = () => {
|
|
59
|
+
unsubscribeNative?.();
|
|
60
|
+
unsubscribeNative = undefined;
|
|
61
|
+
lastKeyMissingAt.clear();
|
|
62
|
+
listeners.clear();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const hasBleKeyMissingSince = (deviceId: string, sinceMs: number): boolean => {
|
|
66
|
+
const at = lastKeyMissingAt.get(normalizeDeviceId(deviceId));
|
|
67
|
+
return at !== undefined && at >= sinceMs;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Calls back for every later signal on this device until the returned cleanup runs. */
|
|
71
|
+
export const onBleKeyMissing = (deviceId: string, callback: () => void): (() => void) => {
|
|
72
|
+
const target = normalizeDeviceId(deviceId);
|
|
73
|
+
const listener: KeyMissingListener = id => {
|
|
74
|
+
if (id === target) callback();
|
|
75
|
+
};
|
|
76
|
+
listeners.add(listener);
|
|
77
|
+
return () => {
|
|
78
|
+
listeners.delete(listener);
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves true once a signal at or after `sinceMs` exists for the device, false when
|
|
84
|
+
* none arrives within `timeoutMs`. The broadcast and the GATT disconnect it explains are
|
|
85
|
+
* delivered on separate paths, so the disconnect can reach JS first.
|
|
86
|
+
*/
|
|
87
|
+
export const waitForBleKeyMissing = (
|
|
88
|
+
deviceId: string,
|
|
89
|
+
sinceMs: number,
|
|
90
|
+
timeoutMs: number
|
|
91
|
+
): Promise<boolean> => {
|
|
92
|
+
if (!startBleKeyMissingTracking()) return Promise.resolve(false);
|
|
93
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs)) return Promise.resolve(true);
|
|
94
|
+
|
|
95
|
+
return new Promise(resolve => {
|
|
96
|
+
const settle = (found: boolean) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(found);
|
|
100
|
+
};
|
|
101
|
+
const cleanup = onBleKeyMissing(deviceId, () => {
|
|
102
|
+
if (hasBleKeyMissingSince(deviceId, sinceMs)) settle(true);
|
|
103
|
+
});
|
|
104
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
105
|
+
});
|
|
106
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,12 @@ import {
|
|
|
47
47
|
getInfosForServiceUuid,
|
|
48
48
|
isSameBleUuid,
|
|
49
49
|
} from './constants';
|
|
50
|
+
import {
|
|
51
|
+
isBleKeyMissingSupported,
|
|
52
|
+
startBleKeyMissingTracking,
|
|
53
|
+
stopBleKeyMissingTracking,
|
|
54
|
+
waitForBleKeyMissing,
|
|
55
|
+
} from './bleKeyMissing';
|
|
50
56
|
import { isNativeBleDisconnectError, toBleDisconnectHardwareError } from './bleNativeDisconnect';
|
|
51
57
|
import {
|
|
52
58
|
isBleStaleBondHardwareError,
|
|
@@ -188,6 +194,28 @@ const shouldRethrowProtocolProbeError = (error: unknown): boolean => {
|
|
|
188
194
|
code === HardwareErrorCode.BleWriteCharacteristicError
|
|
189
195
|
);
|
|
190
196
|
};
|
|
197
|
+
/**
|
|
198
|
+
* Android reports a lost bond (ACTION_KEY_MISSING) while it encrypts a new link, so only
|
|
199
|
+
* a failure this soon after the link started can be explained by it. Later disconnects,
|
|
200
|
+
* such as a firmware-update reboot, must keep their own meaning.
|
|
201
|
+
*/
|
|
202
|
+
export const ANDROID_KEY_MISSING_LINK_WINDOW_MS = 10_000;
|
|
203
|
+
/** The broadcast and the GATT disconnect it explains travel separately; either can land first. */
|
|
204
|
+
export const ANDROID_KEY_MISSING_GRACE_MS = 500;
|
|
205
|
+
/**
|
|
206
|
+
* How a link dropped by a device that refuses a stale bond reaches JS. A wedged write is
|
|
207
|
+
* excluded: the system is still re-pairing then and has not reported key missing yet.
|
|
208
|
+
*/
|
|
209
|
+
const isAndroidLinkLossError = (error: unknown): boolean => {
|
|
210
|
+
const code = (error as { errorCode?: unknown })?.errorCode;
|
|
211
|
+
return (
|
|
212
|
+
isNativeBleDisconnectError(error) ||
|
|
213
|
+
code === HardwareErrorCode.BleDeviceNotBonded ||
|
|
214
|
+
code === HardwareErrorCode.BleDeviceDisconnected ||
|
|
215
|
+
code === HardwareErrorCode.BleCharacteristicNotifyError ||
|
|
216
|
+
code === HardwareErrorCode.BleConnectedError
|
|
217
|
+
);
|
|
218
|
+
};
|
|
191
219
|
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
192
220
|
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
193
221
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
@@ -540,6 +568,9 @@ export default class ReactNativeBleTransport {
|
|
|
540
568
|
*/
|
|
541
569
|
private staleBondErrors: Map<string, Error> = new Map();
|
|
542
570
|
|
|
571
|
+
/** When the current Android link attempt began; bounds which key-missing signals apply. */
|
|
572
|
+
private androidLinkStartedAt: Map<string, number> = new Map();
|
|
573
|
+
|
|
543
574
|
/** Strict or previously confirmed V2 target while acquire installs notifications. */
|
|
544
575
|
private acquiringProtocolV2 = new Set<string>();
|
|
545
576
|
|
|
@@ -1055,7 +1086,13 @@ export default class ReactNativeBleTransport {
|
|
|
1055
1086
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
1056
1087
|
}
|
|
1057
1088
|
|
|
1058
|
-
return this.runLifecycleOperation(uuid, () =>
|
|
1089
|
+
return this.runLifecycleOperation(uuid, async () => {
|
|
1090
|
+
try {
|
|
1091
|
+
return await this.acquireUnlocked(input);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
throw await this.resolveAndroidBondInvalid(uuid, error);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1059
1096
|
}
|
|
1060
1097
|
|
|
1061
1098
|
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
@@ -1166,11 +1203,17 @@ export default class ReactNativeBleTransport {
|
|
|
1166
1203
|
|
|
1167
1204
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1168
1205
|
if (Platform.OS === 'android') {
|
|
1206
|
+
// Subscribe before the link exists: key missing is broadcast while Android encrypts it.
|
|
1207
|
+
startBleKeyMissingTracking();
|
|
1208
|
+
// Failures before the new link starts must not be read against the previous link.
|
|
1209
|
+
this.androidLinkStartedAt.delete(uuid);
|
|
1169
1210
|
// Initiate bonding locally before GATT can trigger peripheral-initiated pairing.
|
|
1170
1211
|
try {
|
|
1171
1212
|
const bondState = await pairDevice(uuid);
|
|
1172
1213
|
if (bondState.bonding) {
|
|
1173
|
-
await onDeviceBondState(uuid, this.bondAbortController.signal
|
|
1214
|
+
await onDeviceBondState(uuid, this.bondAbortController.signal, {
|
|
1215
|
+
systemInitiated: bondState.initiated === false,
|
|
1216
|
+
});
|
|
1174
1217
|
} else if (!bondState.bonded) {
|
|
1175
1218
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
1176
1219
|
}
|
|
@@ -1182,6 +1225,7 @@ export default class ReactNativeBleTransport {
|
|
|
1182
1225
|
});
|
|
1183
1226
|
throw error;
|
|
1184
1227
|
}
|
|
1228
|
+
this.androidLinkStartedAt.set(uuid, Date.now());
|
|
1185
1229
|
}
|
|
1186
1230
|
if (this.stopped) throw ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1187
1231
|
|
|
@@ -1689,16 +1733,44 @@ export default class ReactNativeBleTransport {
|
|
|
1689
1733
|
`Device protocol has not been detected for ${uuid}`
|
|
1690
1734
|
);
|
|
1691
1735
|
}
|
|
1692
|
-
if (protocol === 'V2') {
|
|
1693
|
-
return this.callProtocolV2(uuid, name, data, options);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
1736
|
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1697
|
-
if (this.runPromise && !forceRun) {
|
|
1737
|
+
if (protocol !== 'V2' && this.runPromise && !forceRun) {
|
|
1698
1738
|
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1699
1739
|
}
|
|
1700
1740
|
|
|
1701
|
-
|
|
1741
|
+
try {
|
|
1742
|
+
return protocol === 'V2'
|
|
1743
|
+
? await this.callProtocolV2(uuid, name, data, options)
|
|
1744
|
+
: await this.callProtocolV1(uuid, name, data, options);
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
// An expected-V1 acquire skips the probe, so the first call is what meets the dropped link.
|
|
1747
|
+
throw await this.resolveAndroidBondInvalid(uuid, error);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Android 16+ keeps a bond the device no longer holds keys for. The device drops the link
|
|
1753
|
+
* and the only structured evidence is ACTION_KEY_MISSING, so a link loss right after
|
|
1754
|
+
* connect is re-read as an invalid bond when that signal belongs to the same attempt.
|
|
1755
|
+
*/
|
|
1756
|
+
private async resolveAndroidBondInvalid(uuid: string, error: unknown): Promise<unknown> {
|
|
1757
|
+
if (Platform.OS !== 'android' || !isBleKeyMissingSupported()) return error;
|
|
1758
|
+
if (isBleStaleBondHardwareError(error) || !isAndroidLinkLossError(error)) return error;
|
|
1759
|
+
const linkStartedAt = this.androidLinkStartedAt.get(uuid);
|
|
1760
|
+
if (
|
|
1761
|
+
linkStartedAt === undefined ||
|
|
1762
|
+
Date.now() - linkStartedAt > ANDROID_KEY_MISSING_LINK_WINDOW_MS
|
|
1763
|
+
) {
|
|
1764
|
+
return error;
|
|
1765
|
+
}
|
|
1766
|
+
if (!(await waitForBleKeyMissing(uuid, linkStartedAt, ANDROID_KEY_MISSING_GRACE_MS))) {
|
|
1767
|
+
return error;
|
|
1768
|
+
}
|
|
1769
|
+
Log?.debug('[ReactNativeBleTransport] Android key missing, bond is invalid:', uuid);
|
|
1770
|
+
return ERRORS.TypedError(HardwareErrorCode.BleBondInvalid, undefined, {
|
|
1771
|
+
phase: 'connect',
|
|
1772
|
+
reason: 'key_missing',
|
|
1773
|
+
});
|
|
1702
1774
|
}
|
|
1703
1775
|
|
|
1704
1776
|
private async callProtocolV1(
|
|
@@ -1966,6 +2038,8 @@ export default class ReactNativeBleTransport {
|
|
|
1966
2038
|
this.androidPriorityResetTimers.forEach(timeout => clearTimeout(timeout));
|
|
1967
2039
|
this.androidPriorityResetTimers.clear();
|
|
1968
2040
|
this.androidHighPriorityDevices.clear();
|
|
2041
|
+
this.androidLinkStartedAt.clear();
|
|
2042
|
+
stopBleKeyMissingTracking();
|
|
1969
2043
|
const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceDisconnected);
|
|
1970
2044
|
this.runPromise?.reject(error);
|
|
1971
2045
|
this.runPromise = null;
|