expo-beacon 0.3.2 → 0.4.0
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/README.md +253 -14
- package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +42 -18
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +63 -3
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +172 -12
- package/build/ExpoBeacon.types.d.ts +45 -1
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/build/ExpoBeaconModule.d.ts +75 -1
- package/build/ExpoBeaconModule.d.ts.map +1 -1
- package/build/ExpoBeaconModule.js +1 -9
- package/build/ExpoBeaconModule.js.map +1 -1
- package/build/ExpoBeaconModule.web.d.ts +5 -1
- package/build/ExpoBeaconModule.web.d.ts.map +1 -1
- package/build/ExpoBeaconModule.web.js +4 -0
- package/build/ExpoBeaconModule.web.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/ios/ExpoBeaconModule.swift +408 -39
- package/package.json +1 -1
- package/src/ExpoBeacon.types.ts +50 -1
- package/src/ExpoBeaconModule.ts +32 -11
- package/src/ExpoBeaconModule.web.ts +12 -0
- package/src/index.ts +5 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# expo-beacon
|
|
2
2
|
|
|
3
|
-
An Expo module for scanning, pairing, and monitoring iBeacons in React Native apps.
|
|
3
|
+
An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beacons in React Native apps.
|
|
4
4
|
|
|
5
|
-
- **Scan** for nearby iBeacons via
|
|
5
|
+
- **Scan** for nearby iBeacons and Eddystone beacons via one-shot or continuous BLE scans
|
|
6
6
|
- **Pair** specific beacons for persistent tracking across app restarts
|
|
7
7
|
- **Monitor** paired beacons in the background with enter/exit callbacks
|
|
8
8
|
- **Distance events** fired continuously while a monitored beacon is in range
|
|
@@ -11,7 +11,7 @@ An Expo module for scanning, pairing, and monitoring iBeacons in React Native ap
|
|
|
11
11
|
| Platform | Native implementation |
|
|
12
12
|
| -------- | --------------------------------------------------------------------------------------------------------- |
|
|
13
13
|
| Android | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) (`org.altbeacon:android-beacon-library`) |
|
|
14
|
-
| iOS | CoreLocation (UUID-targeted scans & monitoring) + CoreBluetooth (wildcard scanning)
|
|
14
|
+
| iOS | CoreLocation (UUID-targeted scans & monitoring) + CoreBluetooth (wildcard scanning & Eddystone) |
|
|
15
15
|
| Web | Not supported (throws on all calls) |
|
|
16
16
|
|
|
17
17
|
---
|
|
@@ -46,9 +46,11 @@ In Xcode under **Signing & Capabilities**, enable:
|
|
|
46
46
|
- **Background Modes → Location updates**
|
|
47
47
|
- **Background Modes → Uses Bluetooth LE accessories**
|
|
48
48
|
|
|
49
|
-
> iOS limits apps to **20 simultaneously monitored regions
|
|
49
|
+
> iOS limits apps to **20 simultaneously monitored regions** (iBeacon only — Eddystone beacons are monitored via BLE and do not count toward this limit).
|
|
50
50
|
>
|
|
51
51
|
> **Wildcard scanning**: When you call `scanForBeaconsAsync()` with an empty or omitted `uuids` array, iOS uses CoreBluetooth raw BLE scanning to discover all nearby iBeacons. This works **in the foreground only** — it is an Apple platform limitation. UUID-targeted scans and background monitoring continue to use CoreLocation.
|
|
52
|
+
>
|
|
53
|
+
> **Eddystone scanning**: Eddystone beacons use standard BLE service data (UUID `0xFEAA`), which iOS does not filter. `scanForEddystonesAsync()` and continuous scanning work in the foreground on both platforms.
|
|
52
54
|
|
|
53
55
|
### Android
|
|
54
56
|
|
|
@@ -70,13 +72,16 @@ import type {
|
|
|
70
72
|
BeaconScanResult,
|
|
71
73
|
BeaconRegionEvent,
|
|
72
74
|
BeaconDistanceEvent,
|
|
75
|
+
EddystoneScanResult,
|
|
76
|
+
EddystoneRegionEvent,
|
|
77
|
+
EddystoneDistanceEvent,
|
|
73
78
|
} from "expo-beacon";
|
|
74
79
|
|
|
75
80
|
export default function App() {
|
|
76
81
|
const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);
|
|
77
82
|
|
|
78
83
|
useEffect(() => {
|
|
79
|
-
// 1. Pair a known
|
|
84
|
+
// 1. Pair a known iBeacon for monitoring
|
|
80
85
|
ExpoBeacon.pairBeacon(
|
|
81
86
|
"lobby-entrance",
|
|
82
87
|
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
|
|
@@ -84,7 +89,14 @@ export default function App() {
|
|
|
84
89
|
100,
|
|
85
90
|
);
|
|
86
91
|
|
|
87
|
-
//
|
|
92
|
+
// 1b. Pair a known Eddystone-UID beacon for monitoring
|
|
93
|
+
ExpoBeacon.pairEddystone(
|
|
94
|
+
"meeting-room",
|
|
95
|
+
"edd1ebeac04e5defa017", // 10-byte namespace
|
|
96
|
+
"0123456789ab", // 6-byte instance
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// 2. Subscribe to iBeacon region events
|
|
88
100
|
const enterSub = ExpoBeacon.addListener(
|
|
89
101
|
"onBeaconEnter",
|
|
90
102
|
(e: BeaconRegionEvent) =>
|
|
@@ -99,7 +111,24 @@ export default function App() {
|
|
|
99
111
|
console.log(`${e.identifier}: ${e.distance.toFixed(2)} m`),
|
|
100
112
|
);
|
|
101
113
|
|
|
102
|
-
// 3.
|
|
114
|
+
// 3. Subscribe to Eddystone region events
|
|
115
|
+
const eddyEnterSub = ExpoBeacon.addListener(
|
|
116
|
+
"onEddystoneEnter",
|
|
117
|
+
(e: EddystoneRegionEvent) =>
|
|
118
|
+
console.log(`Eddystone entered ${e.identifier} (${e.namespace})`),
|
|
119
|
+
);
|
|
120
|
+
const eddyExitSub = ExpoBeacon.addListener(
|
|
121
|
+
"onEddystoneExit",
|
|
122
|
+
(e: EddystoneRegionEvent) =>
|
|
123
|
+
console.log(`Eddystone exited ${e.identifier}`),
|
|
124
|
+
);
|
|
125
|
+
const eddyDistSub = ExpoBeacon.addListener(
|
|
126
|
+
"onEddystoneDistance",
|
|
127
|
+
(e: EddystoneDistanceEvent) =>
|
|
128
|
+
console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// 4. Start background monitoring (fires for both paired iBeacons and Eddystones)
|
|
103
132
|
ExpoBeacon.requestPermissionsAsync().then((granted) => {
|
|
104
133
|
if (granted) ExpoBeacon.startMonitoring(10); // enter events within 10 m
|
|
105
134
|
});
|
|
@@ -108,6 +137,9 @@ export default function App() {
|
|
|
108
137
|
enterSub.remove();
|
|
109
138
|
exitSub.remove();
|
|
110
139
|
distSub.remove();
|
|
140
|
+
eddyEnterSub.remove();
|
|
141
|
+
eddyExitSub.remove();
|
|
142
|
+
eddyDistSub.remove();
|
|
111
143
|
ExpoBeacon.stopMonitoring();
|
|
112
144
|
};
|
|
113
145
|
}, []);
|
|
@@ -210,20 +242,24 @@ startContinuousScan(): void
|
|
|
210
242
|
|
|
211
243
|
Begins a **continuous BLE scan** that fires an [`onBeaconFound`](#onbeaconfound) event every time a beacon advertisement is received. Call [`stopContinuousScan()`](#stopcontinuousscan) to end it.
|
|
212
244
|
|
|
213
|
-
Unlike `scanForBeaconsAsync`, this never resolves — it streams results in real time.
|
|
245
|
+
Unlike `scanForBeaconsAsync`, this never resolves — it streams results in real time. Eddystone beacons are also reported via the [`onEddystoneFound`](#oneddystonefound) event.
|
|
214
246
|
|
|
215
|
-
|
|
247
|
+
**iOS note**: Due to CoreLocation API constraints, `startContinuousScan()` on iOS only ranges iBeacons that have been previously paired with `pairBeacon()`. On Android, all nearby BLE beacons are reported regardless of pairing status. Eddystone beacons are discovered on both platforms via CoreBluetooth / AltBeacon regardless of pairing.
|
|
216
248
|
|
|
217
249
|
```ts
|
|
218
250
|
const sub = ExpoBeacon.addListener("onBeaconFound", (beacon) => {
|
|
219
251
|
console.log("Live:", beacon.uuid, beacon.distance);
|
|
220
252
|
});
|
|
253
|
+
const eddySub = ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
|
|
254
|
+
console.log("Eddystone:", beacon.frameType, beacon.namespace ?? beacon.url);
|
|
255
|
+
});
|
|
221
256
|
|
|
222
257
|
ExpoBeacon.startContinuousScan();
|
|
223
258
|
|
|
224
259
|
// later, when done:
|
|
225
260
|
ExpoBeacon.stopContinuousScan();
|
|
226
261
|
sub.remove();
|
|
262
|
+
eddySub.remove();
|
|
227
263
|
```
|
|
228
264
|
|
|
229
265
|
---
|
|
@@ -299,13 +335,99 @@ paired.forEach((b) =>
|
|
|
299
335
|
|
|
300
336
|
---
|
|
301
337
|
|
|
338
|
+
### `scanForEddystonesAsync(scanDurationMs?)`
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Starts a **one-shot BLE scan** for Eddystone beacons, waits for `scanDurationMs` milliseconds, then resolves with all Eddystone beacons discovered.
|
|
345
|
+
|
|
346
|
+
| Parameter | Type | Default | Description |
|
|
347
|
+
| ---------------- | -------- | ------- | ---------------------------------------------- |
|
|
348
|
+
| `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds |
|
|
349
|
+
|
|
350
|
+
Returns an array of [`EddystoneScanResult`](#eddystonescanresult) objects. Discovers both Eddystone-UID and Eddystone-URL frames.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
|
|
354
|
+
eddystones.forEach((b) => {
|
|
355
|
+
if (b.frameType === "uid") {
|
|
356
|
+
console.log(`UID: ns=${b.namespace} inst=${b.instance} dist=${b.distance.toFixed(1)}m`);
|
|
357
|
+
} else {
|
|
358
|
+
console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### `pairEddystone(identifier, namespace, instance)`
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
pairEddystone(identifier: string, namespace: string, instance: string): void
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Registers an Eddystone-UID beacon for persistent region monitoring. Paired Eddystones survive app restarts (stored in `SharedPreferences` on Android, `UserDefaults` on iOS). Calling `pairEddystone` with an existing `identifier` replaces that entry.
|
|
372
|
+
|
|
373
|
+
| Parameter | Type | Description |
|
|
374
|
+
| ------------ | -------- | ---------------------------------------------------------------- |
|
|
375
|
+
| `identifier` | `string` | Your unique label for this beacon (e.g. `"meeting-room"`) |
|
|
376
|
+
| `namespace` | `string` | 10-byte namespace ID as a hex string (20 characters) |
|
|
377
|
+
| `instance` | `string` | 6-byte instance ID as a hex string (12 characters) |
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
ExpoBeacon.pairEddystone(
|
|
381
|
+
"meeting-room",
|
|
382
|
+
"edd1ebeac04e5defa017",
|
|
383
|
+
"0123456789ab",
|
|
384
|
+
);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### `unpairEddystone(identifier)`
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
unpairEddystone(identifier: string): void
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Removes a previously paired Eddystone beacon.
|
|
396
|
+
|
|
397
|
+
| Parameter | Type | Description |
|
|
398
|
+
| ------------ | -------- | ---------------------------------- |
|
|
399
|
+
| `identifier` | `string` | The label used when pairing |
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
ExpoBeacon.unpairEddystone("meeting-room");
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### `getPairedEddystones()`
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
getPairedEddystones(): PairedEddystone[]
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Returns the list of all currently paired Eddystone beacons from persistent storage.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const paired = ExpoBeacon.getPairedEddystones();
|
|
417
|
+
paired.forEach((e) =>
|
|
418
|
+
console.log(e.identifier, e.namespace, e.instance)
|
|
419
|
+
);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
302
424
|
### `startMonitoring(options?)`
|
|
303
425
|
|
|
304
426
|
```ts
|
|
305
427
|
startMonitoring(options?: MonitoringOptions | number): Promise<void>
|
|
306
428
|
```
|
|
307
429
|
|
|
308
|
-
Starts background region monitoring for all paired beacons.
|
|
430
|
+
Starts background region monitoring for all paired beacons (both iBeacons and Eddystones).
|
|
309
431
|
|
|
310
432
|
Accepts either a `MonitoringOptions` object or a plain `number` (backward-compatible shorthand for `maxDistance`).
|
|
311
433
|
|
|
@@ -463,7 +585,7 @@ ExpoBeacon.addListener("onBeaconDistance", (e) => {
|
|
|
463
585
|
|
|
464
586
|
### `onBeaconFound`
|
|
465
587
|
|
|
466
|
-
Fired during a **continuous scan** (started with `startContinuousScan()`) each time
|
|
588
|
+
Fired during a **continuous scan** (started with `startContinuousScan()`) each time an iBeacon advertisement is received.
|
|
467
589
|
|
|
468
590
|
**Payload**: [`BeaconScanResult`](#beaconscanresult)
|
|
469
591
|
|
|
@@ -475,6 +597,66 @@ ExpoBeacon.addListener("onBeaconFound", (b) => {
|
|
|
475
597
|
|
|
476
598
|
---
|
|
477
599
|
|
|
600
|
+
### `onEddystoneFound`
|
|
601
|
+
|
|
602
|
+
Fired during a **continuous scan** (started with `startContinuousScan()`) each time an Eddystone advertisement is received.
|
|
603
|
+
|
|
604
|
+
**Payload**: [`EddystoneScanResult`](#eddystonescanresult)
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
ExpoBeacon.addListener("onEddystoneFound", (b) => {
|
|
608
|
+
if (b.frameType === "uid") {
|
|
609
|
+
console.log(`Eddystone-UID: ${b.namespace}/${b.instance} at ${b.distance.toFixed(1)} m`);
|
|
610
|
+
} else {
|
|
611
|
+
console.log(`Eddystone-URL: ${b.url}`);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
### `onEddystoneEnter`
|
|
619
|
+
|
|
620
|
+
Fired when a paired Eddystone-UID beacon enters range during monitoring. If `maxDistance` was set in `startMonitoring`, this only fires when the measured distance is within that threshold.
|
|
621
|
+
|
|
622
|
+
**Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
ExpoBeacon.addListener("onEddystoneEnter", (e) => {
|
|
626
|
+
console.log(`Eddystone entered "${e.identifier}" (ns: ${e.namespace}) at ~${e.distance.toFixed(1)} m`);
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
### `onEddystoneExit`
|
|
633
|
+
|
|
634
|
+
Fired when a paired Eddystone-UID beacon leaves range during monitoring.
|
|
635
|
+
|
|
636
|
+
**Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
ExpoBeacon.addListener("onEddystoneExit", (e) => {
|
|
640
|
+
console.log(`Eddystone left "${e.identifier}"`);
|
|
641
|
+
});
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
### `onEddystoneDistance`
|
|
647
|
+
|
|
648
|
+
Fired continuously during monitoring whenever a distance update is received for a paired Eddystone beacon (~1 update/sec).
|
|
649
|
+
|
|
650
|
+
**Payload**: [`EddystoneDistanceEvent`](#eddystonedistanceevent)
|
|
651
|
+
|
|
652
|
+
```ts
|
|
653
|
+
ExpoBeacon.addListener("onEddystoneDistance", (e) => {
|
|
654
|
+
console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`);
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
478
660
|
## TypeScript Types
|
|
479
661
|
|
|
480
662
|
### `BeaconScanResult`
|
|
@@ -549,6 +731,61 @@ type BeaconDistanceEvent = {
|
|
|
549
731
|
};
|
|
550
732
|
```
|
|
551
733
|
|
|
734
|
+
### `EddystoneScanResult`
|
|
735
|
+
|
|
736
|
+
Returned by `scanForEddystonesAsync` and used in `onEddystoneFound` events.
|
|
737
|
+
|
|
738
|
+
```ts
|
|
739
|
+
type EddystoneScanResult = {
|
|
740
|
+
frameType: "uid" | "url";
|
|
741
|
+
namespace?: string; // 10-byte hex string (20 chars). Present for UID frames.
|
|
742
|
+
instance?: string; // 6-byte hex string (12 chars). Present for UID frames.
|
|
743
|
+
url?: string; // Decoded URL. Present for URL frames.
|
|
744
|
+
rssi: number; // Signal strength in dBm
|
|
745
|
+
distance: number; // Estimated distance in metres
|
|
746
|
+
txPower: number; // Calibrated TX power
|
|
747
|
+
};
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### `PairedEddystone`
|
|
751
|
+
|
|
752
|
+
Returned by `getPairedEddystones`.
|
|
753
|
+
|
|
754
|
+
```ts
|
|
755
|
+
type PairedEddystone = {
|
|
756
|
+
identifier: string; // Your label
|
|
757
|
+
namespace: string; // 10-byte hex string (20 chars)
|
|
758
|
+
instance: string; // 6-byte hex string (12 chars)
|
|
759
|
+
};
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### `EddystoneRegionEvent`
|
|
763
|
+
|
|
764
|
+
Payload for `onEddystoneEnter` and `onEddystoneExit`.
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
type EddystoneRegionEvent = {
|
|
768
|
+
identifier: string; // Matches PairedEddystone.identifier
|
|
769
|
+
namespace: string;
|
|
770
|
+
instance: string;
|
|
771
|
+
event: "enter" | "exit";
|
|
772
|
+
distance: number; // Measured distance in metres at event time; –1 if unavailable
|
|
773
|
+
};
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### `EddystoneDistanceEvent`
|
|
777
|
+
|
|
778
|
+
Payload for `onEddystoneDistance`.
|
|
779
|
+
|
|
780
|
+
```ts
|
|
781
|
+
type EddystoneDistanceEvent = {
|
|
782
|
+
identifier: string;
|
|
783
|
+
namespace: string;
|
|
784
|
+
instance: string;
|
|
785
|
+
distance: number; // Estimated distance in metres
|
|
786
|
+
};
|
|
787
|
+
```
|
|
788
|
+
|
|
552
789
|
---
|
|
553
790
|
|
|
554
791
|
### `MonitoringOptions`
|
|
@@ -626,15 +863,17 @@ Default scan timing: 1.1 s scan window every 5 s.
|
|
|
626
863
|
|
|
627
864
|
### iOS
|
|
628
865
|
|
|
629
|
-
`startMonitoring()` activates `CLLocationManager` **region monitoring
|
|
866
|
+
`startMonitoring()` activates `CLLocationManager` **region monitoring** for paired iBeacons. iOS can wake or relaunch the app when the device crosses a region boundary, even if the app has been force-quit. `allowsBackgroundLocationUpdates` is `true` and `pausesLocationUpdatesAutomatically` is `false`.
|
|
867
|
+
|
|
868
|
+
For paired Eddystone beacons, iOS uses **CoreBluetooth BLE scanning** during monitoring. BLE scanning works reliably in the foreground and while the app is backgrounded with `Uses Bluetooth LE accessories` background mode enabled. However, BLE scanning may be throttled or stopped by iOS when the app is suspended.
|
|
630
869
|
|
|
631
|
-
> iOS limits apps to **20 simultaneously monitored regions
|
|
870
|
+
> iOS limits apps to **20 simultaneously monitored regions** (iBeacon only — Eddystone monitoring does not count toward this limit).
|
|
632
871
|
|
|
633
872
|
---
|
|
634
873
|
|
|
635
874
|
## Notifications
|
|
636
875
|
|
|
637
|
-
A local notification is posted for every
|
|
876
|
+
A local notification is posted for every beacon enter and exit event (both iBeacon and Eddystone). All notification settings can be customised via [`setNotificationConfig()`](#setnotificationconfigconfig) or inline in [`startMonitoring(options)`](#startmonitoringoptions).
|
|
638
877
|
|
|
639
878
|
### Defaults
|
|
640
879
|
|
|
@@ -16,26 +16,50 @@ class BeaconEventReceiver(
|
|
|
16
16
|
if (intent.action != ACTION_BEACON_EVENT) return
|
|
17
17
|
|
|
18
18
|
val identifier = intent.getStringExtra("identifier") ?: return
|
|
19
|
-
val uuid = intent.getStringExtra("uuid") ?: ""
|
|
20
|
-
val major = intent.getIntExtra("major", 0)
|
|
21
|
-
val minor = intent.getIntExtra("minor", 0)
|
|
22
19
|
val eventType = intent.getStringExtra("event") ?: return
|
|
20
|
+
val beaconType = intent.getStringExtra("beaconType") ?: "ibeacon"
|
|
21
|
+
val distance = intent.getDoubleExtra("distance", -1.0)
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
if (beaconType == "eddystone") {
|
|
24
|
+
val namespace = intent.getStringExtra("namespace") ?: ""
|
|
25
|
+
val instance = intent.getStringExtra("instance") ?: ""
|
|
26
|
+
|
|
27
|
+
val params = mapOf(
|
|
28
|
+
"identifier" to identifier,
|
|
29
|
+
"namespace" to namespace,
|
|
30
|
+
"instance" to instance,
|
|
31
|
+
"event" to eventType,
|
|
32
|
+
"distance" to distance
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
val eventName = when (eventType) {
|
|
36
|
+
"enter" -> "onEddystoneEnter"
|
|
37
|
+
"exit" -> "onEddystoneExit"
|
|
38
|
+
"distance" -> "onEddystoneDistance"
|
|
39
|
+
else -> return
|
|
40
|
+
}
|
|
41
|
+
onEvent(eventName, params)
|
|
42
|
+
} else {
|
|
43
|
+
val uuid = intent.getStringExtra("uuid") ?: ""
|
|
44
|
+
val major = intent.getIntExtra("major", 0)
|
|
45
|
+
val minor = intent.getIntExtra("minor", 0)
|
|
46
|
+
|
|
47
|
+
val params = mapOf(
|
|
48
|
+
"identifier" to identifier,
|
|
49
|
+
"uuid" to uuid,
|
|
50
|
+
"major" to major,
|
|
51
|
+
"minor" to minor,
|
|
52
|
+
"event" to eventType,
|
|
53
|
+
"distance" to distance
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
val eventName = when (eventType) {
|
|
57
|
+
"enter" -> "onBeaconEnter"
|
|
58
|
+
"exit" -> "onBeaconExit"
|
|
59
|
+
"distance" -> "onBeaconDistance"
|
|
60
|
+
else -> return
|
|
61
|
+
}
|
|
62
|
+
onEvent(eventName, params)
|
|
38
63
|
}
|
|
39
|
-
onEvent(eventName, params)
|
|
40
64
|
}
|
|
41
65
|
}
|
|
@@ -16,6 +16,8 @@ import org.json.JSONArray
|
|
|
16
16
|
|
|
17
17
|
private const val PREFS_NAME = "expo.beacon.paired"
|
|
18
18
|
private const val PREFS_KEY = "paired_beacons"
|
|
19
|
+
private const val EDDYSTONE_PREFS_NAME = "expo.beacon.paired_eddystones"
|
|
20
|
+
private const val EDDYSTONE_PREFS_KEY = "paired_eddystones"
|
|
19
21
|
private const val CHANNEL_ID = "expo_beacon_channel"
|
|
20
22
|
private const val FOREGROUND_NOTIF_ID = 1001
|
|
21
23
|
private const val ENTER_EXIT_NOTIF_BASE_ID = 2000
|
|
@@ -84,6 +86,18 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
84
86
|
BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25")
|
|
85
87
|
)
|
|
86
88
|
}
|
|
89
|
+
// Register Eddystone-UID parser
|
|
90
|
+
if (manager.beaconParsers.none { it.layout?.contains("s:0-1=feaa,m:2-2=00") == true }) {
|
|
91
|
+
manager.beaconParsers.add(
|
|
92
|
+
BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19")
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
// Register Eddystone-URL parser
|
|
96
|
+
if (manager.beaconParsers.none { it.layout?.contains("s:0-1=feaa,m:2-2=10") == true }) {
|
|
97
|
+
manager.beaconParsers.add(
|
|
98
|
+
BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v")
|
|
99
|
+
)
|
|
100
|
+
}
|
|
87
101
|
// Use continuous scanning (not JobScheduler) for foreground service
|
|
88
102
|
// Guard: throws if called after ranging/monitoring has already started
|
|
89
103
|
try { manager.setEnableScheduledScanJobs(false) } catch (_: IllegalStateException) {}
|
|
@@ -126,6 +140,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
126
140
|
val json = prefs.getString(PREFS_KEY, "[]") ?: "[]"
|
|
127
141
|
val beacons = try { JSONArray(json) } catch (_: Exception) { JSONArray() }
|
|
128
142
|
|
|
143
|
+
// Load paired Eddystones
|
|
144
|
+
val eddystonePrefs: SharedPreferences = getSharedPreferences(EDDYSTONE_PREFS_NAME, Context.MODE_PRIVATE)
|
|
145
|
+
val eddystoneJson = eddystonePrefs.getString(EDDYSTONE_PREFS_KEY, "[]") ?: "[]"
|
|
146
|
+
val eddystones = try { JSONArray(eddystoneJson) } catch (_: Exception) { JSONArray() }
|
|
147
|
+
|
|
129
148
|
// Stop previous regions and distance-log ranging
|
|
130
149
|
distanceLogRegions.forEach {
|
|
131
150
|
try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
@@ -136,6 +155,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
136
155
|
}
|
|
137
156
|
monitoredRegions.clear()
|
|
138
157
|
|
|
158
|
+
// iBeacon regions
|
|
139
159
|
for (i in 0 until beacons.length()) {
|
|
140
160
|
val b = beacons.getJSONObject(i)
|
|
141
161
|
val region = Region(
|
|
@@ -160,6 +180,34 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
160
180
|
}
|
|
161
181
|
}
|
|
162
182
|
}
|
|
183
|
+
|
|
184
|
+
// Eddystone-UID regions
|
|
185
|
+
for (i in 0 until eddystones.length()) {
|
|
186
|
+
val e = eddystones.getJSONObject(i)
|
|
187
|
+
val identifier = e.getString("identifier")
|
|
188
|
+
val namespace = e.getString("namespace")
|
|
189
|
+
val instance = e.getString("instance")
|
|
190
|
+
val region = Region(
|
|
191
|
+
identifier,
|
|
192
|
+
Identifier.parse("0x$namespace"),
|
|
193
|
+
Identifier.parse("0x$instance"),
|
|
194
|
+
null
|
|
195
|
+
)
|
|
196
|
+
monitoredRegions.add(region)
|
|
197
|
+
try {
|
|
198
|
+
beaconManager.startMonitoringBeaconsInRegion(region)
|
|
199
|
+
} catch (ex: RemoteException) {
|
|
200
|
+
ex.printStackTrace()
|
|
201
|
+
}
|
|
202
|
+
if (distanceLogRegions.add(region)) {
|
|
203
|
+
try {
|
|
204
|
+
beaconManager.startRangingBeaconsInRegion(region)
|
|
205
|
+
} catch (ex: RemoteException) {
|
|
206
|
+
distanceLogRegions.remove(region)
|
|
207
|
+
ex.printStackTrace()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
163
211
|
}
|
|
164
212
|
|
|
165
213
|
private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
|
|
@@ -284,13 +332,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
284
332
|
}
|
|
285
333
|
|
|
286
334
|
private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double) {
|
|
335
|
+
// Determine if this is an Eddystone region based on identifier format
|
|
336
|
+
// Eddystone regions have id1 as a hex namespace (not a UUID)
|
|
337
|
+
val id1Str = region.id1?.toString() ?: ""
|
|
338
|
+
val isEddystone = id1Str.startsWith("0x")
|
|
339
|
+
|
|
287
340
|
val intent = Intent(ACTION_BEACON_EVENT).apply {
|
|
288
341
|
putExtra("identifier", region.uniqueId)
|
|
289
|
-
putExtra("uuid", region.id1?.toString() ?: "")
|
|
290
|
-
putExtra("major", region.id2?.toInt() ?: 0)
|
|
291
|
-
putExtra("minor", region.id3?.toInt() ?: 0)
|
|
292
342
|
putExtra("event", eventType)
|
|
293
343
|
putExtra("distance", distance)
|
|
344
|
+
if (isEddystone) {
|
|
345
|
+
putExtra("beaconType", "eddystone")
|
|
346
|
+
putExtra("namespace", id1Str.removePrefix("0x"))
|
|
347
|
+
putExtra("instance", region.id2?.toString()?.removePrefix("0x") ?: "")
|
|
348
|
+
} else {
|
|
349
|
+
putExtra("beaconType", "ibeacon")
|
|
350
|
+
putExtra("uuid", id1Str)
|
|
351
|
+
putExtra("major", region.id2?.toInt() ?: 0)
|
|
352
|
+
putExtra("minor", region.id3?.toInt() ?: 0)
|
|
353
|
+
}
|
|
294
354
|
setPackage(packageName)
|
|
295
355
|
}
|
|
296
356
|
sendBroadcast(intent)
|