@elizaos/capacitor-appblocker 1.0.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/ElizaosCapacitorAppblocker.podspec +18 -0
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/ai/eliza/plugins/appblocker/AppBlockerForegroundService.kt +345 -0
- package/android/src/main/java/ai/eliza/plugins/appblocker/AppBlockerPlugin.kt +260 -0
- package/android/src/main/java/ai/eliza/plugins/appblocker/AppBlockerStateStore.kt +65 -0
- package/dist/esm/definitions.d.ts +53 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +14 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +51 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +66 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +69 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/AppBlockerPlugin/AppBlockerPlugin.swift +196 -0
- package/ios/Sources/AppBlockerPlugin/AppBlockerShared.swift +102 -0
- package/ios/Sources/AppBlockerPlugin/FamilyActivityPickerBridge.swift +47 -0
- package/package.json +77 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ElizaosCapacitorAppblocker'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license'] || { :type => 'MIT' }
|
|
10
|
+
s.homepage = 'https://elizaos.ai'
|
|
11
|
+
s.authors = { 'elizaOS' => 'dev@elizaos.ai' }
|
|
12
|
+
s.source = { :git => 'https://github.com/elizaOS/eliza.git', :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.frameworks = 'FamilyControls', 'ManagedSettings'
|
|
16
|
+
s.dependency 'Capacitor'
|
|
17
|
+
s.swift_version = '5.1'
|
|
18
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
|
|
4
|
+
androidxCoreVersion = project.hasProperty('androidxCoreVersion') ? rootProject.ext.androidxCoreVersion : '1.12.0'
|
|
5
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
|
|
6
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
apply plugin: 'com.android.library'
|
|
10
|
+
android {
|
|
11
|
+
namespace = "ai.eliza.plugins.appblocker"
|
|
12
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
|
13
|
+
|
|
14
|
+
defaultConfig {
|
|
15
|
+
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 26
|
|
16
|
+
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
|
17
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
buildTypes {
|
|
21
|
+
release {
|
|
22
|
+
minifyEnabled false
|
|
23
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
compileOptions {
|
|
28
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
29
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
repositories {
|
|
35
|
+
google()
|
|
36
|
+
maven {
|
|
37
|
+
url = uri(rootProject.ext.mavenCentralMirrorUrl)
|
|
38
|
+
}
|
|
39
|
+
mavenCentral()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
dependencies {
|
|
43
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
44
|
+
implementation project(':capacitor-android')
|
|
45
|
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
|
46
|
+
implementation "androidx.core:core-ktx:$androidxCoreVersion"
|
|
47
|
+
|
|
48
|
+
testImplementation "junit:junit:$junitVersion"
|
|
49
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
50
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
4
|
+
|
|
5
|
+
<uses-permission
|
|
6
|
+
android:name="android.permission.PACKAGE_USAGE_STATS"
|
|
7
|
+
tools:ignore="ProtectedPermissions" />
|
|
8
|
+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
9
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
10
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
|
11
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
12
|
+
|
|
13
|
+
<application>
|
|
14
|
+
<service
|
|
15
|
+
android:name=".AppBlockerForegroundService"
|
|
16
|
+
android:enabled="true"
|
|
17
|
+
android:exported="false"
|
|
18
|
+
android:foregroundServiceType="specialUse">
|
|
19
|
+
<property
|
|
20
|
+
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
21
|
+
android:value="app_blocking_monitor" />
|
|
22
|
+
</service>
|
|
23
|
+
</application>
|
|
24
|
+
</manifest>
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
package ai.eliza.plugins.appblocker
|
|
2
|
+
|
|
3
|
+
import android.app.Notification
|
|
4
|
+
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.Service
|
|
7
|
+
import android.app.usage.UsageEvents
|
|
8
|
+
import android.app.usage.UsageStatsManager
|
|
9
|
+
import android.content.ActivityNotFoundException
|
|
10
|
+
import android.content.Context
|
|
11
|
+
import android.content.Intent
|
|
12
|
+
import android.graphics.Color
|
|
13
|
+
import android.graphics.PixelFormat
|
|
14
|
+
import android.os.Build
|
|
15
|
+
import android.os.Handler
|
|
16
|
+
import android.os.IBinder
|
|
17
|
+
import android.os.Looper
|
|
18
|
+
import android.provider.Settings
|
|
19
|
+
import android.view.Gravity
|
|
20
|
+
import android.view.View
|
|
21
|
+
import android.view.WindowManager
|
|
22
|
+
import android.widget.Button
|
|
23
|
+
import android.widget.FrameLayout
|
|
24
|
+
import android.widget.LinearLayout
|
|
25
|
+
import android.widget.TextView
|
|
26
|
+
import androidx.core.app.NotificationCompat
|
|
27
|
+
import java.text.SimpleDateFormat
|
|
28
|
+
import java.util.Date
|
|
29
|
+
import java.util.Locale
|
|
30
|
+
|
|
31
|
+
class AppBlockerForegroundService : Service() {
|
|
32
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
33
|
+
private var polling = false
|
|
34
|
+
private var ownPackageName = ""
|
|
35
|
+
private var overlayView: View? = null
|
|
36
|
+
private var windowManager: WindowManager? = null
|
|
37
|
+
|
|
38
|
+
private val pollRunnable = object : Runnable {
|
|
39
|
+
override fun run() {
|
|
40
|
+
if (!polling) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
checkForegroundApp()
|
|
44
|
+
if (polling) {
|
|
45
|
+
handler.postDelayed(this, POLL_INTERVAL_MS)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
51
|
+
|
|
52
|
+
override fun onCreate() {
|
|
53
|
+
super.onCreate()
|
|
54
|
+
ownPackageName = packageName
|
|
55
|
+
windowManager = getSystemService(WINDOW_SERVICE) as? WindowManager
|
|
56
|
+
createNotificationChannel()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
60
|
+
when (intent?.action) {
|
|
61
|
+
ACTION_STOP -> {
|
|
62
|
+
stopPolling()
|
|
63
|
+
hideBlockingOverlay()
|
|
64
|
+
stopForegroundCompat()
|
|
65
|
+
stopSelf()
|
|
66
|
+
return START_NOT_STICKY
|
|
67
|
+
}
|
|
68
|
+
ACTION_START, null -> Unit
|
|
69
|
+
else -> return START_NOT_STICKY
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
val saved = AppBlockerStateStore.load(this)
|
|
73
|
+
if (saved == null || saved.packageNames.isEmpty()) {
|
|
74
|
+
stopSelf()
|
|
75
|
+
return START_NOT_STICKY
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
startForeground(NOTIFICATION_ID, buildNotification(saved))
|
|
79
|
+
startPolling()
|
|
80
|
+
|
|
81
|
+
val endsAtEpochMs = saved.endsAtEpochMs
|
|
82
|
+
if (endsAtEpochMs != null) {
|
|
83
|
+
val delayMs = endsAtEpochMs - System.currentTimeMillis()
|
|
84
|
+
if (delayMs > 0) {
|
|
85
|
+
handler.postDelayed({
|
|
86
|
+
AppBlockerStateStore.clear(this)
|
|
87
|
+
stopPolling()
|
|
88
|
+
hideBlockingOverlay()
|
|
89
|
+
stopForegroundCompat()
|
|
90
|
+
stopSelf()
|
|
91
|
+
}, delayMs)
|
|
92
|
+
} else {
|
|
93
|
+
AppBlockerStateStore.clear(this)
|
|
94
|
+
stopSelf()
|
|
95
|
+
return START_NOT_STICKY
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return START_STICKY
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override fun onDestroy() {
|
|
103
|
+
stopPolling()
|
|
104
|
+
hideBlockingOverlay()
|
|
105
|
+
super.onDestroy()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private fun startPolling() {
|
|
109
|
+
if (polling) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
polling = true
|
|
113
|
+
handler.post(pollRunnable)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun stopPolling() {
|
|
117
|
+
polling = false
|
|
118
|
+
handler.removeCallbacks(pollRunnable)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun checkForegroundApp() {
|
|
122
|
+
val saved = AppBlockerStateStore.load(this)
|
|
123
|
+
if (saved == null || saved.packageNames.isEmpty()) {
|
|
124
|
+
hideBlockingOverlay()
|
|
125
|
+
stopPolling()
|
|
126
|
+
stopForegroundCompat()
|
|
127
|
+
stopSelf()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!Settings.canDrawOverlays(this)) {
|
|
132
|
+
hideBlockingOverlay()
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
val foregroundPackage = getForegroundPackage() ?: return
|
|
137
|
+
val shouldBlock = foregroundPackage != ownPackageName &&
|
|
138
|
+
foregroundPackage != "com.android.launcher" &&
|
|
139
|
+
!foregroundPackage.contains("launcher", ignoreCase = true) &&
|
|
140
|
+
AppBlockerStateStore.isBlocked(this, foregroundPackage)
|
|
141
|
+
|
|
142
|
+
if (shouldBlock) {
|
|
143
|
+
showBlockingOverlay(saved)
|
|
144
|
+
} else {
|
|
145
|
+
hideBlockingOverlay()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun getForegroundPackage(): String? {
|
|
150
|
+
val usageStatsManager = getSystemService("usagestats") as? UsageStatsManager ?: return null
|
|
151
|
+
val now = System.currentTimeMillis()
|
|
152
|
+
val usageEvents = usageStatsManager.queryEvents(now - 2_000, now)
|
|
153
|
+
val event = UsageEvents.Event()
|
|
154
|
+
var packageName: String? = null
|
|
155
|
+
while (usageEvents.hasNextEvent()) {
|
|
156
|
+
usageEvents.getNextEvent(event)
|
|
157
|
+
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
|
|
158
|
+
packageName = event.packageName
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return packageName
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun showBlockingOverlay(saved: SavedAppBlock) {
|
|
165
|
+
if (overlayView != null) {
|
|
166
|
+
updateOverlayMessage(saved)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
val overlayCard = LinearLayout(this).apply {
|
|
171
|
+
orientation = LinearLayout.VERTICAL
|
|
172
|
+
gravity = Gravity.CENTER
|
|
173
|
+
setPadding(64, 64, 64, 64)
|
|
174
|
+
setBackgroundColor(Color.parseColor("#E8EFE2"))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
val titleView = TextView(this).apply {
|
|
178
|
+
id = View.generateViewId()
|
|
179
|
+
text = "App Blocked"
|
|
180
|
+
textSize = 28f
|
|
181
|
+
setTextColor(Color.parseColor("#132011"))
|
|
182
|
+
gravity = Gravity.CENTER
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val messageView = TextView(this).apply {
|
|
186
|
+
id = View.generateViewId()
|
|
187
|
+
tag = "message"
|
|
188
|
+
textSize = 16f
|
|
189
|
+
setTextColor(Color.parseColor("#2D3C2B"))
|
|
190
|
+
gravity = Gravity.CENTER
|
|
191
|
+
setPadding(0, 24, 0, 32)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
val homeButton = Button(this).apply {
|
|
195
|
+
text = "Go Home"
|
|
196
|
+
setOnClickListener { goHome() }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
overlayCard.addView(
|
|
200
|
+
titleView,
|
|
201
|
+
LinearLayout.LayoutParams(
|
|
202
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
203
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
204
|
+
),
|
|
205
|
+
)
|
|
206
|
+
overlayCard.addView(
|
|
207
|
+
messageView,
|
|
208
|
+
LinearLayout.LayoutParams(
|
|
209
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
210
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
overlayCard.addView(
|
|
214
|
+
homeButton,
|
|
215
|
+
LinearLayout.LayoutParams(
|
|
216
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
217
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
val overlayRoot = FrameLayout(this).apply {
|
|
222
|
+
setBackgroundColor(Color.parseColor("#CC132011"))
|
|
223
|
+
addView(
|
|
224
|
+
overlayCard,
|
|
225
|
+
FrameLayout.LayoutParams(
|
|
226
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
227
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
228
|
+
Gravity.CENTER,
|
|
229
|
+
).apply {
|
|
230
|
+
marginStart = 48
|
|
231
|
+
marginEnd = 48
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
val windowType = if (Build.VERSION.SDK_INT >= 26) {
|
|
237
|
+
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
|
238
|
+
} else {
|
|
239
|
+
@Suppress("DEPRECATION")
|
|
240
|
+
WindowManager.LayoutParams.TYPE_PHONE
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
val layoutParams = WindowManager.LayoutParams(
|
|
244
|
+
WindowManager.LayoutParams.MATCH_PARENT,
|
|
245
|
+
WindowManager.LayoutParams.MATCH_PARENT,
|
|
246
|
+
windowType,
|
|
247
|
+
768,
|
|
248
|
+
PixelFormat.TRANSLUCENT,
|
|
249
|
+
).apply {
|
|
250
|
+
gravity = Gravity.CENTER
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
updateOverlayMessage(saved, messageView)
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
windowManager?.addView(overlayRoot, layoutParams)
|
|
257
|
+
overlayView = overlayRoot
|
|
258
|
+
} catch (_: Exception) {
|
|
259
|
+
overlayView = null
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private fun updateOverlayMessage(saved: SavedAppBlock, messageView: TextView? = findOverlayMessageView()) {
|
|
264
|
+
val message = saved.endsAtEpochMs?.let { endsAtEpochMs ->
|
|
265
|
+
val formatter = SimpleDateFormat("h:mm a", Locale.getDefault())
|
|
266
|
+
"This app is blocked by Eliza until ${formatter.format(Date(endsAtEpochMs))}."
|
|
267
|
+
} ?: "This app is blocked by Eliza until you unblock it."
|
|
268
|
+
messageView?.text = message
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private fun findOverlayMessageView(): TextView? {
|
|
272
|
+
val root = overlayView as? FrameLayout ?: return null
|
|
273
|
+
return root.findViewWithTag("message") as? TextView
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun hideBlockingOverlay() {
|
|
277
|
+
val currentOverlay = overlayView ?: return
|
|
278
|
+
try {
|
|
279
|
+
windowManager?.removeView(currentOverlay)
|
|
280
|
+
overlayView = null
|
|
281
|
+
} catch (_: Exception) {
|
|
282
|
+
overlayView = null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun goHome() {
|
|
287
|
+
val intent = Intent(Intent.ACTION_MAIN).apply {
|
|
288
|
+
addCategory(Intent.CATEGORY_HOME)
|
|
289
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
startActivity(intent)
|
|
293
|
+
} catch (_: ActivityNotFoundException) {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private fun createNotificationChannel() {
|
|
298
|
+
val channel = NotificationChannel(
|
|
299
|
+
CHANNEL_ID,
|
|
300
|
+
"App Blocker",
|
|
301
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
302
|
+
).apply {
|
|
303
|
+
description = "Eliza is monitoring and blocking selected apps."
|
|
304
|
+
}
|
|
305
|
+
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
306
|
+
notificationManager?.createNotificationChannel(channel)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun buildNotification(saved: SavedAppBlock): Notification {
|
|
310
|
+
val count = saved.packageNames.size
|
|
311
|
+
val countSuffix = if (count == 1) "" else "s"
|
|
312
|
+
val contentText = if (saved.endsAtEpochMs != null) {
|
|
313
|
+
val formatter = SimpleDateFormat("h:mm a", Locale.getDefault())
|
|
314
|
+
val endsAt = formatter.format(Date(saved.endsAtEpochMs))
|
|
315
|
+
"Blocking $count app$countSuffix until $endsAt."
|
|
316
|
+
} else {
|
|
317
|
+
val pronoun = if (count == 1) "it" else "them"
|
|
318
|
+
"Blocking $count app$countSuffix until you unblock $pronoun."
|
|
319
|
+
}
|
|
320
|
+
return NotificationCompat.Builder(this, CHANNEL_ID)
|
|
321
|
+
.setContentTitle("App Blocker Active")
|
|
322
|
+
.setContentText(contentText)
|
|
323
|
+
.setSmallIcon(android.R.drawable.ic_lock_lock)
|
|
324
|
+
.setOngoing(true)
|
|
325
|
+
.build()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private fun stopForegroundCompat() {
|
|
329
|
+
if (Build.VERSION.SDK_INT >= 24) {
|
|
330
|
+
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
331
|
+
} else {
|
|
332
|
+
@Suppress("DEPRECATION")
|
|
333
|
+
stopForeground(true)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
companion object {
|
|
338
|
+
const val ACTION_START = "ai.eliza.plugins.appblocker.ACTION_START"
|
|
339
|
+
const val ACTION_STOP = "ai.eliza.plugins.appblocker.ACTION_STOP"
|
|
340
|
+
|
|
341
|
+
private const val CHANNEL_ID = "eliza_app_blocker"
|
|
342
|
+
private const val NOTIFICATION_ID = 9201
|
|
343
|
+
private const val POLL_INTERVAL_MS = 500L
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
package ai.eliza.plugins.appblocker
|
|
2
|
+
|
|
3
|
+
import android.app.AppOpsManager
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Process
|
|
9
|
+
import android.provider.Settings
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
11
|
+
import com.getcapacitor.JSArray
|
|
12
|
+
import com.getcapacitor.JSObject
|
|
13
|
+
import com.getcapacitor.Plugin
|
|
14
|
+
import com.getcapacitor.PluginCall
|
|
15
|
+
import com.getcapacitor.PluginMethod
|
|
16
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
17
|
+
import java.time.Instant
|
|
18
|
+
|
|
19
|
+
@CapacitorPlugin(name = "ElizaAppBlocker")
|
|
20
|
+
class AppBlockerPlugin : Plugin() {
|
|
21
|
+
@PluginMethod
|
|
22
|
+
override fun checkPermissions(call: PluginCall) {
|
|
23
|
+
call.resolve(buildPermissionResult())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@PluginMethod
|
|
27
|
+
override fun requestPermissions(call: PluginCall) {
|
|
28
|
+
if (!hasUsageAccess()) {
|
|
29
|
+
openSettings(Settings.ACTION_USAGE_ACCESS_SETTINGS, null)
|
|
30
|
+
} else if (!canDrawOverlays()) {
|
|
31
|
+
openSettings(
|
|
32
|
+
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
|
33
|
+
Uri.parse("package:${context.packageName}"),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
call.resolve(buildPermissionResult())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@PluginMethod
|
|
40
|
+
fun getInstalledApps(call: PluginCall) {
|
|
41
|
+
val launcherIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
|
42
|
+
val matches = if (Build.VERSION.SDK_INT >= 33) {
|
|
43
|
+
context.packageManager.queryIntentActivities(
|
|
44
|
+
launcherIntent,
|
|
45
|
+
PackageManager.ResolveInfoFlags.of(0),
|
|
46
|
+
)
|
|
47
|
+
} else {
|
|
48
|
+
@Suppress("DEPRECATION")
|
|
49
|
+
context.packageManager.queryIntentActivities(launcherIntent, 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
val ownPackageName = context.packageName
|
|
53
|
+
val apps = matches
|
|
54
|
+
.asSequence()
|
|
55
|
+
.mapNotNull { resolveInfo ->
|
|
56
|
+
val packageName = resolveInfo.activityInfo?.packageName?.trim().orEmpty()
|
|
57
|
+
if (packageName.isEmpty() || packageName == ownPackageName) {
|
|
58
|
+
return@mapNotNull null
|
|
59
|
+
}
|
|
60
|
+
val displayName = resolveInfo.loadLabel(context.packageManager)
|
|
61
|
+
?.toString()
|
|
62
|
+
?.trim()
|
|
63
|
+
.takeUnless { it.isNullOrEmpty() }
|
|
64
|
+
?: packageName
|
|
65
|
+
JSObject().apply {
|
|
66
|
+
put("packageName", packageName)
|
|
67
|
+
put("displayName", displayName)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
.distinctBy { it.getString("packageName") }
|
|
71
|
+
.sortedBy { it.getString("displayName")?.lowercase() ?: "" }
|
|
72
|
+
.toList()
|
|
73
|
+
|
|
74
|
+
call.resolve(
|
|
75
|
+
JSObject().apply {
|
|
76
|
+
put("apps", JSArray(apps))
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@PluginMethod
|
|
82
|
+
fun selectApps(call: PluginCall) {
|
|
83
|
+
call.resolve(
|
|
84
|
+
JSObject().apply {
|
|
85
|
+
put("apps", JSArray())
|
|
86
|
+
put("cancelled", true)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@PluginMethod
|
|
92
|
+
fun blockApps(call: PluginCall) {
|
|
93
|
+
if (!hasUsageAccess() || !canDrawOverlays()) {
|
|
94
|
+
call.resolve(
|
|
95
|
+
JSObject().apply {
|
|
96
|
+
put("success", false)
|
|
97
|
+
put("endsAt", null as String?)
|
|
98
|
+
put("error", missingPermissionReason())
|
|
99
|
+
put("blockedCount", 0)
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
val explicitPackageNames = call.data.optJSONArray("packageNames")
|
|
106
|
+
val normalizedPackageNames = buildList {
|
|
107
|
+
if (explicitPackageNames == null) return@buildList
|
|
108
|
+
for (index in 0 until explicitPackageNames.length()) {
|
|
109
|
+
val value = explicitPackageNames.optString(index).trim()
|
|
110
|
+
if (value.isNotEmpty()) {
|
|
111
|
+
add(value)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.distinct()
|
|
116
|
+
.sorted()
|
|
117
|
+
|
|
118
|
+
if (normalizedPackageNames.isEmpty()) {
|
|
119
|
+
call.resolve(
|
|
120
|
+
JSObject().apply {
|
|
121
|
+
put("success", false)
|
|
122
|
+
put("endsAt", null as String?)
|
|
123
|
+
put("error", "Select at least one Android app to block.")
|
|
124
|
+
put("blockedCount", 0)
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
val durationMinutes = parseDurationMinutes(call)
|
|
131
|
+
val endsAtEpochMs = durationMinutes?.let { System.currentTimeMillis() + (it * 60_000L) }
|
|
132
|
+
|
|
133
|
+
AppBlockerStateStore.save(
|
|
134
|
+
context = context,
|
|
135
|
+
packageNames = normalizedPackageNames,
|
|
136
|
+
endsAtEpochMs = endsAtEpochMs,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
val serviceIntent = Intent(context, AppBlockerForegroundService::class.java).apply {
|
|
140
|
+
action = AppBlockerForegroundService.ACTION_START
|
|
141
|
+
}
|
|
142
|
+
ContextCompat.startForegroundService(context, serviceIntent)
|
|
143
|
+
|
|
144
|
+
call.resolve(
|
|
145
|
+
JSObject().apply {
|
|
146
|
+
put("success", true)
|
|
147
|
+
put("endsAt", endsAtEpochMs?.let { Instant.ofEpochMilli(it).toString() })
|
|
148
|
+
put("blockedCount", normalizedPackageNames.size)
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@PluginMethod
|
|
154
|
+
fun unblockApps(call: PluginCall) {
|
|
155
|
+
AppBlockerStateStore.clear(context)
|
|
156
|
+
context.stopService(
|
|
157
|
+
Intent(context, AppBlockerForegroundService::class.java).apply {
|
|
158
|
+
action = AppBlockerForegroundService.ACTION_STOP
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
call.resolve(
|
|
163
|
+
JSObject().apply {
|
|
164
|
+
put("success", true)
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@PluginMethod
|
|
170
|
+
fun getStatus(call: PluginCall) {
|
|
171
|
+
val saved = AppBlockerStateStore.load(context)
|
|
172
|
+
val permission = buildPermissionResult()
|
|
173
|
+
val reason = if (saved != null && (!hasUsageAccess() || !canDrawOverlays())) {
|
|
174
|
+
missingPermissionReason()
|
|
175
|
+
} else {
|
|
176
|
+
permission.getString("reason")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
call.resolve(
|
|
180
|
+
JSObject().apply {
|
|
181
|
+
put("available", true)
|
|
182
|
+
put("active", saved != null)
|
|
183
|
+
put("platform", "android")
|
|
184
|
+
put("engine", "usage-stats-overlay")
|
|
185
|
+
put("blockedCount", saved?.packageNames?.size ?: 0)
|
|
186
|
+
put("blockedPackageNames", JSArray(saved?.packageNames ?: emptyList<String>()))
|
|
187
|
+
put("endsAt", saved?.endsAtEpochMs?.let { Instant.ofEpochMilli(it).toString() })
|
|
188
|
+
put("permissionStatus", permission.getString("status"))
|
|
189
|
+
if (!reason.isNullOrBlank()) {
|
|
190
|
+
put("reason", reason)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fun parseDurationMinutes(call: PluginCall): Long? {
|
|
197
|
+
val rawValue = call.data.opt("durationMinutes") ?: return null
|
|
198
|
+
val duration = when (rawValue) {
|
|
199
|
+
is Number -> rawValue.toLong()
|
|
200
|
+
is String -> rawValue.toLongOrNull()
|
|
201
|
+
else -> null
|
|
202
|
+
}
|
|
203
|
+
return duration?.takeIf { it > 0 }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private fun buildPermissionResult(): JSObject {
|
|
207
|
+
val usageAccess = hasUsageAccess()
|
|
208
|
+
val overlayAccess = canDrawOverlays()
|
|
209
|
+
return JSObject().apply {
|
|
210
|
+
put("status", if (usageAccess && overlayAccess) "granted" else "not-determined")
|
|
211
|
+
put("canRequest", !usageAccess || !overlayAccess)
|
|
212
|
+
missingPermissionReason()?.let { put("reason", it) }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun missingPermissionReason(): String? {
|
|
217
|
+
val missingUsageAccess = !hasUsageAccess()
|
|
218
|
+
val missingOverlayAccess = !canDrawOverlays()
|
|
219
|
+
return when {
|
|
220
|
+
missingUsageAccess && missingOverlayAccess ->
|
|
221
|
+
"Android needs Usage Access and Draw Over Other Apps before Eliza can block apps on this phone."
|
|
222
|
+
missingUsageAccess ->
|
|
223
|
+
"Android needs Usage Access before Eliza can detect and block foreground apps."
|
|
224
|
+
missingOverlayAccess ->
|
|
225
|
+
"Android needs Draw Over Other Apps before Eliza can show the blocking shield."
|
|
226
|
+
else -> null
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private fun hasUsageAccess(): Boolean {
|
|
231
|
+
val appOps = context.getSystemService("appops") as? AppOpsManager ?: return false
|
|
232
|
+
val mode = if (Build.VERSION.SDK_INT >= 29) {
|
|
233
|
+
appOps.unsafeCheckOpNoThrow(
|
|
234
|
+
AppOpsManager.OPSTR_GET_USAGE_STATS,
|
|
235
|
+
Process.myUid(),
|
|
236
|
+
context.packageName,
|
|
237
|
+
)
|
|
238
|
+
} else {
|
|
239
|
+
@Suppress("DEPRECATION")
|
|
240
|
+
appOps.checkOpNoThrow(
|
|
241
|
+
AppOpsManager.OPSTR_GET_USAGE_STATS,
|
|
242
|
+
Process.myUid(),
|
|
243
|
+
context.packageName,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
return mode == AppOpsManager.MODE_ALLOWED
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private fun canDrawOverlays(): Boolean {
|
|
250
|
+
return Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(context)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun openSettings(action: String, uri: Uri?) {
|
|
254
|
+
val intent = Intent(action).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
255
|
+
if (uri != null) {
|
|
256
|
+
intent.data = uri
|
|
257
|
+
}
|
|
258
|
+
context.startActivity(intent)
|
|
259
|
+
}
|
|
260
|
+
}
|