expo-app-blocker 0.1.59 → 0.1.60

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,16 +21,11 @@ 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
- @Volatile private var paused = false
25
-
26
- private val resumeRunnable = Runnable {
27
- Log.d(TAG, "Temporary unlock expired, resuming blocking")
28
- paused = false
29
- }
24
+ private val unlockController = TemporaryUnlockController(handler)
30
25
 
31
26
  private val pollRunnable = object : Runnable {
32
27
  override fun run() {
33
- if (!paused) {
28
+ if (!unlockController.isUnlocked) {
34
29
  val foregroundPackage = getCurrentForegroundPackage()
35
30
  if (foregroundPackage != null && foregroundPackage != lastForegroundPackage) {
36
31
  Log.d(TAG, "Foreground changed: $foregroundPackage")
@@ -123,15 +118,19 @@ class AppBlockerService : Service() {
123
118
  }
124
119
 
125
120
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
126
- val action = intent?.action
127
- if (action == ACTION_TEMPORARY_UNLOCK) {
128
- val minutes = intent.getIntExtra(EXTRA_DURATION_MINUTES, 0)
129
- if (minutes > 0) {
121
+ when (intent?.action) {
122
+ ACTION_TEMPORARY_UNLOCK -> {
123
+ val minutes = intent.getIntExtra(EXTRA_DURATION_MINUTES, 0)
130
124
  Log.d(TAG, "Temporary unlock for $minutes minutes")
131
- handler.removeCallbacks(resumeRunnable)
132
- paused = true
125
+ unlockController.unlock(minutes)
133
126
  overlayManager.hide()
134
- handler.postDelayed(resumeRunnable, minutes * 60_000L)
127
+ }
128
+ ACTION_RELOCK -> {
129
+ Log.d(TAG, "Relock: ending temporary unlock")
130
+ unlockController.relock()
131
+ // Forget the last-seen app so a blocked app already in the foreground
132
+ // is re-detected and re-blocked on the next poll.
133
+ lastForegroundPackage = null
135
134
  }
136
135
  }
137
136
  return START_STICKY
@@ -200,15 +199,11 @@ class AppBlockerService : Service() {
200
199
  private const val POLL_INTERVAL_MS = 500L
201
200
  private const val LOOKBACK_WINDOW_MS = 10_000L
202
201
  private const val ACTION_TEMPORARY_UNLOCK = "expo.modules.appblocker.TEMPORARY_UNLOCK"
202
+ private const val ACTION_RELOCK = "expo.modules.appblocker.RELOCK"
203
203
  private const val EXTRA_DURATION_MINUTES = "duration_minutes"
204
204
 
205
205
  fun start(context: Context) {
206
- val intent = Intent(context, AppBlockerService::class.java)
207
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
208
- context.startForegroundService(intent)
209
- } else {
210
- context.startService(intent)
211
- }
206
+ startCommand(context, Intent(context, AppBlockerService::class.java))
212
207
  }
213
208
 
214
209
  fun stop(context: Context) {
@@ -221,6 +216,17 @@ class AppBlockerService : Service() {
221
216
  action = ACTION_TEMPORARY_UNLOCK
222
217
  putExtra(EXTRA_DURATION_MINUTES, durationMinutes)
223
218
  }
219
+ startCommand(context, intent)
220
+ }
221
+
222
+ fun relock(context: Context) {
223
+ val intent = Intent(context, AppBlockerService::class.java).apply {
224
+ action = ACTION_RELOCK
225
+ }
226
+ startCommand(context, intent)
227
+ }
228
+
229
+ private fun startCommand(context: Context, intent: Intent) {
224
230
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
225
231
  context.startForegroundService(intent)
226
232
  } else {
@@ -118,6 +118,11 @@ class ExpoAppBlockerModule : Module() {
118
118
  Log.d(TAG, "temporaryUnlockAndroid: $durationMinutes minutes")
119
119
  }
120
120
 
121
+ Function("relockAndroid") {
122
+ AppBlockerService.relock(context)
123
+ Log.d(TAG, "relockAndroid")
124
+ }
125
+
121
126
  AsyncFunction("getInstalledApps") {
122
127
  val pm = context.packageManager
123
128
  val intent = Intent(Intent.ACTION_MAIN).apply {
@@ -0,0 +1,37 @@
1
+ package expo.modules.appblocker
2
+
3
+ import android.os.Handler
4
+
5
+ /**
6
+ * Owns the "temporary unlock" state for Android blocking.
7
+ *
8
+ * 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.
11
+ *
12
+ * Not thread-safe beyond the [isUnlocked] read: drive [unlock] / [relock] from
13
+ * the same (main) thread the [Handler] is bound to.
14
+ */
15
+ class TemporaryUnlockController(private val handler: Handler) {
16
+ @Volatile private var unlocked = false
17
+
18
+ private val expireRunnable = Runnable { unlocked = false }
19
+
20
+ /** True while a temporary unlock is in effect (blocking should be suppressed). */
21
+ val isUnlocked: Boolean
22
+ get() = unlocked
23
+
24
+ /** Suppress blocking for [durationMinutes], replacing any pending expiry. No-op if <= 0. */
25
+ fun unlock(durationMinutes: Int) {
26
+ if (durationMinutes <= 0) return
27
+ handler.removeCallbacks(expireRunnable)
28
+ unlocked = true
29
+ handler.postDelayed(expireRunnable, durationMinutes * 60_000L)
30
+ }
31
+
32
+ /** End any active unlock immediately, restoring blocking. */
33
+ fun relock() {
34
+ handler.removeCallbacks(expireRunnable)
35
+ unlocked = false
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-app-blocker",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
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
@@ -187,8 +187,9 @@ export function getRemainingUnlockTime(): number {
187
187
  }
188
188
 
189
189
  export async function relockApps(): Promise<RelockResult> {
190
- if (Platform.OS !== "ios") {
191
- throw new Error("Relock is only available on iOS");
190
+ if (Platform.OS === "android") {
191
+ NativeModule.relockAndroid();
192
+ return { locked: true };
192
193
  }
193
194
  return NativeModule.relockApps();
194
195
  }