expo-app-blocker 0.1.76 → 0.1.77

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.
@@ -131,6 +131,43 @@ class ExpoAppBlockerModule : Module() {
131
131
  TemporaryUnlockController.remainingSeconds(context)
132
132
  }
133
133
 
134
+ // Last-resort recovery for an unrecoverable native state (observed:
135
+ // expo-sqlite's Android connection degrading after enough reuse in a
136
+ // long-lived process — a fresh `openDatabaseAsync` + first pragma NPEs
137
+ // even after a full DB rebuild). Apps that keep this module's
138
+ // AppBlockerService running as a foreground service can end up with an
139
+ // unusually long-lived process (the OS won't kill it just because the
140
+ // Activity was "closed"), which surfaces exactly this kind of
141
+ // native-module degradation far more than a normal app would. Only a
142
+ // genuine process kill clears it: queue the relaunch intent (optionally
143
+ // straight back into `deepLink`, e.g. the blocker intercept URL the
144
+ // caller was trying to reach) — `startActivity` only needs to queue the
145
+ // intent with ActivityManagerService, which survives our process dying
146
+ // right after — then kill this process outright. No AlarmManager/
147
+ // PendingIntent needed (and SCHEDULE_EXACT_ALARM would require a
148
+ // permission apps otherwise don't need just for this).
149
+ Function("restartApp") { deepLink: String? ->
150
+ val intent = if (!deepLink.isNullOrEmpty()) {
151
+ Intent(Intent.ACTION_VIEW, Uri.parse(deepLink))
152
+ } else {
153
+ context.packageManager.getLaunchIntentForPackage(context.packageName)
154
+ }
155
+ if (intent == null) {
156
+ Log.e(TAG, "restartApp: no launch intent resolvable, cannot self-heal")
157
+ return@Function
158
+ }
159
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
160
+
161
+ context.startActivity(intent)
162
+
163
+ Log.w(
164
+ TAG,
165
+ "restartApp: relaunch queued, killing process ${Process.myPid()} to recover " +
166
+ "from an unrecoverable native state"
167
+ )
168
+ Process.killProcess(Process.myPid())
169
+ }
170
+
134
171
  AsyncFunction("getInstalledApps") {
135
172
  val pm = context.packageManager
136
173
  val intent = Intent(Intent.ACTION_MAIN).apply {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "Expo module for cross-platform app blocking. Android: UsageStatsManager + Overlay. iOS: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity).",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -222,6 +222,35 @@ export function checkAndClearPendingUnlock(): boolean {
222
222
  return NativeModule.checkAndClearPendingUnlock();
223
223
  }
224
224
 
225
+ /**
226
+ * Android-only, last-resort recovery: forces a genuine process kill and
227
+ * relaunch. Some native-layer failures (observed: an expo-sqlite connection
228
+ * that NPEs on every operation, even a fresh `openDatabaseAsync` against a
229
+ * freshly-rebuilt database file) can only be cleared by a real process
230
+ * restart — closing/reopening the JS-side handle doesn't reach far enough.
231
+ * Apps that run this module's `AppBlockerService` as a foreground service
232
+ * can end up with an unusually long-lived Android process (the OS won't
233
+ * kill it just because the Activity was "closed"), which surfaces this kind
234
+ * of native-module degradation far more than it would in a normal app.
235
+ *
236
+ * Queues a relaunch (optionally straight back into `deepLink`, e.g. the
237
+ * blocker intercept URL the caller was trying to reach), then calls
238
+ * `Process.killProcess`. If that succeeds the process is gone before the
239
+ * call would otherwise return. The native call itself can still reject
240
+ * (e.g. a missing Android permission) — this is already the last-resort
241
+ * path, so that failure is swallowed rather than left as an unhandled
242
+ * rejection.
243
+ *
244
+ * No-op on iOS: that platform's process model doesn't exhibit this failure
245
+ * mode, and there is no equivalent restart primitive.
246
+ */
247
+ export function restartAppForRecovery(deepLink?: string): void {
248
+ if (Platform.OS !== "android") return;
249
+ NativeModule.restartApp(deepLink ?? null).catch((err: unknown) => {
250
+ console.warn("[expo-app-blocker] restartAppForRecovery failed", err);
251
+ });
252
+ }
253
+
225
254
  /**
226
255
  * One OS-level block event: the blocker intercepted a blocked app (iOS
227
256
  * shield render / Android foreground block). `interceptedAt` is epoch