expo-app-blocker 0.1.60 → 0.1.61

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.
@@ -21,7 +21,7 @@ class AppBlockerService : Service() {
21
21
  private val handler = Handler(Looper.getMainLooper())
22
22
  private var lastForegroundPackage: String? = null
23
23
  private lateinit var overlayManager: OverlayManager
24
- private val unlockController = TemporaryUnlockController(handler)
24
+ private val unlockController by lazy { TemporaryUnlockController(this, handler) }
25
25
 
26
26
  private val pollRunnable = object : Runnable {
27
27
  override fun run() {
@@ -123,6 +123,10 @@ class ExpoAppBlockerModule : Module() {
123
123
  Log.d(TAG, "relockAndroid")
124
124
  }
125
125
 
126
+ Function("getRemainingUnlockTimeAndroid") {
127
+ TemporaryUnlockController.remainingSeconds(context)
128
+ }
129
+
126
130
  AsyncFunction("getInstalledApps") {
127
131
  val pm = context.packageManager
128
132
  val intent = Intent(Intent.ACTION_MAIN).apply {
@@ -1,37 +1,64 @@
1
1
  package expo.modules.appblocker
2
2
 
3
+ import android.content.Context
3
4
  import android.os.Handler
4
5
 
5
6
  /**
6
- * Owns the "temporary unlock" state for Android blocking.
7
+ * Single source of truth for the Android "temporary unlock" state.
7
8
  *
8
9
  * While unlocked, the monitor should not block — see [isUnlocked]. An unlock
9
- * auto-expires after the requested duration; [relock] ends it early. All timer
10
- * bookkeeping is hidden here so the service only asks a single yes/no question.
10
+ * auto-expires after the requested duration; [relock] ends it early. The expiry
11
+ * timestamp is persisted (see [Store]) so the JS module can read the remaining
12
+ * time without holding a reference to the running service, mirroring the iOS
13
+ * shared-defaults approach.
11
14
  *
12
- * Not thread-safe beyond the [isUnlocked] read: drive [unlock] / [relock] from
13
- * the same (main) thread the [Handler] is bound to.
15
+ * Drive [unlock] / [relock] from the same (main) thread the [Handler] is bound to.
14
16
  */
15
- class TemporaryUnlockController(private val handler: Handler) {
16
- @Volatile private var unlocked = false
17
-
18
- private val expireRunnable = Runnable { unlocked = false }
17
+ class TemporaryUnlockController(
18
+ private val context: Context,
19
+ private val handler: Handler,
20
+ ) {
21
+ private val expireRunnable = Runnable { Store.clear(context) }
19
22
 
20
23
  /** True while a temporary unlock is in effect (blocking should be suppressed). */
21
24
  val isUnlocked: Boolean
22
- get() = unlocked
25
+ get() = Store.remainingSeconds(context) > 0
23
26
 
24
27
  /** Suppress blocking for [durationMinutes], replacing any pending expiry. No-op if <= 0. */
25
28
  fun unlock(durationMinutes: Int) {
26
29
  if (durationMinutes <= 0) return
30
+ val durationMs = durationMinutes * 60_000L
27
31
  handler.removeCallbacks(expireRunnable)
28
- unlocked = true
29
- handler.postDelayed(expireRunnable, durationMinutes * 60_000L)
32
+ Store.setExpiry(context, System.currentTimeMillis() + durationMs)
33
+ handler.postDelayed(expireRunnable, durationMs)
30
34
  }
31
35
 
32
36
  /** End any active unlock immediately, restoring blocking. */
33
37
  fun relock() {
34
38
  handler.removeCallbacks(expireRunnable)
35
- unlocked = false
39
+ Store.clear(context)
40
+ }
41
+
42
+ /**
43
+ * Stateless persistence for the unlock expiry. Readable from anywhere with a
44
+ * [Context] — the running service is not required.
45
+ */
46
+ companion object Store {
47
+ private const val KEY_EXPIRES_AT = "temporary_unlock_expires_at"
48
+
49
+ private fun setExpiry(context: Context, expiresAtMs: Long) {
50
+ AppBlockerPrefs.get(context).edit().putLong(KEY_EXPIRES_AT, expiresAtMs).apply()
51
+ }
52
+
53
+ private fun clear(context: Context) {
54
+ AppBlockerPrefs.get(context).edit().remove(KEY_EXPIRES_AT).apply()
55
+ }
56
+
57
+ /** Seconds remaining on the active unlock, or 0 if none / already expired. */
58
+ fun remainingSeconds(context: Context): Int {
59
+ val expiresAtMs = AppBlockerPrefs.get(context).getLong(KEY_EXPIRES_AT, 0L)
60
+ val remainingMs = expiresAtMs - System.currentTimeMillis()
61
+ return if (remainingMs > 0) (remainingMs / 1000).toInt() else 0
62
+ }
36
63
  }
37
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
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
@@ -181,8 +181,9 @@ export function isTemporarilyUnlocked(): boolean {
181
181
  return NativeModule.isTemporarilyUnlocked();
182
182
  }
183
183
 
184
+ /** Seconds remaining on the active temporary unlock, or 0 if none. */
184
185
  export function getRemainingUnlockTime(): number {
185
- if (Platform.OS !== "ios") return 0;
186
+ if (Platform.OS === "android") return NativeModule.getRemainingUnlockTimeAndroid();
186
187
  return NativeModule.getRemainingUnlockTime();
187
188
  }
188
189