expo-beacon 0.5.6 → 0.6.2
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 +283 -24
- package/android/src/main/java/expo/modules/beacon/BeaconEventLogger.kt +98 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventReceiver.kt +2 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +57 -0
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +61 -6
- package/build/ExpoBeacon.types.d.ts +54 -0
- package/build/ExpoBeacon.types.d.ts.map +1 -1
- package/build/ExpoBeacon.types.js.map +1 -1
- package/build/ExpoBeaconModule.d.ts +16 -3
- package/build/ExpoBeaconModule.d.ts.map +1 -1
- package/build/ExpoBeaconModule.js.map +1 -1
- package/build/ExpoBeaconModule.web.d.ts +6 -1
- package/build/ExpoBeaconModule.web.d.ts.map +1 -1
- package/build/ExpoBeaconModule.web.js +5 -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/BeaconEventLogger.swift +137 -0
- package/ios/ExpoBeaconModule.swift +131 -14
- package/package.json +1 -1
- package/src/ExpoBeacon.types.ts +58 -0
- package/src/ExpoBeaconModule.ts +22 -0
- package/src/ExpoBeaconModule.web.ts +7 -0
- package/src/index.ts +4 -0
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
8
8
|
| **Pair** | Register specific beacons for persistent tracking — survives app restarts |
|
|
9
9
|
| **Monitor** | Background enter/exit region detection with distance-based filtering |
|
|
10
10
|
| **Distance** | Real-time distance updates (~1/sec) while monitoring |
|
|
11
|
+
| **Timeout** | Fire a one-shot event after a beacon stays in range for a configured duration |
|
|
12
|
+
| **Event Logging** | Persist every beacon event to a local SQLite database for diagnostics & replay |
|
|
11
13
|
| **Notifications** | Automatic local notifications on region enter/exit, fully customisable |
|
|
12
14
|
|
|
13
15
|
| Platform | Native Implementation |
|
|
@@ -32,6 +34,8 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
32
34
|
- [Pairing & Unpairing Beacons](#pairing--unpairing-beacons)
|
|
33
35
|
- [Background Monitoring](#background-monitoring)
|
|
34
36
|
- [Customizing Notifications](#customizing-notifications)
|
|
37
|
+
- [Beacon Timeout](#beacon-timeout)
|
|
38
|
+
- [Event Logging](#event-logging)
|
|
35
39
|
- [Cancelling a Scan](#cancelling-a-scan)
|
|
36
40
|
- [Full API Reference](#full-api-reference)
|
|
37
41
|
- [requestPermissionsAsync()](#requestpermissionsasync)
|
|
@@ -49,6 +53,11 @@ An Expo module for scanning, pairing, and monitoring **iBeacons** and **Eddyston
|
|
|
49
53
|
- [startMonitoring()](#startmonitoringoptions)
|
|
50
54
|
- [stopMonitoring()](#stopmonitoring)
|
|
51
55
|
- [setNotificationConfig()](#setnotificationconfigconfig)
|
|
56
|
+
- [enableEventLogging()](#enableeventlogging)
|
|
57
|
+
- [disableEventLogging()](#disableeventlogging)
|
|
58
|
+
- [getEventLogs()](#geteventlogsoptions)
|
|
59
|
+
- [clearEventLogs()](#cleareventlogs)
|
|
60
|
+
- [destroyEventLogs()](#destroyeventlogs)
|
|
52
61
|
- [Events](#events)
|
|
53
62
|
- [TypeScript Types](#typescript-types)
|
|
54
63
|
- [Background Behaviour](#background-behaviour)
|
|
@@ -551,6 +560,103 @@ await ExpoBeacon.startMonitoring({
|
|
|
551
560
|
|
|
552
561
|
---
|
|
553
562
|
|
|
563
|
+
### Beacon Timeout
|
|
564
|
+
|
|
565
|
+
Pair a beacon with `timeoutSeconds` to fire a one-shot event after the beacon has been continuously in range for that duration. The timer resets if the beacon exits and re-enters range.
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
import { useEffect } from "react";
|
|
569
|
+
import ExpoBeacon from "expo-beacon";
|
|
570
|
+
import type { BeaconTimeoutEvent, EddystoneTimeoutEvent } from "expo-beacon";
|
|
571
|
+
|
|
572
|
+
// Pair with a 30-second timeout
|
|
573
|
+
ExpoBeacon.pairBeacon(
|
|
574
|
+
"lobby-entrance",
|
|
575
|
+
"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
|
|
576
|
+
1,
|
|
577
|
+
100,
|
|
578
|
+
undefined, // name (optional)
|
|
579
|
+
30, // timeoutSeconds — fires after 30 s in range
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
// Pair Eddystone with a 60-second timeout
|
|
583
|
+
ExpoBeacon.pairEddystone(
|
|
584
|
+
"meeting-room",
|
|
585
|
+
"edd1ebeac04e5defa017",
|
|
586
|
+
"0123456789ab",
|
|
587
|
+
undefined, // name (optional)
|
|
588
|
+
60, // timeoutSeconds — fires after 60 s in range
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
// Listen for the timeout events
|
|
592
|
+
useEffect(() => {
|
|
593
|
+
const beaconTimeout = ExpoBeacon.addListener(
|
|
594
|
+
"onBeaconTimeout",
|
|
595
|
+
(e: BeaconTimeoutEvent) => {
|
|
596
|
+
console.log(`Beacon "${e.identifier}" in range for configured duration! dist: ${e.distance.toFixed(1)}m`);
|
|
597
|
+
},
|
|
598
|
+
);
|
|
599
|
+
const eddystoneTimeout = ExpoBeacon.addListener(
|
|
600
|
+
"onEddystoneTimeout",
|
|
601
|
+
(e: EddystoneTimeoutEvent) => {
|
|
602
|
+
console.log(`Eddystone "${e.identifier}" in range for configured duration!`);
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
return () => {
|
|
607
|
+
beaconTimeout.remove();
|
|
608
|
+
eddystoneTimeout.remove();
|
|
609
|
+
};
|
|
610
|
+
}, []);
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
> **Note**: The timeout fires once per enter cycle. If the beacon exits and re-enters range, the timer starts over.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
### Event Logging
|
|
618
|
+
|
|
619
|
+
Enable SQLite-backed event logging to persist every beacon event locally. Useful for diagnostics, debugging, and replaying event history.
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
import ExpoBeacon from "expo-beacon";
|
|
623
|
+
import type { EventLogEntry, EventLogQueryOptions } from "expo-beacon";
|
|
624
|
+
|
|
625
|
+
// Enable logging — creates/opens the SQLite database
|
|
626
|
+
ExpoBeacon.enableEventLogging();
|
|
627
|
+
|
|
628
|
+
// ... scanning, monitoring, etc. — all events are now persisted automatically ...
|
|
629
|
+
|
|
630
|
+
// Query all recent events
|
|
631
|
+
const logs: EventLogEntry[] = ExpoBeacon.getEventLogs();
|
|
632
|
+
console.log(logs);
|
|
633
|
+
// [
|
|
634
|
+
// { id: 42, timestamp: 1712345678000, eventType: "onBeaconEnter",
|
|
635
|
+
// identifier: "lobby", data: { uuid: "E2C5…", major: 1, minor: 100, ... } },
|
|
636
|
+
// ...
|
|
637
|
+
// ]
|
|
638
|
+
|
|
639
|
+
// Filter by event type and time range
|
|
640
|
+
const enterLogs = ExpoBeacon.getEventLogs({
|
|
641
|
+
eventType: "onBeaconEnter",
|
|
642
|
+
sinceTimestamp: Date.now() - 3600_000, // last hour
|
|
643
|
+
limit: 100,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Disable logging (retains existing data)
|
|
647
|
+
ExpoBeacon.disableEventLogging();
|
|
648
|
+
|
|
649
|
+
// Clear all logged events (keeps the database)
|
|
650
|
+
ExpoBeacon.clearEventLogs();
|
|
651
|
+
|
|
652
|
+
// Destroy the database entirely (also disables logging)
|
|
653
|
+
ExpoBeacon.destroyEventLogs();
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
> **Storage**: Events are stored in a local SQLite database (`expo_beacon_events.db`). No external dependencies are required — Android uses the built-in SQLite, iOS uses the system `libsqlite3`.
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
554
660
|
### Cancelling a Scan
|
|
555
661
|
|
|
556
662
|
Cancel any in-progress one-shot scan (iBeacon or Eddystone). The pending promise will reject with error code `SCAN_CANCELLED`.
|
|
@@ -708,10 +814,10 @@ Cancels any in-progress one-shot scan (iBeacon or Eddystone). The pending promis
|
|
|
708
814
|
|
|
709
815
|
---
|
|
710
816
|
|
|
711
|
-
### `pairBeacon(identifier, uuid, major, minor)`
|
|
817
|
+
### `pairBeacon(identifier, uuid, major, minor, name?, timeoutSeconds?)`
|
|
712
818
|
|
|
713
819
|
```ts
|
|
714
|
-
pairBeacon(identifier: string, uuid: string, major: number, minor: number): void
|
|
820
|
+
pairBeacon(identifier: string, uuid: string, major: number, minor: number, name?: string, timeoutSeconds?: number): void
|
|
715
821
|
```
|
|
716
822
|
|
|
717
823
|
Registers an iBeacon for persistent monitoring.
|
|
@@ -722,11 +828,16 @@ Registers an iBeacon for persistent monitoring.
|
|
|
722
828
|
| `uuid` | `string` | iBeacon proximity UUID (case-insensitive, e.g. `"E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"`) |
|
|
723
829
|
| `major` | `number` | Major value: `0`–`65535` |
|
|
724
830
|
| `minor` | `number` | Minor value: `0`–`65535` |
|
|
831
|
+
| `name` | `string?` | Optional BLE device name for display purposes |
|
|
832
|
+
| `timeoutSeconds` | `number?` | Fire `onBeaconTimeout` once after the beacon stays in range this many seconds. Timer resets on exit/re-enter. |
|
|
725
833
|
|
|
726
834
|
**Possible errors**: `INVALID_UUID`, `INVALID_MAJOR`, `INVALID_MINOR`.
|
|
727
835
|
|
|
728
836
|
```ts
|
|
729
837
|
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42);
|
|
838
|
+
|
|
839
|
+
// With timeout — fires onBeaconTimeout after 30 s in range
|
|
840
|
+
ExpoBeacon.pairBeacon("main-door", "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0", 1, 42, undefined, 30);
|
|
730
841
|
```
|
|
731
842
|
|
|
732
843
|
---
|
|
@@ -764,10 +875,10 @@ const paired = ExpoBeacon.getPairedBeacons();
|
|
|
764
875
|
|
|
765
876
|
---
|
|
766
877
|
|
|
767
|
-
### `pairEddystone(identifier, namespace, instance)`
|
|
878
|
+
### `pairEddystone(identifier, namespace, instance, name?, timeoutSeconds?)`
|
|
768
879
|
|
|
769
880
|
```ts
|
|
770
|
-
pairEddystone(identifier: string, namespace: string, instance: string): void
|
|
881
|
+
pairEddystone(identifier: string, namespace: string, instance: string, name?: string, timeoutSeconds?: number): void
|
|
771
882
|
```
|
|
772
883
|
|
|
773
884
|
Registers an Eddystone-UID beacon for persistent monitoring.
|
|
@@ -777,11 +888,16 @@ Registers an Eddystone-UID beacon for persistent monitoring.
|
|
|
777
888
|
| `identifier` | `string` | Unique label (e.g. `"meeting-room"`) |
|
|
778
889
|
| `namespace` | `string` | 10-byte namespace ID as hex string — must be exactly **20 hex characters** |
|
|
779
890
|
| `instance` | `string` | 6-byte instance ID as hex string — must be exactly **12 hex characters** |
|
|
891
|
+
| `name` | `string?` | Optional BLE device name for display purposes |
|
|
892
|
+
| `timeoutSeconds` | `number?` | Fire `onEddystoneTimeout` once after the beacon stays in range this many seconds. Timer resets on exit/re-enter. |
|
|
780
893
|
|
|
781
894
|
**Possible errors**: `INVALID_NAMESPACE`, `INVALID_INSTANCE`.
|
|
782
895
|
|
|
783
896
|
```ts
|
|
784
897
|
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab");
|
|
898
|
+
|
|
899
|
+
// With timeout — fires onEddystoneTimeout after 60 s in range
|
|
900
|
+
ExpoBeacon.pairEddystone("meeting-room", "edd1ebeac04e5defa017", "0123456789ab", undefined, 60);
|
|
785
901
|
```
|
|
786
902
|
|
|
787
903
|
---
|
|
@@ -897,27 +1013,82 @@ For one-off overrides, pass `notifications` inside `startMonitoring(options)` in
|
|
|
897
1013
|
|
|
898
1014
|
See [`NotificationConfig`](#notificationconfig) for the full shape.
|
|
899
1015
|
|
|
1016
|
+
---
|
|
1017
|
+
|
|
1018
|
+
### `enableEventLogging()`
|
|
1019
|
+
|
|
900
1020
|
```ts
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1021
|
+
enableEventLogging(): void
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
Creates/opens the local SQLite database and starts persisting **every beacon event** (`onBeaconEnter`, `onBeaconExit`, `onBeaconDistance`, `onBeaconTimeout`, `onBeaconFound`, `onEddystoneEnter`, etc.). Call before `startMonitoring()` or `startContinuousScan()`.
|
|
1025
|
+
|
|
1026
|
+
```ts
|
|
1027
|
+
ExpoBeacon.enableEventLogging();
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
### `disableEventLogging()`
|
|
1033
|
+
|
|
1034
|
+
```ts
|
|
1035
|
+
disableEventLogging(): void
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Stops persisting events. Previously logged data is **retained** — call `clearEventLogs()` or `destroyEventLogs()` to remove it.
|
|
1039
|
+
|
|
1040
|
+
```ts
|
|
1041
|
+
ExpoBeacon.disableEventLogging();
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
---
|
|
1045
|
+
|
|
1046
|
+
### `getEventLogs(options?)`
|
|
1047
|
+
|
|
1048
|
+
```ts
|
|
1049
|
+
getEventLogs(options?: EventLogQueryOptions): EventLogEntry[]
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Retrieves logged events from the SQLite database, newest first.
|
|
1053
|
+
|
|
1054
|
+
| Property | Type | Default | Description |
|
|
1055
|
+
|---|---|---|---|
|
|
1056
|
+
| `limit` | `number` | `1000` | Max rows to return (capped at 10 000) |
|
|
1057
|
+
| `eventType` | `string` | `undefined` | Filter by event name (e.g. `"onBeaconEnter"`) |
|
|
1058
|
+
| `sinceTimestamp` | `number` | `undefined` | Only events with `timestamp >= value` (ms since epoch) |
|
|
1059
|
+
|
|
1060
|
+
**Returns**: `EventLogEntry[]`
|
|
1061
|
+
|
|
1062
|
+
```ts
|
|
1063
|
+
const logs = ExpoBeacon.getEventLogs({ eventType: "onBeaconEnter", limit: 50 });
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
---
|
|
1067
|
+
|
|
1068
|
+
### `clearEventLogs()`
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
clearEventLogs(): void
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
Deletes all rows from the event log table. The database file remains.
|
|
1075
|
+
|
|
1076
|
+
```ts
|
|
1077
|
+
ExpoBeacon.clearEventLogs();
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
---
|
|
1081
|
+
|
|
1082
|
+
### `destroyEventLogs()`
|
|
1083
|
+
|
|
1084
|
+
```ts
|
|
1085
|
+
destroyEventLogs(): void
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Disables logging **and** deletes the entire SQLite database file.
|
|
1089
|
+
|
|
1090
|
+
```ts
|
|
1091
|
+
ExpoBeacon.destroyEventLogs();
|
|
921
1092
|
```
|
|
922
1093
|
|
|
923
1094
|
---
|
|
@@ -944,6 +1115,8 @@ sub.remove();
|
|
|
944
1115
|
| `onEddystoneEnter` | Paired Eddystone enters range (respects `maxDistance`) | `EddystoneRegionEvent` |
|
|
945
1116
|
| `onEddystoneExit` | Paired Eddystone leaves range (always fires) | `EddystoneRegionEvent` |
|
|
946
1117
|
| `onEddystoneDistance` | Periodic Eddystone distance update during monitoring | `EddystoneDistanceEvent` |
|
|
1118
|
+
| `onBeaconTimeout` | Paired iBeacon in range for configured `timeoutSeconds` | `BeaconTimeoutEvent` |
|
|
1119
|
+
| `onEddystoneTimeout` | Paired Eddystone in range for configured `timeoutSeconds` | `EddystoneTimeoutEvent` |
|
|
947
1120
|
|
|
948
1121
|
### Event Detail
|
|
949
1122
|
|
|
@@ -1038,6 +1211,31 @@ ExpoBeacon.addListener("onEddystoneDistance", (e) => {
|
|
|
1038
1211
|
});
|
|
1039
1212
|
```
|
|
1040
1213
|
|
|
1214
|
+
#### `onBeaconTimeout`
|
|
1215
|
+
|
|
1216
|
+
Fired **once** when a paired iBeacon has been continuously in range for its configured `timeoutSeconds` duration. The timer resets on exit/re-enter.
|
|
1217
|
+
|
|
1218
|
+
```ts
|
|
1219
|
+
ExpoBeacon.addListener("onBeaconTimeout", (e) => {
|
|
1220
|
+
// e.identifier — "lobby-entrance"
|
|
1221
|
+
// e.uuid, e.major, e.minor — beacon identity
|
|
1222
|
+
// e.distance — metres at the moment the timeout fired
|
|
1223
|
+
console.log(`Beacon "${e.identifier}" timeout — in range for configured duration`);
|
|
1224
|
+
});
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
#### `onEddystoneTimeout`
|
|
1228
|
+
|
|
1229
|
+
Fired **once** when a paired Eddystone has been continuously in range for its configured `timeoutSeconds` duration.
|
|
1230
|
+
|
|
1231
|
+
```ts
|
|
1232
|
+
ExpoBeacon.addListener("onEddystoneTimeout", (e) => {
|
|
1233
|
+
// e.identifier, e.namespace, e.instance — Eddystone identity
|
|
1234
|
+
// e.distance — metres at the moment the timeout fired (–1 if unavailable)
|
|
1235
|
+
console.log(`Eddystone "${e.identifier}" timeout`);
|
|
1236
|
+
});
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1041
1239
|
---
|
|
1042
1240
|
|
|
1043
1241
|
## TypeScript Types
|
|
@@ -1050,17 +1248,21 @@ import type {
|
|
|
1050
1248
|
PairedBeacon,
|
|
1051
1249
|
BeaconRegionEvent,
|
|
1052
1250
|
BeaconDistanceEvent,
|
|
1251
|
+
BeaconTimeoutEvent,
|
|
1053
1252
|
EddystoneFrameType,
|
|
1054
1253
|
EddystoneScanResult,
|
|
1055
1254
|
PairedEddystone,
|
|
1056
1255
|
EddystoneRegionEvent,
|
|
1057
1256
|
EddystoneDistanceEvent,
|
|
1257
|
+
EddystoneTimeoutEvent,
|
|
1058
1258
|
ExpoBeaconModuleEvents,
|
|
1059
1259
|
MonitoringOptions,
|
|
1060
1260
|
NotificationConfig,
|
|
1061
1261
|
BeaconNotificationConfig,
|
|
1062
1262
|
ForegroundServiceConfig,
|
|
1063
1263
|
NotificationChannelConfig,
|
|
1264
|
+
EventLogQueryOptions,
|
|
1265
|
+
EventLogEntry,
|
|
1064
1266
|
} from "expo-beacon";
|
|
1065
1267
|
```
|
|
1066
1268
|
|
|
@@ -1089,6 +1291,8 @@ type PairedBeacon = {
|
|
|
1089
1291
|
uuid: string;
|
|
1090
1292
|
major: number;
|
|
1091
1293
|
minor: number;
|
|
1294
|
+
name?: string; // Optional BLE device name
|
|
1295
|
+
timeoutSeconds?: number; // Fires onBeaconTimeout after this duration in range
|
|
1092
1296
|
};
|
|
1093
1297
|
```
|
|
1094
1298
|
|
|
@@ -1146,6 +1350,8 @@ type PairedEddystone = {
|
|
|
1146
1350
|
identifier: string;
|
|
1147
1351
|
namespace: string; // 20 hex chars
|
|
1148
1352
|
instance: string; // 12 hex chars
|
|
1353
|
+
name?: string; // Optional BLE device name
|
|
1354
|
+
timeoutSeconds?: number; // Fires onEddystoneTimeout after this duration in range
|
|
1149
1355
|
};
|
|
1150
1356
|
```
|
|
1151
1357
|
|
|
@@ -1234,6 +1440,59 @@ type NotificationChannelConfig = {
|
|
|
1234
1440
|
};
|
|
1235
1441
|
```
|
|
1236
1442
|
|
|
1443
|
+
### `BeaconTimeoutEvent`
|
|
1444
|
+
|
|
1445
|
+
Payload for `onBeaconTimeout`.
|
|
1446
|
+
|
|
1447
|
+
```ts
|
|
1448
|
+
type BeaconTimeoutEvent = {
|
|
1449
|
+
identifier: string;
|
|
1450
|
+
uuid: string;
|
|
1451
|
+
major: number;
|
|
1452
|
+
minor: number;
|
|
1453
|
+
distance: number; // Metres at timeout fire (–1 if unavailable)
|
|
1454
|
+
};
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### `EddystoneTimeoutEvent`
|
|
1458
|
+
|
|
1459
|
+
Payload for `onEddystoneTimeout`.
|
|
1460
|
+
|
|
1461
|
+
```ts
|
|
1462
|
+
type EddystoneTimeoutEvent = {
|
|
1463
|
+
identifier: string;
|
|
1464
|
+
namespace: string;
|
|
1465
|
+
instance: string;
|
|
1466
|
+
distance: number; // Metres at timeout fire (–1 if unavailable)
|
|
1467
|
+
};
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
### `EventLogQueryOptions`
|
|
1471
|
+
|
|
1472
|
+
Passed to `getEventLogs()`.
|
|
1473
|
+
|
|
1474
|
+
```ts
|
|
1475
|
+
type EventLogQueryOptions = {
|
|
1476
|
+
limit?: number; // Max entries (default: 1000, max: 10000)
|
|
1477
|
+
eventType?: string; // Filter by event name
|
|
1478
|
+
sinceTimestamp?: number; // Only events after this ms-epoch timestamp
|
|
1479
|
+
};
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
### `EventLogEntry`
|
|
1483
|
+
|
|
1484
|
+
Returned by `getEventLogs()`.
|
|
1485
|
+
|
|
1486
|
+
```ts
|
|
1487
|
+
type EventLogEntry = {
|
|
1488
|
+
id: number; // Auto-increment row ID
|
|
1489
|
+
timestamp: number; // Milliseconds since epoch
|
|
1490
|
+
eventType: string; // e.g. "onBeaconEnter"
|
|
1491
|
+
identifier?: string; // Beacon identifier, if available
|
|
1492
|
+
data: Record<string, unknown>; // Full event payload
|
|
1493
|
+
};
|
|
1494
|
+
```
|
|
1495
|
+
|
|
1237
1496
|
---
|
|
1238
1497
|
|
|
1239
1498
|
## Background Behaviour
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package expo.modules.beacon
|
|
2
|
+
|
|
3
|
+
import android.content.ContentValues
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.database.sqlite.SQLiteDatabase
|
|
6
|
+
import android.database.sqlite.SQLiteOpenHelper
|
|
7
|
+
import org.json.JSONObject
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* SQLite-backed event logger for beacon events.
|
|
11
|
+
* Thread-safe — all writes go through a single SQLiteOpenHelper.
|
|
12
|
+
*/
|
|
13
|
+
internal class BeaconEventLogger(context: Context) :
|
|
14
|
+
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
private const val DB_NAME = "expo_beacon_events.db"
|
|
18
|
+
private const val DB_VERSION = 1
|
|
19
|
+
private const val TABLE = "events"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun onCreate(db: SQLiteDatabase) {
|
|
23
|
+
db.execSQL("""
|
|
24
|
+
CREATE TABLE IF NOT EXISTS $TABLE (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
timestamp INTEGER NOT NULL,
|
|
27
|
+
event_type TEXT NOT NULL,
|
|
28
|
+
identifier TEXT,
|
|
29
|
+
data TEXT NOT NULL
|
|
30
|
+
)
|
|
31
|
+
""".trimIndent())
|
|
32
|
+
db.execSQL("CREATE INDEX IF NOT EXISTS idx_events_ts ON $TABLE (timestamp)")
|
|
33
|
+
db.execSQL("CREATE INDEX IF NOT EXISTS idx_events_type ON $TABLE (event_type)")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
37
|
+
db.execSQL("DROP TABLE IF EXISTS $TABLE")
|
|
38
|
+
onCreate(db)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun logEvent(eventType: String, identifier: String?, data: Map<String, Any?>) {
|
|
42
|
+
val json = JSONObject()
|
|
43
|
+
data.forEach { (k, v) -> json.put(k, v ?: JSONObject.NULL) }
|
|
44
|
+
val values = ContentValues().apply {
|
|
45
|
+
put("timestamp", System.currentTimeMillis())
|
|
46
|
+
put("event_type", eventType)
|
|
47
|
+
put("identifier", identifier)
|
|
48
|
+
put("data", json.toString())
|
|
49
|
+
}
|
|
50
|
+
writableDatabase.insert(TABLE, null, values)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fun getEvents(limit: Int = 1000, eventType: String? = null, sinceTimestamp: Long? = null): List<Map<String, Any?>> {
|
|
54
|
+
val args = mutableListOf<String>()
|
|
55
|
+
val clauses = mutableListOf<String>()
|
|
56
|
+
if (eventType != null) {
|
|
57
|
+
clauses.add("event_type = ?")
|
|
58
|
+
args.add(eventType)
|
|
59
|
+
}
|
|
60
|
+
if (sinceTimestamp != null) {
|
|
61
|
+
clauses.add("timestamp >= ?")
|
|
62
|
+
args.add(sinceTimestamp.toString())
|
|
63
|
+
}
|
|
64
|
+
val where = if (clauses.isEmpty()) "" else "WHERE ${clauses.joinToString(" AND ")}"
|
|
65
|
+
val safeLimit = limit.coerceIn(1, 10000)
|
|
66
|
+
val cursor = readableDatabase.rawQuery(
|
|
67
|
+
"SELECT id, timestamp, event_type, identifier, data FROM $TABLE $where ORDER BY timestamp DESC LIMIT ?",
|
|
68
|
+
(args + safeLimit.toString()).toTypedArray()
|
|
69
|
+
)
|
|
70
|
+
val results = mutableListOf<Map<String, Any?>>()
|
|
71
|
+
cursor.use {
|
|
72
|
+
while (it.moveToNext()) {
|
|
73
|
+
results.add(buildMap {
|
|
74
|
+
put("id", it.getLong(0))
|
|
75
|
+
put("timestamp", it.getLong(1))
|
|
76
|
+
put("eventType", it.getString(2))
|
|
77
|
+
put("identifier", it.getString(3))
|
|
78
|
+
put("data", try {
|
|
79
|
+
val json = JSONObject(it.getString(4))
|
|
80
|
+
val map = mutableMapOf<String, Any?>()
|
|
81
|
+
json.keys().forEach { k -> map[k] = json.opt(k) }
|
|
82
|
+
map
|
|
83
|
+
} catch (_: Exception) { emptyMap<String, Any?>() })
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fun clearEvents() {
|
|
91
|
+
writableDatabase.delete(TABLE, null, null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fun destroyDatabase(context: Context) {
|
|
95
|
+
close()
|
|
96
|
+
context.deleteDatabase(DB_NAME)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -41,6 +41,7 @@ class BeaconEventReceiver(
|
|
|
41
41
|
"enter" -> "onEddystoneEnter"
|
|
42
42
|
"exit" -> "onEddystoneExit"
|
|
43
43
|
"distance" -> "onEddystoneDistance"
|
|
44
|
+
"timeout" -> "onEddystoneTimeout"
|
|
44
45
|
else -> return
|
|
45
46
|
}
|
|
46
47
|
onEvent(eventName, params)
|
|
@@ -62,6 +63,7 @@ class BeaconEventReceiver(
|
|
|
62
63
|
"enter" -> "onBeaconEnter"
|
|
63
64
|
"exit" -> "onBeaconExit"
|
|
64
65
|
"distance" -> "onBeaconDistance"
|
|
66
|
+
"timeout" -> "onBeaconTimeout"
|
|
65
67
|
else -> return
|
|
66
68
|
}
|
|
67
69
|
onEvent(eventName, params)
|
|
@@ -6,7 +6,9 @@ import android.content.Intent
|
|
|
6
6
|
import android.content.SharedPreferences
|
|
7
7
|
import android.content.pm.ServiceInfo
|
|
8
8
|
import android.os.Build
|
|
9
|
+
import android.os.Handler
|
|
9
10
|
import android.os.IBinder
|
|
11
|
+
import android.os.Looper
|
|
10
12
|
import android.os.RemoteException
|
|
11
13
|
import android.util.Log
|
|
12
14
|
import java.util.concurrent.atomic.AtomicInteger
|
|
@@ -48,6 +50,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
48
50
|
// Distance logging
|
|
49
51
|
private val distanceLogRegions = java.util.concurrent.CopyOnWriteArraySet<Region>()
|
|
50
52
|
|
|
53
|
+
// Timeout timers — fire once after beacon stays in range for configured duration
|
|
54
|
+
private val timeoutHandler = Handler(Looper.getMainLooper())
|
|
55
|
+
private val timeoutRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
|
|
56
|
+
// Per-beacon timeout seconds lookup (identifier → seconds), loaded from paired data
|
|
57
|
+
private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
|
58
|
+
|
|
51
59
|
companion object {
|
|
52
60
|
private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
|
|
53
61
|
|
|
@@ -123,6 +131,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
123
131
|
val eddystoneJson = eddystonePrefs.getString(EDDYSTONE_PREFS_KEY, "[]") ?: "[]"
|
|
124
132
|
val eddystones = try { JSONArray(eddystoneJson) } catch (_: Exception) { JSONArray() }
|
|
125
133
|
|
|
134
|
+
// Build timeout lookup from paired beacon data
|
|
135
|
+
beaconTimeouts.clear()
|
|
136
|
+
for (i in 0 until beacons.length()) {
|
|
137
|
+
val b = beacons.getJSONObject(i)
|
|
138
|
+
val id = b.getString("identifier")
|
|
139
|
+
if (b.has("timeoutSeconds")) {
|
|
140
|
+
val secs = b.optInt("timeoutSeconds", 0)
|
|
141
|
+
if (secs > 0) beaconTimeouts[id] = secs
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (i in 0 until eddystones.length()) {
|
|
145
|
+
val e = eddystones.getJSONObject(i)
|
|
146
|
+
val id = e.getString("identifier")
|
|
147
|
+
if (e.has("timeoutSeconds")) {
|
|
148
|
+
val secs = e.optInt("timeoutSeconds", 0)
|
|
149
|
+
if (secs > 0) beaconTimeouts[id] = secs
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
// Stop previous regions and distance-log ranging
|
|
127
154
|
distanceLogRegions.forEach {
|
|
128
155
|
try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
@@ -212,6 +239,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
212
239
|
enteredRegions.add(region.uniqueId)
|
|
213
240
|
sendBeaconBroadcast(region, "enter", -1.0)
|
|
214
241
|
showEnterExitNotification(region, "enter")
|
|
242
|
+
scheduleTimeoutIfConfigured(region)
|
|
215
243
|
}
|
|
216
244
|
}
|
|
217
245
|
|
|
@@ -229,12 +257,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
229
257
|
missCounters.remove(region.uniqueId)
|
|
230
258
|
}
|
|
231
259
|
if (wasEntered) {
|
|
260
|
+
cancelTimeout(region.uniqueId)
|
|
232
261
|
sendBeaconBroadcast(region, "exit", -1.0)
|
|
233
262
|
showEnterExitNotification(region, "exit")
|
|
234
263
|
}
|
|
235
264
|
return
|
|
236
265
|
}
|
|
237
266
|
|
|
267
|
+
cancelTimeout(region.uniqueId)
|
|
238
268
|
enteredRegions.remove(region.uniqueId)
|
|
239
269
|
sendBeaconBroadcast(region, "exit", -1.0)
|
|
240
270
|
showEnterExitNotification(region, "exit")
|
|
@@ -268,8 +298,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
268
298
|
rangingRegions.remove(region)
|
|
269
299
|
sendBeaconBroadcast(region, "enter", beacon.distance)
|
|
270
300
|
showEnterExitNotification(region, "enter")
|
|
301
|
+
scheduleTimeoutIfConfigured(region)
|
|
271
302
|
}
|
|
272
303
|
HysteresisAction.EXIT -> {
|
|
304
|
+
cancelTimeout(region.uniqueId)
|
|
273
305
|
enteredRegions.remove(region.uniqueId)
|
|
274
306
|
rangingRegions.add(region)
|
|
275
307
|
sendBeaconBroadcast(region, "exit", beacon.distance)
|
|
@@ -283,6 +315,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
283
315
|
missCounters[region.uniqueId] = count
|
|
284
316
|
|
|
285
317
|
if (enteredRegions.contains(region.uniqueId) && count >= EXIT_MISS_THRESHOLD) {
|
|
318
|
+
cancelTimeout(region.uniqueId)
|
|
286
319
|
enteredRegions.remove(region.uniqueId)
|
|
287
320
|
missCounters[region.uniqueId] = 0
|
|
288
321
|
enterCounters[region.uniqueId] = 0
|
|
@@ -344,6 +377,27 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
344
377
|
return HysteresisAction.NONE
|
|
345
378
|
}
|
|
346
379
|
|
|
380
|
+
// MARK: - Timeout timer helpers
|
|
381
|
+
|
|
382
|
+
private fun scheduleTimeoutIfConfigured(region: Region) {
|
|
383
|
+
val seconds = beaconTimeouts[region.uniqueId] ?: return
|
|
384
|
+
// Cancel any existing timer (shouldn't happen, but be safe)
|
|
385
|
+
cancelTimeout(region.uniqueId)
|
|
386
|
+
val runnable = Runnable {
|
|
387
|
+
timeoutRunnables.remove(region.uniqueId)
|
|
388
|
+
// Only fire if the beacon is still in range
|
|
389
|
+
if (enteredRegions.contains(region.uniqueId)) {
|
|
390
|
+
sendBeaconBroadcast(region, "timeout", -1.0)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
timeoutRunnables[region.uniqueId] = runnable
|
|
394
|
+
timeoutHandler.postDelayed(runnable, seconds * 1000L)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private fun cancelTimeout(regionId: String) {
|
|
398
|
+
timeoutRunnables.remove(regionId)?.let { timeoutHandler.removeCallbacks(it) }
|
|
399
|
+
}
|
|
400
|
+
|
|
347
401
|
private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double) {
|
|
348
402
|
// Determine if this is an Eddystone region based on identifier format
|
|
349
403
|
// Eddystone regions have id1 as a hex namespace (not a UUID)
|
|
@@ -470,6 +524,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
470
524
|
}
|
|
471
525
|
|
|
472
526
|
override fun onDestroy() {
|
|
527
|
+
timeoutHandler.removeCallbacksAndMessages(null)
|
|
528
|
+
timeoutRunnables.clear()
|
|
529
|
+
beaconTimeouts.clear()
|
|
473
530
|
beaconManager.removeMonitorNotifier(monitorNotifier)
|
|
474
531
|
beaconManager.removeRangeNotifier(rangeNotifier)
|
|
475
532
|
beaconManager.removeRangeNotifier(distanceLoggingRangeNotifier)
|