expo-app-blocker 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 expo-app-blocker contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ # expo-app-blocker
2
+
3
+ Cross-platform app blocking module for Expo. Block other apps and redirect users to your app.
4
+
5
+ **Android**: UsageStatsManager + Foreground Service + System Overlay
6
+ **iOS**: Screen Time API (FamilyControls + ManagedSettings + DeviceActivity)
7
+
8
+ ## Features
9
+
10
+ - Block specific apps from being used
11
+ - Detect when a blocked app is opened (Android: polling, iOS: system shield)
12
+ - Customizable iOS shield overlay (icon, title, subtitle, button text, colors)
13
+ - Temporary unlock with timer
14
+ - Auto-relock when unlock period expires (iOS DeviceActivityMonitor extension)
15
+ - Notification when blocked app is detected
16
+ - Persist blocked apps across app restarts
17
+ - Native view for rendering blocked app names/icons on iOS (Apple's opaque tokens)
18
+ - Automatic iOS extension target creation via `@bacons/apple-targets`
19
+ - Full Expo Config Plugin - no manual native setup required
20
+
21
+ ## Prerequisites
22
+
23
+ ### Apple Developer Portal (iOS)
24
+
25
+ 1. Register **4 App IDs** with **Family Controls** and **App Groups** capabilities:
26
+ - `com.yourapp.id` (main app)
27
+ - `com.yourapp.id.DeviceActivityMonitor`
28
+ - `com.yourapp.id.ShieldAction`
29
+ - `com.yourapp.id.ShieldConfiguration`
30
+
31
+ 2. Create an **App Group**: `group.com.yourapp.blocker` (or your chosen identifier)
32
+
33
+ 3. Assign the App Group to all 4 App IDs
34
+
35
+ 4. Request **Family Controls** capability approval (works in dev builds without approval)
36
+
37
+ ### Android
38
+
39
+ No special setup required beyond what the config plugin handles automatically.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npx expo install expo-app-blocker
45
+ ```
46
+
47
+ > `@bacons/apple-targets` is included as a dependency for automatic iOS extension target creation.
48
+
49
+ ## Configuration
50
+
51
+ Add the plugin to your `app.json`:
52
+
53
+ ```json
54
+ {
55
+ "expo": {
56
+ "scheme": "myapp",
57
+ "ios": {
58
+ "bundleIdentifier": "com.yourapp.id",
59
+ "appleTeamId": "YOUR_TEAM_ID"
60
+ },
61
+ "android": {
62
+ "package": "com.yourapp.id"
63
+ },
64
+ "plugins": [
65
+ ["expo-app-blocker", {
66
+ "ios": {
67
+ "appGroup": "group.com.yourapp.blocker",
68
+ "shield": {
69
+ "title": "Hold on!",
70
+ "subtitle": "{appName} is blocked.",
71
+ "primaryButtonLabel": "Earn Free Time",
72
+ "secondaryButtonLabel": "Not now",
73
+ "primaryButtonColor": "#7cb518",
74
+ "icon": "./assets/shield-icon.png"
75
+ }
76
+ }
77
+ }]
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Plugin Options
84
+
85
+ | Option | Type | Default | Description |
86
+ |---|---|---|---|
87
+ | `ios.appGroup` | `string` | Required | App Group identifier for shared data |
88
+ | `ios.shield.title` | `string` | `"Hold on!"` | Shield overlay title |
89
+ | `ios.shield.subtitle` | `string` | `"{appName} is blocked."` | Shield subtitle. `{appName}` is replaced with the blocked app name |
90
+ | `ios.shield.primaryButtonLabel` | `string` | `"Earn Free Time"` | Primary button text |
91
+ | `ios.shield.secondaryButtonLabel` | `string\|null` | `"Not now"` | Secondary button text. Set to `null` to hide |
92
+ | `ios.shield.primaryButtonColor` | `string` | `"#7cb518"` | Primary button background color (hex) |
93
+ | `ios.shield.backgroundBlurStyle` | `string` | `"systemThickMaterial"` | iOS blur style |
94
+ | `ios.shield.icon` | `string` | SF Symbol | Path to custom shield icon PNG (relative to project root, e.g. `"./assets/shield-icon.png"`) |
95
+ | `android.notificationTitle` | `string` | `"App Blocked"` | Notification title |
96
+ | `android.notificationText` | `string` | `"{appName} is blocked."` | Notification text |
97
+
98
+ ### EAS Build
99
+
100
+ For EAS Build, declare extensions for credential management:
101
+
102
+ ```json
103
+ {
104
+ "extra": {
105
+ "eas": {
106
+ "build": {
107
+ "experimental": {
108
+ "ios": {
109
+ "appExtensions": [
110
+ {
111
+ "targetName": "DeviceActivityMonitor",
112
+ "bundleIdentifier": "com.yourapp.id.DeviceActivityMonitor",
113
+ "entitlements": {
114
+ "com.apple.developer.family-controls": true,
115
+ "com.apple.security.application-groups": ["group.com.yourapp.blocker"]
116
+ }
117
+ },
118
+ {
119
+ "targetName": "ShieldAction",
120
+ "bundleIdentifier": "com.yourapp.id.ShieldAction",
121
+ "entitlements": {
122
+ "com.apple.developer.family-controls": true,
123
+ "com.apple.security.application-groups": ["group.com.yourapp.blocker"]
124
+ }
125
+ },
126
+ {
127
+ "targetName": "ShieldConfiguration",
128
+ "bundleIdentifier": "com.yourapp.id.ShieldConfiguration",
129
+ "entitlements": {
130
+ "com.apple.developer.family-controls": true,
131
+ "com.apple.security.application-groups": ["group.com.yourapp.blocker"]
132
+ }
133
+ }
134
+ ]
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Build
144
+
145
+ ```bash
146
+ # Generate native projects
147
+ npx expo prebuild --clean
148
+
149
+ # Run on Android
150
+ npx expo run:android
151
+
152
+ # Run on iOS (physical device required for Screen Time APIs)
153
+ npx expo run:ios --device
154
+ ```
155
+
156
+ ## API Reference
157
+
158
+ ### Permissions
159
+
160
+ ```typescript
161
+ import { getPermissionStatus, requestPermissions } from 'expo-app-blocker';
162
+
163
+ // Check current permission status
164
+ const status = await getPermissionStatus();
165
+ // Returns: { allGranted: boolean, details: AndroidPermissions | IOSPermissions }
166
+
167
+ // Request permissions (iOS: triggers Screen Time authorization)
168
+ const result = await requestPermissions();
169
+ ```
170
+
171
+ ### Android: Permission Settings
172
+
173
+ ```typescript
174
+ import { openOverlaySettings, openUsageStatsSettings } from 'expo-app-blocker';
175
+
176
+ // Open system settings for overlay permission
177
+ openOverlaySettings();
178
+
179
+ // Open system settings for usage access
180
+ openUsageStatsSettings();
181
+ ```
182
+
183
+ ### Android: App Blocking
184
+
185
+ ```typescript
186
+ import { setBlockedApps, getBlockedApps, getInstalledApps } from 'expo-app-blocker';
187
+
188
+ // Get list of installed apps
189
+ const apps = await getInstalledApps();
190
+ // Returns: [{ packageName: string, name: string }]
191
+
192
+ // Set which apps to block (by package name)
193
+ setBlockedApps(['com.instagram.android', 'com.google.android.youtube']);
194
+
195
+ // Get currently blocked apps
196
+ const blocked = getBlockedApps();
197
+ // Returns: ['com.instagram.android', 'com.google.android.youtube']
198
+ ```
199
+
200
+ ### Android: Monitoring Control
201
+
202
+ ```typescript
203
+ import { startMonitoring, stopMonitoring } from 'expo-app-blocker';
204
+
205
+ // Start the foreground service (auto-started on module init)
206
+ startMonitoring();
207
+
208
+ // Stop monitoring
209
+ stopMonitoring();
210
+ ```
211
+
212
+ ### iOS: App Selection
213
+
214
+ ```typescript
215
+ import { presentFamilyActivityPicker } from 'expo-app-blocker';
216
+
217
+ // Opens the iOS system app/category picker
218
+ const items = await presentFamilyActivityPicker();
219
+ // Returns: IOSBlockedItem[] - opaque tokens for selected apps/categories
220
+ ```
221
+
222
+ ### iOS: Block Configuration
223
+
224
+ ```typescript
225
+ import { setBlockConfiguration, getBlockConfiguration, clearAllBlocks } from 'expo-app-blocker';
226
+
227
+ // Apply blocks (shields appear on selected apps)
228
+ await setBlockConfiguration({
229
+ blockedItems: items, // from presentFamilyActivityPicker()
230
+ isActive: true,
231
+ });
232
+
233
+ // Get current configuration
234
+ const config = getBlockConfiguration();
235
+
236
+ // Remove all blocks
237
+ clearAllBlocks();
238
+ ```
239
+
240
+ ### iOS: Temporary Unlock
241
+
242
+ ```typescript
243
+ import {
244
+ temporaryUnlock,
245
+ isTemporarilyUnlocked,
246
+ getRemainingUnlockTime,
247
+ relockApps,
248
+ } from 'expo-app-blocker';
249
+
250
+ // Unlock for N minutes (removes shields temporarily)
251
+ const result = await temporaryUnlock(15);
252
+ // Returns: { unlocked: boolean, expiresAt: number }
253
+
254
+ // Check if currently unlocked
255
+ const unlocked = isTemporarilyUnlocked();
256
+
257
+ // Get remaining seconds
258
+ const seconds = getRemainingUnlockTime();
259
+
260
+ // Re-lock immediately
261
+ await relockApps();
262
+ ```
263
+
264
+ ### iOS: Shield Button Events
265
+
266
+ ```typescript
267
+ import { addPendingUnlockListener, checkAndClearPendingUnlock } from 'expo-app-blocker';
268
+
269
+ // Check if user tapped shield button while app was closed
270
+ const hasPending = checkAndClearPendingUnlock();
271
+
272
+ // Listen for real-time shield button taps
273
+ const subscription = addPendingUnlockListener(() => {
274
+ // User tapped "Earn Free Time" on the shield
275
+ // Navigate to your unlock/quiz screen
276
+ });
277
+
278
+ // Clean up
279
+ subscription?.remove();
280
+ ```
281
+
282
+ ### iOS: Native Blocked Apps List
283
+
284
+ Renders blocked app tokens with real names and icons using Apple's native Label view:
285
+
286
+ ```typescript
287
+ import { BlockedAppsNativeList } from 'expo-app-blocker';
288
+
289
+ // In your component
290
+ <BlockedAppsNativeList
291
+ items={blockedItems}
292
+ selectionData={selectionBase64}
293
+ style={{ minHeight: 200 }}
294
+ />
295
+ ```
296
+
297
+ ## Platform Notes
298
+
299
+ ### iOS Limitations
300
+
301
+ - **Physical device required** - Screen Time APIs don't work in the simulator
302
+ - **App tokens are opaque** - You cannot extract app names/bundle IDs from tokens. Use `BlockedAppsNativeList` to render them with Apple's native Label
303
+ - **FamilyActivityPicker is required** - No API to enumerate installed apps on iOS
304
+ - **Shield customization is limited** - Only icon, title, subtitle, button labels, and colors can be changed. No custom views, fonts, or animations
305
+ - **Cannot open apps from shield** - Use notifications as a workaround to redirect users to your app
306
+
307
+ ### Android Limitations
308
+
309
+ - **~500ms detection delay** - The foreground polling interval means a blocked app is briefly visible before the overlay appears
310
+ - **Overlay permission requires manual grant** - Users must enable "Display over other apps" in system settings
311
+ - **Usage access permission requires manual grant** - Users must enable in system settings
312
+ - **OEM battery optimizations** - Some manufacturers (Xiaomi, Samsung, etc.) may kill the foreground service. Users may need to disable battery optimization for your app
313
+
314
+ ### Android Permissions (auto-added by config plugin)
315
+
316
+ | Permission | Purpose |
317
+ |---|---|
318
+ | `SYSTEM_ALERT_WINDOW` | Display blocking overlay |
319
+ | `FOREGROUND_SERVICE` | Run monitoring service |
320
+ | `FOREGROUND_SERVICE_SPECIAL_USE` | Required for Android 14+ |
321
+ | `PACKAGE_USAGE_STATS` | Detect foreground app |
322
+ | `RECEIVE_BOOT_COMPLETED` | Auto-start service on boot |
323
+ | `POST_NOTIFICATIONS` | Show blocked app notifications |
324
+
325
+ ## How It Works
326
+
327
+ ### Android Flow
328
+
329
+ 1. `ExpoAppBlockerModule` starts `AppBlockerService` as a foreground service
330
+ 2. Service polls `UsageStatsManager` every 500ms to detect the foreground app
331
+ 3. If the foreground app is in the blocked list:
332
+ - A full-screen overlay covers the screen
333
+ - A notification is sent with a deep link to your app
334
+ - Your app is brought to the foreground
335
+ 4. Blocked apps are persisted in SharedPreferences
336
+
337
+ ### iOS Flow
338
+
339
+ 1. User authorizes Screen Time via `requestPermissions()`
340
+ 2. User selects apps to block via `presentFamilyActivityPicker()`
341
+ 3. `setBlockConfiguration()` applies shields via `ManagedSettingsStore`
342
+ 4. When a blocked app is opened, iOS shows the shield overlay (customized via `ShieldConfigurationExtension`)
343
+ 5. When the user taps the shield button, `ShieldActionExtension` sends a notification via Darwin notification center
344
+ 6. Your app receives the event and can navigate to an unlock flow
345
+ 7. `temporaryUnlock()` removes shields for a duration
346
+ 8. `DeviceActivityMonitor` extension re-applies shields when the unlock period expires
347
+
348
+ ## Contributing
349
+
350
+ Contributions are welcome! Please open an issue or PR.
351
+
352
+ ## License
353
+
354
+ MIT
@@ -0,0 +1,49 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+
4
+ group = 'expo.modules.appblocker'
5
+ version = '0.1.0'
6
+
7
+ buildscript {
8
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
9
+ if (expoModulesCorePlugin.exists()) {
10
+ apply from: expoModulesCorePlugin
11
+ applyKotlinExpoModulesCorePlugin()
12
+ }
13
+
14
+ repositories {
15
+ mavenCentral()
16
+ }
17
+
18
+ dependencies {
19
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20")
20
+ }
21
+ }
22
+
23
+ android {
24
+ namespace "expo.modules.appblocker"
25
+
26
+ compileSdkVersion safeExtGet("compileSdkVersion", 35)
27
+
28
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
29
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
30
+ compileOptions {
31
+ sourceCompatibility JavaVersion.VERSION_17
32
+ targetCompatibility JavaVersion.VERSION_17
33
+ }
34
+
35
+ kotlinOptions {
36
+ jvmTarget = JavaVersion.VERSION_17.majorVersion
37
+ }
38
+ }
39
+
40
+ defaultConfig {
41
+ minSdkVersion safeExtGet("minSdkVersion", 24)
42
+ targetSdkVersion safeExtGet("targetSdkVersion", 35)
43
+ }
44
+ }
45
+
46
+ dependencies {
47
+ implementation project(':expo-modules-core')
48
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.20"
49
+ }
@@ -0,0 +1 @@
1
+ <manifest/>
@@ -0,0 +1,21 @@
1
+ package expo.modules.appblocker
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+
6
+ object AppBlockerPrefs {
7
+ const val PREFS_NAME = "expo_app_blocker_prefs"
8
+ const val KEY_BLOCKED_PACKAGES = "blocked_packages"
9
+
10
+ fun get(context: Context): SharedPreferences =
11
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
12
+
13
+ fun getBlockedPackages(context: Context): Set<String> =
14
+ get(context).getStringSet(KEY_BLOCKED_PACKAGES, emptySet()) ?: emptySet()
15
+
16
+ fun setBlockedPackages(context: Context, packages: Collection<String>) {
17
+ get(context).edit()
18
+ .putStringSet(KEY_BLOCKED_PACKAGES, packages.toSet())
19
+ .apply()
20
+ }
21
+ }
@@ -0,0 +1,176 @@
1
+ package expo.modules.appblocker
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.app.Service
8
+ import android.app.usage.UsageEvents
9
+ import android.app.usage.UsageStatsManager
10
+ import android.content.Context
11
+ import android.content.Intent
12
+ import android.net.Uri
13
+ import android.os.Build
14
+ import android.os.Handler
15
+ import android.os.IBinder
16
+ import android.os.Looper
17
+ import android.util.Log
18
+ import androidx.core.app.NotificationCompat
19
+
20
+ class AppBlockerService : Service() {
21
+ private val handler = Handler(Looper.getMainLooper())
22
+ private var lastForegroundPackage: String? = null
23
+ private lateinit var overlayManager: OverlayManager
24
+
25
+ private val pollRunnable = object : Runnable {
26
+ override fun run() {
27
+ val foregroundPackage = getCurrentForegroundPackage()
28
+ if (foregroundPackage != null && foregroundPackage != lastForegroundPackage) {
29
+ Log.d(TAG, "Foreground changed: $foregroundPackage")
30
+ lastForegroundPackage = foregroundPackage
31
+ handleForegroundChange(foregroundPackage)
32
+ }
33
+ handler.postDelayed(this, POLL_INTERVAL_MS)
34
+ }
35
+ }
36
+
37
+ override fun onBind(intent: Intent?): IBinder? = null
38
+
39
+ override fun onCreate() {
40
+ super.onCreate()
41
+ Log.d(TAG, "AppBlockerService onCreate")
42
+ overlayManager = OverlayManager(this)
43
+ createChannelsIfNeeded()
44
+ startForeground(NOTIFICATION_ID, buildNotification())
45
+ handler.post(pollRunnable)
46
+ }
47
+
48
+ private fun handleForegroundChange(foregroundPackage: String) {
49
+ val blocked = AppBlockerPrefs.getBlockedPackages(this)
50
+ if (foregroundPackage in blocked) {
51
+ Log.d(TAG, "Blocked app in foreground: $foregroundPackage")
52
+ overlayManager.show(foregroundPackage)
53
+ showBlockedNotification(foregroundPackage)
54
+ } else {
55
+ overlayManager.hide()
56
+ }
57
+ }
58
+
59
+ private fun showBlockedNotification(packageName: String) {
60
+ val appName = try {
61
+ val pm = this.packageManager
62
+ val appInfo = pm.getApplicationInfo(packageName, 0)
63
+ pm.getApplicationLabel(appInfo).toString()
64
+ } catch (e: Exception) {
65
+ packageName
66
+ }
67
+
68
+ val scheme = this.packageName.replace(".", "-")
69
+ val deepLinkIntent = Intent(
70
+ Intent.ACTION_VIEW,
71
+ Uri.parse("${scheme}://blocked?app=${Uri.encode(appName)}&package=${Uri.encode(packageName)}")
72
+ ).apply {
73
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
74
+ }
75
+
76
+ val pendingIntent = PendingIntent.getActivity(
77
+ this, 0, deepLinkIntent,
78
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
79
+ )
80
+
81
+ val notification = NotificationCompat.Builder(this, BLOCKED_CHANNEL_ID)
82
+ .setContentTitle("App Blocked")
83
+ .setContentText("$appName is blocked. Tap to earn free time!")
84
+ .setSmallIcon(applicationInfo.icon)
85
+ .setAutoCancel(true)
86
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
87
+ .setContentIntent(pendingIntent)
88
+ .build()
89
+
90
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
91
+ manager.notify(BLOCKED_NOTIFICATION_ID, notification)
92
+ }
93
+
94
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
95
+ Log.d(TAG, "AppBlockerService onStartCommand")
96
+ return START_STICKY
97
+ }
98
+
99
+ override fun onDestroy() {
100
+ Log.d(TAG, "AppBlockerService onDestroy")
101
+ handler.removeCallbacks(pollRunnable)
102
+ overlayManager.hide()
103
+ super.onDestroy()
104
+ }
105
+
106
+ private fun getCurrentForegroundPackage(): String? {
107
+ val usageStatsManager =
108
+ getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
109
+ val endTime = System.currentTimeMillis()
110
+ val beginTime = endTime - LOOKBACK_WINDOW_MS
111
+ val events = usageStatsManager.queryEvents(beginTime, endTime)
112
+ val event = UsageEvents.Event()
113
+ var latestForeground: String? = null
114
+ while (events.hasNextEvent()) {
115
+ events.getNextEvent(event)
116
+ if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
117
+ latestForeground = event.packageName
118
+ }
119
+ }
120
+ return latestForeground
121
+ }
122
+
123
+ private fun createChannelsIfNeeded() {
124
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
125
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
126
+
127
+ val serviceChannel = NotificationChannel(
128
+ CHANNEL_ID, "App Blocker", NotificationManager.IMPORTANCE_LOW
129
+ ).apply {
130
+ description = "Keeps the app blocker running"
131
+ setShowBadge(false)
132
+ }
133
+ manager.createNotificationChannel(serviceChannel)
134
+
135
+ val blockedChannel = NotificationChannel(
136
+ BLOCKED_CHANNEL_ID, "Blocked App Alerts", NotificationManager.IMPORTANCE_HIGH
137
+ ).apply {
138
+ description = "Notifications when a blocked app is detected"
139
+ }
140
+ manager.createNotificationChannel(blockedChannel)
141
+ }
142
+ }
143
+
144
+ private fun buildNotification(): Notification =
145
+ NotificationCompat.Builder(this, CHANNEL_ID)
146
+ .setContentTitle("App Blocker")
147
+ .setContentText("Monitoring blocked apps")
148
+ .setSmallIcon(applicationInfo.icon)
149
+ .setOngoing(true)
150
+ .setPriority(NotificationCompat.PRIORITY_LOW)
151
+ .build()
152
+
153
+ companion object {
154
+ private const val TAG = "ExpoAppBlocker"
155
+ private const val CHANNEL_ID = "expo_app_blocker_channel"
156
+ private const val BLOCKED_CHANNEL_ID = "expo_app_blocker_blocked"
157
+ private const val NOTIFICATION_ID = 9001
158
+ private const val BLOCKED_NOTIFICATION_ID = 9002
159
+ private const val POLL_INTERVAL_MS = 500L
160
+ private const val LOOKBACK_WINDOW_MS = 10_000L
161
+
162
+ fun start(context: Context) {
163
+ val intent = Intent(context, AppBlockerService::class.java)
164
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
165
+ context.startForegroundService(intent)
166
+ } else {
167
+ context.startService(intent)
168
+ }
169
+ }
170
+
171
+ fun stop(context: Context) {
172
+ val intent = Intent(context, AppBlockerService::class.java)
173
+ context.stopService(intent)
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,19 @@
1
+ package expo.modules.appblocker
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.util.Log
7
+
8
+ class BootReceiver : BroadcastReceiver() {
9
+ override fun onReceive(context: Context, intent: Intent) {
10
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
11
+ Log.d(TAG, "BootReceiver: BOOT_COMPLETED received, starting service")
12
+ AppBlockerService.start(context.applicationContext)
13
+ }
14
+ }
15
+
16
+ companion object {
17
+ private const val TAG = "ExpoAppBlocker"
18
+ }
19
+ }