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.
- package/android/src/main/java/expo/modules/appblocker/AppBlockerService.kt +26 -20
- package/android/src/main/java/expo/modules/appblocker/ExpoAppBlockerModule.kt +5 -0
- package/android/src/main/java/expo/modules/appblocker/TemporaryUnlockController.kt +37 -0
- package/package.json +1 -1
- package/src/index.ts +3 -2
|
@@ -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
|
-
|
|
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 (!
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
paused = true
|
|
125
|
+
unlockController.unlock(minutes)
|
|
133
126
|
overlayManager.hide()
|
|
134
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
191
|
-
|
|
190
|
+
if (Platform.OS === "android") {
|
|
191
|
+
NativeModule.relockAndroid();
|
|
192
|
+
return { locked: true };
|
|
192
193
|
}
|
|
193
194
|
return NativeModule.relockApps();
|
|
194
195
|
}
|