Fix AndroidManifest.xml parsing, implement debouncing logic in access…#1
Fix AndroidManifest.xml parsing, implement debouncing logic in access…#1moawizsipra80 wants to merge 1 commit into
Conversation
…ibility services, and update category map
There was a problem hiding this comment.
Pull request overview
This PR adds Android accessibility-based URL/app foreground detection to drive category-based time limits and overlays, while updating the Android manifest/resources to wire up the new services.
Changes:
- Added JS category mapping + timer logic and a JS service that subscribes to native foreground-change events.
- Added Android AccessibilityServices (website blocker + category tracker), an overlay activity, and a React Native native module bridge for events/triggering blocks.
- Updated AndroidManifest/resources to register services and provide accessibility service configuration/strings.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/categoryTimer.js | Introduces in-memory per-category time tracking and block checks. |
| src/services/categoryService.js | Subscribes to native foreground-change events and runs per-category timers. |
| src/services/categoryMap.js | Defines the package/domain → category mapping and the global category limit. |
| android/app/src/main/res/xml/website_blocker_config.xml | Accessibility service config for browser URL capture + blocking. |
| android/app/src/main/res/xml/category_tracker_config.xml | Accessibility service config for category tracking broadcast. |
| android/app/src/main/res/values/strings.xml | Adds accessibility service description string resource. |
| android/app/src/main/java/com/appguard2/WebsiteBlockerService.kt | Adds debounced URL detection and overlay triggering for Google. |
| android/app/src/main/java/com/appguard2/WebsiteBlockerOverlayActivity.kt | Adds a full-screen “blocked” overlay activity. |
| android/app/src/main/java/com/appguard2/LimitterPackage.kt | Registers the new CategoryTrackerModule with React Native. |
| android/app/src/main/java/com/appguard2/CategoryTrackerModule.kt | Bridges accessibility broadcasts to JS events and exposes triggerBlock(). |
| android/app/src/main/java/com/appguard2/CategoryTrackerAccessibilityService.kt | Broadcasts foreground package/URL changes for JS consumption. |
| android/app/src/main/AndroidManifest.xml | Registers accessibility services + overlay activity; adjusts MainActivity entry. |
| Limitter-app | Updates the subproject commit pointer. |
| App.tsx | Starts the new category tracking service on app startup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if (categoryTimeS[categoryName] !== undefined) { | ||
| categoryTimeS[categoryName] += 1; | ||
| return categoryTimeS[categoryName]; | ||
| } | ||
| return 0; |
There was a problem hiding this comment.
incrementCategoryTime() never starts counting for categories that aren’t pre-seeded in categoryTimeS (it returns 0 forever). Initialize missing categories on first use (e.g., set to 0 then increment), or pre-populate from the set of categories you support so time is tracked for all mapped categories.
| if (categoryTimeS[categoryName] !== undefined) { | |
| categoryTimeS[categoryName] += 1; | |
| return categoryTimeS[categoryName]; | |
| } | |
| return 0; | |
| if (categoryTimeS[categoryName] === undefined) { | |
| categoryTimeS[categoryName] = 0; | |
| } | |
| categoryTimeS[categoryName] += 1; | |
| return categoryTimeS[categoryName]; |
| @@ -0,0 +1,21 @@ | |||
| import { categoryMap, CATEGORY_LIMIT } from './categoryMap'; | |||
There was a problem hiding this comment.
categoryMap is imported but not used in this file. Either remove the unused import or use it to initialize/derive supported categories (which would also help avoid missed categories in the timer state).
| import { categoryMap, CATEGORY_LIMIT } from './categoryMap'; | |
| import { CATEGORY_LIMIT } from './categoryMap'; |
| const { CategoryTrackerModule } = NativeModules; | ||
| const eventEmitter = new NativeEventEmitter(CategoryTrackerModule); |
There was a problem hiding this comment.
NativeEventEmitter is constructed unconditionally at module load time, but startCategoryService() explicitly handles CategoryTrackerModule being missing. If CategoryTrackerModule is undefined/null, creating the emitter can throw or warn before your guard runs. Create the emitter lazily inside startCategoryService() after confirming the native module exists.
| export const startCategoryService = () => { | ||
| if (!CategoryTrackerModule) { | ||
| console.error("CategoryTrackerModule not found. Ensure Native code is integrated."); | ||
| return; | ||
| } |
There was a problem hiding this comment.
NativeEventEmitter is constructed unconditionally at module load time, but startCategoryService() explicitly handles CategoryTrackerModule being missing. If CategoryTrackerModule is undefined/null, creating the emitter can throw or warn before your guard runs. Create the emitter lazily inside startCategoryService() after confirming the native module exists.
| eventEmitter.addListener('onForegroundChange', (event) => { | ||
| const { packageName, url } = event; | ||
|
|
||
| let detectedItem = null; | ||
| if (url && (packageName === 'com.android.chrome' || packageName === 'com.microsoft.emmx')) { | ||
| for (const key in categoryMap) { | ||
| if (url.includes(key)) { | ||
| detectedItem = key; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!detectedItem && categoryMap[packageName]) { | ||
| detectedItem = packageName; | ||
| } | ||
|
|
||
| if (detectedItem !== currentItem) { | ||
| currentItem = detectedItem; | ||
| handleTimerUpdate(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
The listener subscription returned by addListener isn’t stored/removed, so re-initialization paths (e.g., app-level re-mounts, dev reloads, or calling startCategoryService() again) can accumulate listeners/timers. Consider returning a cleanup function (or exposing stopCategoryService) that removes the listener and clears any active interval.
| } | ||
|
|
||
| @ReactMethod | ||
| fun triggerBlock(type: String) { |
There was a problem hiding this comment.
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) { |
| } | ||
| for (i in 0 until node.childCount) { | ||
| val child = node.getChild(i) ?: continue | ||
| val result = findUrlByRecursion(child) |
There was a problem hiding this comment.
AccessibilityNodeInfo objects obtained via getChild(i) should be recycled when you’re done with them to avoid resource leaks. Wrap child processing in try/finally (recycling the child in finally), and similarly ensure any root node retrieved from rootInActiveWindow is recycled when no longer needed.
| val result = findUrlByRecursion(child) | |
| val result = try { | |
| findUrlByRecursion(child) | |
| } finally { | |
| child.recycle() | |
| } |
| for (i in 0 until node.childCount) { | ||
| val child = node.getChild(i) ?: continue | ||
| val result = findUrlByRecursion(child) | ||
| if (result != null) return result | ||
| } | ||
| return null |
There was a problem hiding this comment.
Same AccessibilityNodeInfo recycling issue as in WebsiteBlockerService: child nodes from getChild(i) should be recycled, and any root node obtained from rootInActiveWindow should also be recycled. Not doing so can increase memory pressure in frequently-called accessibility callbacks.
| <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> |
There was a problem hiding this comment.
The MainActivity declaration dropped several React Native template attributes (android:configChanges, android:windowSoftInputMode, and android:label). Removing these commonly causes unwanted Activity recreation on rotation/config changes and can regress keyboard/resizing behavior. Consider restoring the previous attributes unless there’s a specific reason to remove them.
|
|
||
| const { LimitterModule, TimerEventModule } = NativeModules; | ||
|
|
||
| import { startCategoryService } from './src/services/categoryService'; | ||
|
|
There was a problem hiding this comment.
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.
| const { LimitterModule, TimerEventModule } = NativeModules; | |
| import { startCategoryService } from './src/services/categoryService'; | |
| import { startCategoryService } from './src/services/categoryService'; | |
| const { LimitterModule, TimerEventModule } = NativeModules; |
…ibility services, and update category map