expo-beacon 0.9.0 → 0.9.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.
@@ -68,6 +68,7 @@
68
68
  <service
69
69
  android:name="expo.modules.beacon.BeaconForegroundService"
70
70
  android:foregroundServiceType="connectedDevice"
71
+ android:stopWithTask="false"
71
72
  android:exported="false" />
72
73
 
73
74
  <!-- Restart monitoring after boot.
@@ -91,6 +92,8 @@
91
92
  <!-- Self-scheduled CarPlay watchdog (11-min cadence; above
92
93
  setExactAndAllowWhileIdle per-app quota). -->
93
94
  <action android:name="expo.modules.beacon.ACTION_CARPLAY_WATCHDOG" />
95
+ <!-- Near-term keepalive armed when the app task is swiped away. -->
96
+ <action android:name="expo.modules.beacon.ACTION_TASK_REMOVED_KEEPALIVE" />
94
97
  </intent-filter>
95
98
  </receiver>
96
99
  </application>
@@ -1430,11 +1430,23 @@ class BeaconForegroundService : Service(), BeaconConsumer {
1430
1430
  * `stopSelf()` here; the system will redeliver `onStartCommand` on
1431
1431
  * its own if the process is later reclaimed.
1432
1432
  *
1433
- * Logging is the only side-effect so that stuck-state issues are
1434
- * traceable in logcat.
1433
+ * We also arm a near-term keepalive alarm so devices that tear down the
1434
+ * process on task removal recover before the slower periodic watchdogs run.
1435
1435
  */
1436
1436
  override fun onTaskRemoved(rootIntent: Intent?) {
1437
1437
  val keepAlive = isMonitoringActive(this) || isCarPlayEnabled(this)
1438
+ if (keepAlive) {
1439
+ try {
1440
+ if (isCarPlayEnabled(this)) {
1441
+ startCarPlayObserverInternal()
1442
+ CarPlayWatchdogWorker.schedule(this)
1443
+ BootReceiver.scheduleCarPlayWatchdogAlarm(this)
1444
+ }
1445
+ BootReceiver.scheduleTaskRemovedKeepAlive(this)
1446
+ } catch (t: Throwable) {
1447
+ Log.w(TAG, "Failed to arm task-removed keepalive", t)
1448
+ }
1449
+ }
1438
1450
  Log.d(
1439
1451
  TAG,
1440
1452
  "onTaskRemoved received (monitoring=${isMonitoringActive(this)}, " +
@@ -17,9 +17,12 @@ private const val ACTION_RETRY_MONITORING = "expo.modules.beacon.ACTION_RETRY_MO
17
17
  * process has been killed.
18
18
  */
19
19
  internal const val ACTION_CARPLAY_WATCHDOG = "expo.modules.beacon.ACTION_CARPLAY_WATCHDOG"
20
+ private const val ACTION_TASK_REMOVED_KEEPALIVE = "expo.modules.beacon.ACTION_TASK_REMOVED_KEEPALIVE"
20
21
  private const val RETRY_DELAY_MS = 10_000L
21
22
  private const val RETRY_REQUEST_CODE = 0x424F4F54 // "BOOT"
22
23
  private const val CARPLAY_WATCHDOG_REQUEST_CODE = 0x43504C57 // "CPLW"
24
+ private const val TASK_REMOVED_KEEPALIVE_REQUEST_CODE = 0x54524B41 // "TRKA"
25
+ private const val TASK_REMOVED_KEEPALIVE_DELAY_MS = 2_000L
23
26
 
24
27
  /**
25
28
  * Cadence for the AlarmManager-based CarPlay watchdog. Set to **11 minutes**
@@ -75,6 +78,21 @@ class BootReceiver : BroadcastReceiver() {
75
78
  // Reschedule the next tick. setExactAndAllowWhileIdle is one-shot.
76
79
  scheduleCarPlayWatchdogAlarm(context)
77
80
  }
81
+ ACTION_TASK_REMOVED_KEEPALIVE -> {
82
+ val monitoringActive = BeaconForegroundService.isMonitoringActive(context)
83
+ val carPlayEnabled = BeaconForegroundService.isCarPlayEnabled(context)
84
+ if (!monitoringActive && !carPlayEnabled) {
85
+ Log.d(TAG, "BootReceiver: task-removed keepalive skipped (nothing active)")
86
+ return
87
+ }
88
+ if (carPlayEnabled) {
89
+ tryEnableCarPlay(context)
90
+ scheduleCarPlayWatchdogAlarm(context)
91
+ } else {
92
+ tryStartService(context)
93
+ }
94
+ Log.d(TAG, "BootReceiver: task-removed keepalive ensured service is running")
95
+ }
78
96
  }
79
97
  }
80
98
 
@@ -200,5 +218,35 @@ class BootReceiver : BroadcastReceiver() {
200
218
  Log.w(TAG, "BootReceiver: failed to cancel CarPlay watchdog alarm", t)
201
219
  }
202
220
  }
221
+
222
+ /**
223
+ * Schedule a near-term service keepalive after the user swipes the app
224
+ * task away. Some devices tear down the process despite a foreground
225
+ * service; this closes that gap before the slower periodic watchdogs run.
226
+ */
227
+ @JvmStatic
228
+ fun scheduleTaskRemovedKeepAlive(context: Context) {
229
+ val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
230
+ val intent = Intent(context, BootReceiver::class.java).apply {
231
+ action = ACTION_TASK_REMOVED_KEEPALIVE
232
+ `package` = context.packageName
233
+ }
234
+ val pendingIntent = PendingIntent.getBroadcast(
235
+ context,
236
+ TASK_REMOVED_KEEPALIVE_REQUEST_CODE,
237
+ intent,
238
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
239
+ )
240
+ try {
241
+ alarmManager.setExactAndAllowWhileIdle(
242
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
243
+ SystemClock.elapsedRealtime() + TASK_REMOVED_KEEPALIVE_DELAY_MS,
244
+ pendingIntent,
245
+ )
246
+ Log.d(TAG, "BootReceiver: task-removed keepalive armed (${TASK_REMOVED_KEEPALIVE_DELAY_MS}ms)")
247
+ } catch (t: Throwable) {
248
+ Log.w(TAG, "BootReceiver: failed to arm task-removed keepalive", t)
249
+ }
250
+ }
203
251
  }
204
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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,4 +1,4 @@
1
- import { ConfigPlugin } from '@expo/config-plugins';
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
2
  export type BeaconIOSPluginProps = {
3
3
  /**
4
4
  * Enable the CarPlay "Driving Task" entitlement integration.
@@ -1 +1 @@
1
- {"version":3,"file":"withBeaconIOS.d.ts","sourceRoot":"","sources":["../src/withBeaconIOS.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAKb,MAAM,sBAAsB,CAAC;AAM9B,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;;;;;;OAcG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAIF,wBAAgB,iBAAiB,IAAI,MAAM,CAkC1C;AA+ED,QAAA,MAAM,aAAa,EAAE,YAAY,CAAC,oBAAoB,GAAG,IAAI,CA0E5D,CAAC;AA4CF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"withBeaconIOS.d.ts","sourceRoot":"","sources":["../src/withBeaconIOS.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAKb,MAAM,sBAAsB,CAAC;AAM9B,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;;;;;;;;;;OAcG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAIF,wBAAgB,iBAAiB,IAAI,MAAM,CA4C1C;AAuFD,QAAA,MAAM,aAAa,EAAE,YAAY,CAAC,oBAAoB,GAAG,IAAI,CAuF5D,CAAC;AA6CF,eAAe,aAAa,CAAC"}
@@ -11,52 +11,62 @@ import ExpoBeacon
11
11
  import TSLocationManager
12
12
 
13
13
  final class BeaconGeoPlugin: BeaconLifecycleDelegate {
14
- func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
15
- BackgroundGeolocation.sharedInstance().start()
16
- }
17
- func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
18
- BackgroundGeolocation.sharedInstance().stop()
19
- }
20
- func beaconDidTimeout(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
21
- BackgroundGeolocation.sharedInstance().stop()
22
- }
23
- func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double) {
24
- BackgroundGeolocation.sharedInstance().start()
25
- }
26
- func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double) {
27
- BackgroundGeolocation.sharedInstance().stop()
28
- }
29
- func eddystoneDidTimeout(identifier: String, namespace: String, instance: String, distance: Double) {
30
- BackgroundGeolocation.sharedInstance().stop()
31
- }
32
- // Start tracking when the device connects to CarPlay (wired or wireless),
33
- // stop when it disconnects.
34
- func carPlayDidConnect(transport: String) {
35
- BackgroundGeolocation.sharedInstance().start()
36
- }
37
- func carPlayDidDisconnect() {
38
- BackgroundGeolocation.sharedInstance().stop()
39
- }
14
+ private func startTracking() {
15
+ BackgroundGeolocation.sharedInstance().start()
16
+ BackgroundGeolocation.sharedInstance().changePace(true)
17
+ }
18
+
19
+ private func stopTracking() {
20
+ BackgroundGeolocation.sharedInstance().changePace(false)
21
+ BackgroundGeolocation.sharedInstance().stop()
22
+ }
23
+
24
+ func beaconDidEnter(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
25
+ startTracking()
26
+ }
27
+ func beaconDidExit(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
28
+ stopTracking()
29
+ }
30
+ func beaconDidTimeout(identifier: String, uuid: String, major: Int, minor: Int, distance: Double) {
31
+ stopTracking()
32
+ }
33
+ func eddystoneDidEnter(identifier: String, namespace: String, instance: String, distance: Double) {
34
+ startTracking()
35
+ }
36
+ func eddystoneDidExit(identifier: String, namespace: String, instance: String, distance: Double) {
37
+ stopTracking()
38
+ }
39
+ func eddystoneDidTimeout(identifier: String, namespace: String, instance: String, distance: Double) {
40
+ stopTracking()
41
+ }
42
+ // Start tracking when the device connects to CarPlay (wired or wireless),
43
+ // stop when it disconnects.
44
+ func carPlayDidConnect(transport: String) {
45
+ startTracking()
46
+ }
47
+ func carPlayDidDisconnect() {
48
+ stopTracking()
49
+ }
40
50
  }
41
51
  `;
42
52
  }
43
53
  // ─── Helpers ──────────────────────────────────────────────────────────────────
44
54
  function modifyAppDelegateSwift(contents) {
45
- const importLine = 'import ExpoBeacon';
55
+ const importLine = "import ExpoBeacon";
46
56
  // 1. Add import after the last existing import line.
47
57
  if (!contents.includes(importLine)) {
48
- const lines = contents.split('\n');
49
- const lastImportIdx = lines.reduce((last, line, i) => (line.trimStart().startsWith('import ') ? i : last), -1);
58
+ const lines = contents.split("\n");
59
+ const lastImportIdx = lines.reduce((last, line, i) => (line.trimStart().startsWith("import ") ? i : last), -1);
50
60
  if (lastImportIdx >= 0) {
51
61
  lines.splice(lastImportIdx + 1, 0, importLine);
52
62
  }
53
63
  else {
54
- lines.splice(0, 0, importLine, '');
64
+ lines.splice(0, 0, importLine, "");
55
65
  }
56
- contents = lines.join('\n');
66
+ contents = lines.join("\n");
57
67
  }
58
68
  // 2. Insert registration call before `return super.application(…didFinishLaunchingWithOptions…)`.
59
- const registrationCall = 'BeaconLifecycleRegistry.register(BeaconGeoPlugin())';
69
+ const registrationCall = "BeaconLifecycleRegistry.register(BeaconGeoPlugin())";
60
70
  if (contents.includes(registrationCall)) {
61
71
  return contents; // already patched
62
72
  }
@@ -72,9 +82,12 @@ function modifyAppDelegateSwift(contents) {
72
82
  return super.application(application, didFinishLaunchingWithOptions: launchOptions)
73
83
  }
74
84
  `;
75
- const lastBraceIdx = contents.lastIndexOf('}');
85
+ const lastBraceIdx = contents.lastIndexOf("}");
76
86
  if (lastBraceIdx >= 0) {
77
- contents = contents.slice(0, lastBraceIdx) + methodOverride + contents.slice(lastBraceIdx);
87
+ contents =
88
+ contents.slice(0, lastBraceIdx) +
89
+ methodOverride +
90
+ contents.slice(lastBraceIdx);
78
91
  }
79
92
  return contents;
80
93
  }
@@ -96,7 +109,7 @@ function findAppDir(platformRoot) {
96
109
  catch {
97
110
  continue;
98
111
  }
99
- const swiftPath = path.join(dirPath, 'AppDelegate.swift');
112
+ const swiftPath = path.join(dirPath, "AppDelegate.swift");
100
113
  if (fs.existsSync(swiftPath)) {
101
114
  return { appDir: entry, appDelegatePath: swiftPath };
102
115
  }
@@ -108,15 +121,15 @@ const withBeaconIOS = (config, props) => {
108
121
  const opts = props !== null && props !== void 0 ? props : {};
109
122
  // Step 1 – write BeaconGeoPlugin.swift into the iOS app directory.
110
123
  config = (0, config_plugins_1.withDangerousMod)(config, [
111
- 'ios',
124
+ "ios",
112
125
  (config) => {
113
126
  const platformRoot = config.modRequest.platformProjectRoot;
114
127
  const result = findAppDir(platformRoot);
115
128
  if (!result) {
116
- console.warn('[expo-beacon] Could not locate iOS app directory — BeaconGeoPlugin.swift was not written.');
129
+ console.warn("[expo-beacon] Could not locate iOS app directory — BeaconGeoPlugin.swift was not written.");
117
130
  return config;
118
131
  }
119
- const outputPath = path.join(platformRoot, result.appDir, 'BeaconGeoPlugin.swift');
132
+ const outputPath = path.join(platformRoot, result.appDir, "BeaconGeoPlugin.swift");
120
133
  fs.writeFileSync(outputPath, getIOSPluginSwift());
121
134
  return config;
122
135
  },
@@ -128,10 +141,10 @@ const withBeaconIOS = (config, props) => {
128
141
  const filePath = `${projectName}/BeaconGeoPlugin.swift`;
129
142
  if (!xcodeProject.hasFile(filePath)) {
130
143
  // pbxGroupByName returns the group object; addSourceFile needs the UUID key.
131
- const groups = xcodeProject.hash.project.objects['PBXGroup'];
144
+ const groups = xcodeProject.hash.project.objects["PBXGroup"];
132
145
  let groupKey;
133
146
  for (const key of Object.keys(groups)) {
134
- if (key.endsWith('_comment'))
147
+ if (key.endsWith("_comment"))
135
148
  continue;
136
149
  const g = groups[key];
137
150
  if (g.name === projectName || g.path === projectName) {
@@ -145,16 +158,16 @@ const withBeaconIOS = (config, props) => {
145
158
  });
146
159
  // Step 3 – patch AppDelegate.swift to register the plugin.
147
160
  config = (0, config_plugins_1.withDangerousMod)(config, [
148
- 'ios',
161
+ "ios",
149
162
  (config) => {
150
163
  const platformRoot = config.modRequest.platformProjectRoot;
151
164
  const result = findAppDir(platformRoot);
152
165
  if (!result) {
153
- console.warn('[expo-beacon] AppDelegate.swift not found — ' +
154
- 'please add BeaconLifecycleRegistry.register(BeaconGeoPlugin()) manually.');
166
+ console.warn("[expo-beacon] AppDelegate.swift not found — " +
167
+ "please add BeaconLifecycleRegistry.register(BeaconGeoPlugin()) manually.");
155
168
  return config;
156
169
  }
157
- const original = fs.readFileSync(result.appDelegatePath, 'utf-8');
170
+ const original = fs.readFileSync(result.appDelegatePath, "utf-8");
158
171
  fs.writeFileSync(result.appDelegatePath, modifyAppDelegateSwift(original));
159
172
  return config;
160
173
  },
@@ -166,9 +179,9 @@ const withBeaconIOS = (config, props) => {
166
179
  return config;
167
180
  };
168
181
  // ─── CarPlay Driving Task wiring ──────────────────────────────────────────────
169
- const CARPLAY_ENTITLEMENT_KEY = 'com.apple.developer.carplay-driving-task';
170
- const CARPLAY_SCENE_DELEGATE_CLASS = 'ExpoBeacon.BeaconCarPlaySceneDelegate';
171
- const CARPLAY_SCENE_CONFIG_NAME = 'ExpoBeaconCarPlay';
182
+ const CARPLAY_ENTITLEMENT_KEY = "com.apple.developer.carplay-driving-task";
183
+ const CARPLAY_SCENE_DELEGATE_CLASS = "ExpoBeacon.BeaconCarPlaySceneDelegate";
184
+ const CARPLAY_SCENE_CONFIG_NAME = "ExpoBeaconCarPlay";
172
185
  function withCarPlayDrivingTask(config) {
173
186
  // 4a — entitlement
174
187
  config = (0, config_plugins_1.withEntitlementsPlist)(config, (cfg) => {
@@ -191,7 +204,7 @@ function withCarPlayDrivingTask(config) {
191
204
  (entry === null || entry === void 0 ? void 0 : entry.UISceneConfigurationName) === CARPLAY_SCENE_CONFIG_NAME);
192
205
  if (!alreadyPresent) {
193
206
  role.push({
194
- UISceneClassName: 'CPTemplateApplicationScene',
207
+ UISceneClassName: "CPTemplateApplicationScene",
195
208
  UISceneConfigurationName: CARPLAY_SCENE_CONFIG_NAME,
196
209
  UISceneDelegateClassName: CARPLAY_SCENE_DELEGATE_CLASS,
197
210
  });