expo-beacon 0.10.7 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"withBeaconAndroid.d.ts","sourceRoot":"","sources":["../src/withBeaconAndroid.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,YAAY,EAGZ,MAAM,sBAAsB,CAAC;AAM9B;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,CAAC;AAExE,MAAM,MAAM,wBAAwB,GAAG;IACtC;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE;QACb;2EACmE;QACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;qCAC6B;QAC7B,QAAQ,CAAC,EAAE,mBAAmB,CAAC;KAC/B,CAAC;CACF,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAmFlE;AAoMD,QAAA,MAAM,iBAAiB,EAAE,YAAY,CAAC,wBAAwB,GAAG,IAAI,CA0BpE,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"withBeaconAndroid.d.ts","sourceRoot":"","sources":["../src/withBeaconAndroid.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,YAAY,EAGZ,MAAM,sBAAsB,CAAC;AAM9B;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,CAAC;AAExE,MAAM,MAAM,wBAAwB,GAAG;IACtC;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE;QACb;2EACmE;QACnE,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;qCAC6B;QAC7B,QAAQ,CAAC,EAAE,mBAAmB,CAAC;KAC/B,CAAC;CACF,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA2KlE;AA8ND,QAAA,MAAM,iBAAiB,EAAE,YAAY,CAAC,wBAAwB,GAAG,IAAI,CA0BpE,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -7,66 +7,72 @@ const path = require("path");
7
7
  // ─── Generated Kotlin file ────────────────────────────────────────────────────
8
8
  function getAndroidPluginKotlin(packageName) {
9
9
  return `\
10
- package ${packageName}
10
+ package ${packageName}
11
+
12
+ import android.app.AlarmManager
13
+ import android.app.PendingIntent
14
+ import android.content.BroadcastReceiver
15
+ import android.content.Context
16
+ import android.content.Intent
17
+ import android.os.Handler
18
+ import android.os.Looper
19
+ import android.os.SystemClock
20
+ import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation
21
+ import com.transistorsoft.locationmanager.adapter.callback.TSCallback
22
+ import com.transistorsoft.locationmanager.adapter.callback.TSSyncCallback
23
+ import com.transistorsoft.locationmanager.data.LocationModel
24
+ import expo.modules.beacon.BeaconEventPlugin
11
25
 
12
- import android.content.Context
13
- import android.os.Handler
14
- import android.os.Looper
15
- import com.transistorsoft.locationmanager.adapter.BackgroundGeolocation
16
- import com.transistorsoft.locationmanager.adapter.callback.TSCallback
17
- import com.transistorsoft.locationmanager.adapter.callback.TSSyncCallback
18
- import com.transistorsoft.locationmanager.data.LocationModel
19
- import expo.modules.beacon.BeaconEventPlugin
20
-
21
- class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
22
- private val bgGeo = BackgroundGeolocation.getInstance(ctx, null)
23
- private val noOp = object : TSCallback {
24
- override fun onSuccess() {}
25
- override fun onFailure(error: String) {}
26
+ class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
27
+ private val appContext = ctx.applicationContext
28
+ private val bgGeo = BackgroundGeolocation.getInstance(ctx, null)
29
+ private val noOp = object : TSCallback {
30
+ override fun onSuccess() {}
31
+ override fun onFailure(error: String) {}
26
32
  }
27
33
  private val noOpSync = object : TSSyncCallback {
28
- override fun onSuccess(records: MutableList<LocationModel>) {}
29
- override fun onFailure(error: String) {}
30
- }
31
-
32
- // CarPlay disconnect uses a grace period: flush immediately, but defer the
33
- // actual stop() so a quick reconnect (brief dropout) keeps tracking alive.
34
- private val mainHandler = Handler(Looper.getMainLooper())
35
- private var pendingCarPlayStop: Runnable? = null
36
- private val carPlayStopDelayMs = 30_000L
37
-
38
- private fun startTracking() {
39
- bgGeo.start(noOp)
40
- bgGeo.changePace(true, noOp)
41
- }
42
-
43
- private fun stopTracking() {
44
- bgGeo.sync(noOpSync)
45
- bgGeo.changePace(false, noOp)
46
- bgGeo.stop(noOp)
47
- }
48
-
49
- // Flush queued locations right away, then stop tracking after a grace
50
- // period. Cancelled by cancelPendingCarPlayStop() if CarPlay reconnects.
51
- private fun stopTrackingForCarPlay() {
52
- bgGeo.sync(noOpSync)
53
- bgGeo.changePace(false, noOp)
54
- cancelPendingCarPlayStop()
55
- val stop = Runnable {
56
- pendingCarPlayStop = null
57
- bgGeo.stop(noOp)
58
- }
59
- pendingCarPlayStop = stop
60
- mainHandler.postDelayed(stop, carPlayStopDelayMs)
61
- }
62
-
63
- private fun cancelPendingCarPlayStop() {
64
- pendingCarPlayStop?.let { mainHandler.removeCallbacks(it) }
65
- pendingCarPlayStop = null
66
- }
67
-
68
- override fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
69
- startTracking()
34
+ override fun onSuccess(records: MutableList<LocationModel>) {}
35
+ override fun onFailure(error: String) {}
36
+ }
37
+ private val mainHandler = Handler(Looper.getMainLooper())
38
+ private var pendingCarPlayStop: Runnable? = null
39
+ private val carPlayStopDelayMs = 30_000L
40
+
41
+ private fun startTracking() {
42
+ cancelPendingCarPlayStop()
43
+ bgGeo.start(noOp)
44
+ bgGeo.changePace(true, noOp)
45
+ }
46
+
47
+ private fun stopTracking() {
48
+ cancelPendingCarPlayStop()
49
+ bgGeo.sync(noOpSync)
50
+ bgGeo.changePace(false, noOp)
51
+ bgGeo.stop(noOp)
52
+ }
53
+
54
+ private fun stopTrackingForCarPlay() {
55
+ bgGeo.sync(noOpSync)
56
+ bgGeo.changePace(false, noOp)
57
+ cancelPendingCarPlayStop()
58
+ val token = SystemClock.elapsedRealtimeNanos().toString()
59
+ val stop = Runnable {
60
+ pendingCarPlayStop = null
61
+ BeaconGeoCarPlayStop.performIfCurrent(appContext, token)
62
+ }
63
+ pendingCarPlayStop = stop
64
+ BeaconGeoCarPlayStop.schedule(appContext, token, carPlayStopDelayMs)
65
+ mainHandler.postDelayed(stop, carPlayStopDelayMs)
66
+ }
67
+
68
+ private fun cancelPendingCarPlayStop() {
69
+ pendingCarPlayStop?.let { mainHandler.removeCallbacks(it) }
70
+ pendingCarPlayStop = null
71
+ BeaconGeoCarPlayStop.cancel(appContext)
72
+ }
73
+
74
+ override fun onBeaconEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
75
+ startTracking()
70
76
  override fun onBeaconExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
71
77
  stopTracking()
72
78
  override fun onBeaconTimeout(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) =
@@ -77,16 +83,98 @@ class BeaconGeoPlugin(ctx: Context) : BeaconEventPlugin {
77
83
  stopTracking()
78
84
  override fun onEddystoneTimeout(identifier: String, namespace: String, instance: String, distance: Double) =
79
85
  stopTracking()
80
- // Start tracking when the device connects to Android Auto; stop (after a
81
- // grace period) when it disconnects.
82
- override fun onCarPlayConnected(transport: String) {
83
- cancelPendingCarPlayStop()
84
- startTracking()
85
- }
86
- override fun onCarPlayDisconnected() {
87
- stopTrackingForCarPlay()
88
- }
89
- }
86
+ // Start tracking when the device connects to Android Auto; stop after a
87
+ // grace period when it disconnects. The delayed stop is backed by
88
+ // AlarmManager so it can still fire if the process idles or is killed.
89
+ override fun onCarPlayConnected(transport: String) {
90
+ startTracking()
91
+ }
92
+ override fun onCarPlayDisconnected() {
93
+ stopTrackingForCarPlay()
94
+ }
95
+ }
96
+
97
+ private object BeaconGeoCarPlayStop {
98
+ private const val ACTION_STOP = "${packageName}.expo_beacon.CARPLAY_STOP_TRACKING"
99
+ private const val EXTRA_TOKEN = "token"
100
+ private const val PREFS = "expo_beacon_geo_plugin"
101
+ private const val KEY_PENDING_TOKEN = "pending_carplay_stop_token"
102
+ private const val REQUEST_CODE = 0xBEEA
103
+
104
+ fun schedule(context: Context, token: String, delayMs: Long) {
105
+ val appContext = context.applicationContext
106
+ appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
107
+ .edit()
108
+ .putString(KEY_PENDING_TOKEN, token)
109
+ .apply()
110
+ val alarmManager = appContext.getSystemService(AlarmManager::class.java) ?: return
111
+ val triggerAt = SystemClock.elapsedRealtime() + delayMs
112
+ try {
113
+ alarmManager.setAndAllowWhileIdle(
114
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
115
+ triggerAt,
116
+ pendingIntent(appContext, token, PendingIntent.FLAG_UPDATE_CURRENT),
117
+ )
118
+ } catch (_: SecurityException) {
119
+ alarmManager.set(
120
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
121
+ triggerAt,
122
+ pendingIntent(appContext, token, PendingIntent.FLAG_UPDATE_CURRENT),
123
+ )
124
+ }
125
+ }
126
+
127
+ fun cancel(context: Context) {
128
+ val appContext = context.applicationContext
129
+ appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
130
+ .edit()
131
+ .remove(KEY_PENDING_TOKEN)
132
+ .apply()
133
+ val alarmManager = appContext.getSystemService(AlarmManager::class.java) ?: return
134
+ alarmManager.cancel(pendingIntent(appContext, null, PendingIntent.FLAG_UPDATE_CURRENT))
135
+ }
136
+
137
+ fun performIfCurrent(context: Context, token: String?) {
138
+ if (token == null) return
139
+ val appContext = context.applicationContext
140
+ val prefs = appContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
141
+ if (prefs.getString(KEY_PENDING_TOKEN, null) != token) return
142
+ prefs.edit().remove(KEY_PENDING_TOKEN).apply()
143
+ val bgGeo = BackgroundGeolocation.getInstance(appContext, null)
144
+ val noOp = object : TSCallback {
145
+ override fun onSuccess() {}
146
+ override fun onFailure(error: String) {}
147
+ }
148
+ val noOpSync = object : TSSyncCallback {
149
+ override fun onSuccess(records: MutableList<LocationModel>) {}
150
+ override fun onFailure(error: String) {}
151
+ }
152
+ bgGeo.sync(noOpSync)
153
+ bgGeo.changePace(false, noOp)
154
+ bgGeo.stop(noOp)
155
+ }
156
+
157
+ fun tokenFrom(intent: Intent): String? = intent.getStringExtra(EXTRA_TOKEN)
158
+
159
+ private fun pendingIntent(context: Context, token: String?, flags: Int): PendingIntent {
160
+ val intent = Intent(context, BeaconGeoStopReceiver::class.java).apply {
161
+ action = ACTION_STOP
162
+ token?.let { putExtra(EXTRA_TOKEN, it) }
163
+ }
164
+ return PendingIntent.getBroadcast(
165
+ context,
166
+ REQUEST_CODE,
167
+ intent,
168
+ flags or PendingIntent.FLAG_IMMUTABLE,
169
+ )
170
+ }
171
+ }
172
+
173
+ class BeaconGeoStopReceiver : BroadcastReceiver() {
174
+ override fun onReceive(context: Context, intent: Intent) {
175
+ BeaconGeoCarPlayStop.performIfCurrent(context, BeaconGeoCarPlayStop.tokenFrom(intent))
176
+ }
177
+ }
90
178
  `;
91
179
  }
92
180
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -116,6 +204,7 @@ function modifyMainApplication(contents) {
116
204
  const AA_META_DATA_NAME = "com.google.android.gms.car.application";
117
205
  const AA_META_DATA_RESOURCE = "@xml/automotive_app_desc";
118
206
  const AA_RES_FILE = "automotive_app_desc.xml";
207
+ const BEACON_GEO_STOP_RECEIVER = "BeaconGeoStopReceiver";
119
208
  function getAutomotiveAppDescXml(usesName) {
120
209
  return `<?xml version="1.0" encoding="utf-8"?>
121
210
  <!--
@@ -179,6 +268,30 @@ function withAndroidAutoDescriptor(usesName) {
179
268
  * the `backgroundGeolocation` plugin prop (default: enabled).
180
269
  */
181
270
  const withBeaconGeoPlugin = (config) => {
271
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
272
+ var _a, _b;
273
+ const pkgName = (_a = config.android) === null || _a === void 0 ? void 0 : _a.package;
274
+ if (!pkgName)
275
+ return config;
276
+ const app = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
277
+ const receiverName = `${pkgName}.${BEACON_GEO_STOP_RECEIVER}`;
278
+ const receivers = ((_b = app.receiver) !== null && _b !== void 0 ? _b : (app.receiver = []));
279
+ const exists = receivers.some((receiver) => {
280
+ var _a, _b;
281
+ return ((_a = receiver === null || receiver === void 0 ? void 0 : receiver.$) === null || _a === void 0 ? void 0 : _a["android:name"]) === receiverName ||
282
+ ((_b = receiver === null || receiver === void 0 ? void 0 : receiver.$) === null || _b === void 0 ? void 0 : _b["android:name"]) === `.${BEACON_GEO_STOP_RECEIVER}`;
283
+ });
284
+ if (!exists) {
285
+ receivers.push({
286
+ $: {
287
+ "android:name": receiverName,
288
+ "android:enabled": "true",
289
+ "android:exported": "false",
290
+ },
291
+ });
292
+ }
293
+ return config;
294
+ });
182
295
  // Step 1 – write BeaconGeoPlugin.kt into the app source tree.
183
296
  config = (0, config_plugins_1.withDangerousMod)(config, [
184
297
  "android",