-
Notifications
You must be signed in to change notification settings - Fork 0
Fix AndroidManifest.xml parsing, implement debouncing logic in access… #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: poc/KAN-98-overlay
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,18 +21,16 @@ | |
| android:theme="@style/AppTheme" | ||
| android:usesCleartextTraffic="${usesCleartextTraffic}" | ||
| android:supportsRtl="true"> | ||
|
|
||
| <activity | ||
| android:name=".MainActivity" | ||
| android:label="@string/app_name" | ||
| android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" | ||
| android:launchMode="singleTask" | ||
| android:windowSoftInputMode="adjustResize" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
| android:name=".MainActivity" | ||
| android:exported="true" | ||
| android:launchMode="singleTask"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
|
Comment on lines
25
to
+33
|
||
| <service | ||
| android:name=".LimitterService" | ||
| android:enabled="true" | ||
|
|
@@ -41,6 +39,38 @@ | |
| <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" | ||
| android:value="Digital wellbeing app usage limitation and interface blocking" /> | ||
| </service> | ||
|
|
||
| <service | ||
| android:name=".WebsiteBlockerService" | ||
| android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" | ||
| android:exported="false"> | ||
| <intent-filter> | ||
| <action android:name="android.accessibilityservice.AccessibilityService" /> | ||
| </intent-filter> | ||
| <meta-data | ||
| android:name="android.accessibilityservice" | ||
| android:resource="@xml/website_blocker_config" /> | ||
| </service> | ||
|
|
||
| <!-- Category Tracking POC Service (Integration step) --> | ||
| <service | ||
| android:name=".CategoryTrackerAccessibilityService" | ||
| android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" | ||
| android:exported="false"> | ||
| <intent-filter> | ||
| <action android:name="android.accessibilityservice.AccessibilityService" /> | ||
| </intent-filter> | ||
| <meta-data | ||
| android:name="android.accessibilityservice" | ||
| android:resource="@xml/category_tracker_config" /> | ||
| </service> | ||
| <activity | ||
| android:name=".WebsiteBlockerOverlayActivity" | ||
| android:label="Blocked" | ||
| android:theme="@style/AppTheme" | ||
| android:excludeFromRecents="true" | ||
| android:exported="false" /> | ||
|
|
||
| <receiver | ||
| android:name=".AlarmReceiver" | ||
| android:enabled="true" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package com.appguard2 | ||
|
|
||
| import android.accessibilityservice.AccessibilityService | ||
| import android.content.Intent | ||
| import android.view.accessibility.AccessibilityEvent | ||
| import android.view.accessibility.AccessibilityNodeInfo | ||
| import android.util.Log | ||
|
|
||
| class CategoryTrackerAccessibilityService : AccessibilityService() { | ||
| private val TAG = "CategoryTrackingJS" | ||
| private var lastPackage = "" | ||
| private var lastUrl: String? = null | ||
| private var lastDetectionTime = 0L | ||
|
|
||
| override fun onAccessibilityEvent(event: AccessibilityEvent?) { | ||
| if (event == null) return | ||
|
|
||
| val packageName = event.packageName?.toString() ?: return | ||
| var currentUrl: String? = null | ||
|
|
||
| // 1. Detect Browser URL (if it's a browser) | ||
| if (packageName == "com.android.chrome" || packageName == "com.microsoft.emmx") { | ||
| val rootNode = rootInActiveWindow | ||
| if (rootNode != null) { | ||
| currentUrl = findUrl(rootNode, packageName) | ||
| } | ||
| } | ||
|
|
||
| // 2. Filter duplicates and rapid detections | ||
| val now = System.currentTimeMillis() | ||
| if (packageName == lastPackage && currentUrl == lastUrl && now - lastDetectionTime < 1000) { | ||
| return | ||
| } | ||
|
|
||
| lastPackage = packageName | ||
| lastUrl = currentUrl | ||
| lastDetectionTime = now | ||
|
|
||
| // 3. Broadcast Change | ||
| broadcastChange(packageName, currentUrl) | ||
| } | ||
|
|
||
| private fun broadcastChange(packageId: String, url: String?) { | ||
| val intent = Intent("com.appguard2.CATEGORY_TRACKER_CHANGE").apply { | ||
| putExtra("package", packageId) | ||
| if (url != null) putExtra("url", url) | ||
| setPackage(packageName) // Send only to our app | ||
| } | ||
| sendBroadcast(intent) | ||
| } | ||
|
|
||
| private fun findUrl(rootNode: AccessibilityNodeInfo, packageName: String): String? { | ||
| val urlBarId = when (packageName) { | ||
| "com.android.chrome" -> "com.android.chrome:id/url_bar" | ||
| "com.microsoft.emmx" -> "com.microsoft.emmx:id/url_bar" | ||
| else -> null | ||
| } | ||
|
|
||
| if (urlBarId != null) { | ||
| val nodes = rootNode.findAccessibilityNodeInfosByViewId(urlBarId) | ||
| if (nodes != null && nodes.isNotEmpty()) { | ||
| val url = nodes[0].text?.toString() | ||
| nodes.forEach { it.recycle() } | ||
| return url | ||
| } | ||
| } | ||
| return findUrlByRecursion(rootNode) | ||
| } | ||
|
|
||
| private fun findUrlByRecursion(node: AccessibilityNodeInfo): String? { | ||
| if (node.className == "android.widget.EditText" || node.className == "android.view.View") { | ||
| val text = node.text?.toString() | ||
| if (text != null && (text.contains(".") || text.contains("http"))) { | ||
| return text | ||
| } | ||
| } | ||
| for (i in 0 until node.childCount) { | ||
| val child = node.getChild(i) ?: continue | ||
| val result = findUrlByRecursion(child) | ||
| if (result != null) return result | ||
| } | ||
| return null | ||
|
Comment on lines
+77
to
+82
|
||
| } | ||
|
|
||
| override fun onInterrupt() { | ||
| Log.d(TAG, "Service onInterrupt") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||
| package com.appguard2 | ||||||
|
|
||||||
| import android.content.BroadcastReceiver | ||||||
| import android.content.Context | ||||||
| import android.content.Intent | ||||||
| import android.content.IntentFilter | ||||||
| import android.util.Log | ||||||
| import com.facebook.react.bridge.* | ||||||
| import com.facebook.react.modules.core.DeviceEventManagerModule | ||||||
|
|
||||||
| class CategoryTrackerModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { | ||||||
|
|
||||||
| private val TAG = "CategoryTrackerModule" | ||||||
| private var receiver: BroadcastReceiver? = null | ||||||
|
|
||||||
| override fun getName(): String { | ||||||
| return "CategoryTrackerModule" | ||||||
| } | ||||||
|
|
||||||
| @ReactMethod | ||||||
| fun startTracking() { | ||||||
| if (receiver != null) return | ||||||
|
|
||||||
| receiver = object : BroadcastReceiver() { | ||||||
| override fun onReceive(context: Context?, intent: Intent?) { | ||||||
| if (intent?.action != "com.appguard2.CATEGORY_TRACKER_CHANGE") return | ||||||
|
|
||||||
| val packageName = intent.getStringExtra("package") ?: "" | ||||||
| val url = intent.getStringExtra("url") ?: "" | ||||||
|
|
||||||
| val params = Arguments.createMap().apply { | ||||||
| putString("packageName", packageName) | ||||||
| putString("url", url) | ||||||
| } | ||||||
|
|
||||||
| try { | ||||||
| reactContext | ||||||
| .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) | ||||||
| .emit("onForegroundChange", params) | ||||||
| } catch (e: Exception) { | ||||||
| Log.e(TAG, "emit error: ${e.message}") | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| val filter = IntentFilter("com.appguard2.CATEGORY_TRACKER_CHANGE") | ||||||
| if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { | ||||||
| reactContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED) | ||||||
|
||||||
| reactContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED) | |
| reactContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED) |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type parameter is unused, which makes the API misleading for JS callers. Either remove it (if not needed) or use it (e.g., choose overlay text/behavior based on category), or rename to _type to clearly indicate it’s intentionally unused.
| fun triggerBlock(type: String) { | |
| fun triggerBlock(_type: String) { |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.appguard2 | ||
|
|
||
| import android.graphics.Color | ||
| import android.os.Bundle | ||
| import android.view.Gravity | ||
| import android.view.WindowManager | ||
| import android.widget.Button | ||
| import android.widget.LinearLayout | ||
| import android.widget.TextView | ||
| import androidx.appcompat.app.AppCompatActivity | ||
|
|
||
| class WebsiteBlockerOverlayActivity : AppCompatActivity() { | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| // Make it full screen | ||
| window.setFlags( | ||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or | ||
| WindowManager.LayoutParams.FLAG_FULLSCREEN, | ||
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or | ||
| WindowManager.LayoutParams.FLAG_FULLSCREEN | ||
| ) | ||
|
|
||
| val root = LinearLayout(this).apply { | ||
| orientation = LinearLayout.VERTICAL | ||
| setBackgroundColor(Color.parseColor("#0f0c29")) | ||
| gravity = Gravity.CENTER | ||
| setPadding(80, 80, 80, 80) | ||
| } | ||
|
|
||
| root.addView(TextView(this).apply { | ||
| text = "🚫" | ||
| textSize = 100f | ||
| gravity = Gravity.CENTER | ||
| setPadding(0, 0, 0, 40) | ||
| }) | ||
|
|
||
| root.addView(TextView(this).apply { | ||
| text = "Website Blocked" | ||
| setTextColor(Color.parseColor("#ef4444")) | ||
| textSize = 36f | ||
| setTypeface(null, android.graphics.Typeface.BOLD) | ||
| gravity = Gravity.CENTER | ||
| setPadding(0, 0, 0, 20) | ||
| }) | ||
|
|
||
| root.addView(TextView(this).apply { | ||
| text = "You have reached your limit" | ||
| setTextColor(Color.parseColor("#c4b5fd")) | ||
| textSize = 20f | ||
| gravity = Gravity.CENTER | ||
| setPadding(0, 0, 0, 80) | ||
| }) | ||
|
|
||
| root.addView(Button(this).apply { | ||
| text = "🏠 Go Home" | ||
| setBackgroundColor(Color.parseColor("#7c3aed")) | ||
| setTextColor(Color.WHITE) | ||
| textSize = 18f | ||
| setPadding(80, 40, 80, 40) | ||
| setOnClickListener { | ||
| val homeIntent = android.content.Intent(android.content.Intent.ACTION_MAIN).apply { | ||
| addCategory(android.content.Intent.CATEGORY_HOME) | ||
| flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK | ||
| } | ||
| startActivity(homeIntent) | ||
| finish() | ||
| } | ||
| }) | ||
|
|
||
| setContentView(root) | ||
| } | ||
|
|
||
| override fun onBackPressed() { | ||
| // Disable back button | ||
| // Don't call super.onBackPressed() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import is placed after executable code. While still top-level, many TS/ESLint configurations enforce imports at the top for consistency and to avoid tooling edge-cases. Move
import { startCategoryService ... }up with the other imports.