| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent dfbdfe0 commit 8cca638
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -151,7 +151,13 @@ class UIAgentAccessibilityService : AccessibilityService() { | |||
| 151 | 151 | updateOverlay("Capturing screen...", currentStepCount) | |
| 152 | 152 | Log.d("UIAgentAccessibilityService", "Capturing screen for step $currentStepCount...") | |
| 153 | 153 | ||
| 154 | - val targetWindowId = rootInActiveWindow?.windowId ?: -1 | ||
| 154 | + // Find the target application window (skip our own overlays) | ||
| 155 | + val windows = windows | ||
| 156 | + val targetWindow = windows.find { | ||
| 157 | + it.type == AccessibilityWindowInfo.TYPE_APPLICATION && it.isActive | ||
| 158 | + } ?: windows.find { it.type == AccessibilityWindowInfo.TYPE_APPLICATION } | ||
| 159 | + | ||
| 160 | + val targetWindowId = targetWindow?.id ?: rootInActiveWindow?.windowId ?: -1 | ||
| 155 | 161 | ||
| 156 | 162 | captureScreenshot(mainExecutor, targetWindowId) { bitmap -> | |
| 157 | 163 | if (bitmap == null) { | |
@@ -373,11 +379,52 @@ class UIAgentAccessibilityService : AccessibilityService() { | |||
| 373 | 379 | } | |
| 374 | 380 | ||
| 375 | 381 | fun performClickAt(x: Float, y: Float) { | |
| 376 | - val clickPath = Path() | ||
| 377 | - clickPath.moveTo(x, y) | ||
| 378 | - val gestureBuilder = GestureDescription.Builder() | ||
| 379 | - gestureBuilder.addStroke(GestureDescription.StrokeDescription(clickPath, 0, 100)) | ||
| 380 | - dispatchGesture(gestureBuilder.build(), null, null) | ||
| 382 | + serviceScope.launch { | ||
| 383 | + Log.d("UIAgentAccessibilityService", "Performing click at ($x, $y) - Ghosting overlays") | ||
| 384 | + setOverlaysTouchable(false) | ||
| 385 | + delay(100) // Wait for WindowManager to update flags | ||
| 386 | + | ||
| 387 | + val clickPath = Path() | ||
| 388 | + clickPath.moveTo(x, y) | ||
| 389 | + val gestureBuilder = GestureDescription.Builder() | ||
| 390 | + gestureBuilder.addStroke(GestureDescription.StrokeDescription(clickPath, 0, 100)) | ||
| 391 | + | ||
| 392 | + dispatchGesture(gestureBuilder.build(), object : GestureResultCallback() { | ||
| 393 | + override fun onCompleted(gestureDescription: GestureDescription?) { | ||
| 394 | + super.onCompleted(gestureDescription) | ||
| 395 | + Log.d("UIAgentAccessibilityService", "Gesture completed") | ||
| 396 | + setOverlaysTouchable(true) | ||
| 397 | + } | ||
| 398 | + override fun onCancelled(gestureDescription: GestureDescription?) { | ||
| 399 | + super.onCancelled(gestureDescription) | ||
| 400 | + Log.w("UIAgentAccessibilityService", "Gesture cancelled") | ||
| 401 | + setOverlaysTouchable(true) | ||
| 402 | + } | ||
| 403 | + }, null) | ||
| 404 | + } | ||
| 405 | + } | ||
| 406 | + | ||
| 407 | + private fun setOverlaysTouchable(touchable: Boolean) { | ||
| 408 | + Handler(Looper.getMainLooper()).post { | ||
| 409 | + overlayView?.let { view -> | ||
| 410 | + val params = view.layoutParams as WindowManager.LayoutParams | ||
| 411 | + if (touchable) { | ||
| 412 | + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() | ||
| 413 | + } else { | ||
| 414 | + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | ||
| 415 | + } | ||
| 416 | + windowManager.updateViewLayout(view, params) | ||
| 417 | + } | ||
| 418 | + chatOverlayView?.let { view -> | ||
| 419 | + val params = view.layoutParams as WindowManager.LayoutParams | ||
| 420 | + if (touchable) { | ||
| 421 | + params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() | ||
| 422 | + } else { | ||
| 423 | + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | ||
| 424 | + } | ||
| 425 | + windowManager.updateViewLayout(view, params) | ||
| 426 | + } | ||
| 427 | + } | ||
| 381 | 428 | } | |
| 382 | 429 | ||
| 383 | 430 | private fun stopWithNotification(message: String) { | |
@@ -536,7 +583,7 @@ class UIAgentAccessibilityService : AccessibilityService() { | |||
| 536 | 583 | val root = LinearLayout(this).apply { | |
| 537 | 584 | orientation = LinearLayout.VERTICAL | |
| 538 | 585 | background = GradientDrawable().apply { | |
| 539 | - setColor(Color.parseColor("#EE111111")) | ||
| 586 | + setColor(Color.parseColor("#99111111")) // Further reduced opacity to 60% just in case | ||
| 540 | 587 | cornerRadius = 24f | |
| 541 | 588 | } | |
| 542 | 589 | setPadding(20, 20, 20, 20) | |
@@ -673,17 +720,39 @@ class UIAgentAccessibilityService : AccessibilityService() { | |||
| 673 | 720 | } | |
| 674 | 721 | ||
| 675 | 722 | fun performSwipe(startX: Float, startY: Float, endX: Float, endY: Float, duration: Long = 500L) { | |
| 676 | - val swipePath = Path() | ||
| 677 | - swipePath.moveTo(startX, startY) | ||
| 678 | - swipePath.lineTo(endX, endY) | ||
| 679 | - val gestureBuilder = GestureDescription.Builder() | ||
| 680 | - gestureBuilder.addStroke(GestureDescription.StrokeDescription(swipePath, 0, duration)) | ||
| 681 | - dispatchGesture(gestureBuilder.build(), null, null) | ||
| 723 | + serviceScope.launch { | ||
| 724 | + Log.d("UIAgentAccessibilityService", "Performing swipe from ($startX, $startY) to ($endX, $endY)") | ||
| 725 | + setOverlaysTouchable(false) | ||
| 726 | + delay(100) | ||
| 727 | + | ||
| 728 | + val swipePath = Path() | ||
| 729 | + swipePath.moveTo(startX, startY) | ||
| 730 | + swipePath.lineTo(endX, endY) | ||
| 731 | + val gestureBuilder = GestureDescription.Builder() | ||
| 732 | + gestureBuilder.addStroke(GestureDescription.StrokeDescription(swipePath, 0, duration)) | ||
| 733 | + | ||
| 734 | + dispatchGesture(gestureBuilder.build(), object : GestureResultCallback() { | ||
| 735 | + override fun onCompleted(gestureDescription: GestureDescription?) { | ||
| 736 | + super.onCompleted(gestureDescription) | ||
| 737 | + setOverlaysTouchable(true) | ||
| 738 | + } | ||
| 739 | + override fun onCancelled(gestureDescription: GestureDescription?) { | ||
| 740 | + super.onCancelled(gestureDescription) | ||
| 741 | + setOverlaysTouchable(true) | ||
| 742 | + } | ||
| 743 | + }, null) | ||
| 744 | + } | ||
| 682 | 745 | } | |
| 683 | 746 | ||
| 684 | 747 | fun getClickableElementsJson(): String { | |
| 685 | - val rootNode = rootInActiveWindow ?: return "[]" | ||
| 686 | - // Ensure we are only traversing the target app's window | ||
| 748 | + // Look for the application window specifically | ||
| 749 | + val windows = windows | ||
| 750 | + val targetWindow = windows.find { | ||
| 751 | + it.type == AccessibilityWindowInfo.TYPE_APPLICATION && it.isActive | ||
| 752 | + } ?: windows.find { it.type == AccessibilityWindowInfo.TYPE_APPLICATION } | ||
| 753 | + | ||
| 754 | + val rootNode = targetWindow?.root ?: rootInActiveWindow ?: return "[]" | ||
| 755 | + | ||
| 687 | 756 | if (rootNode.packageName == packageName) { | |
| 688 | 757 | Log.w("UIAgentAccessibilityService", "Root node belongs to our service, skipping UI tree") | |
| 689 | 758 | return "[]" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,49 @@ | |||
| 1 | + ## Chat Overlay & Interactivity Documentation | ||
| 2 | + | ||
| 3 | + This document explains the implementation of the conversational chat overlay and how we solved the technical challenge of overlay interference with AI actions. | ||
| 4 | + | ||
| 5 | + ### 1. The Problem: Overlay Interference | ||
| 6 | + | ||
| 7 | + When a persistent chat window is added as a floating overlay (`TYPE_ACCESSIBILITY_OVERLAY`), it introduces two major issues for an AI-driven automation agent: | ||
| 8 | + | ||
| 9 | + #### A. Visual Confusion (Screenshot Pollution) | ||
| 10 | + If the agent takes a standard screenshot of the display, the chat window will appear in the image. The AI might then try to interact with the chat window itself (e.g., clicking the "Send" button) instead of the target application it is supposed to automate. | ||
| 11 | + | ||
| 12 | + #### B. Touch Interception (Click Blockage) | ||
| 13 | + Android overlays are physical layers. If the AI decides to click a button in the target app that is currently hidden behind the chat window, the click will be intercepted by the chat overlay. The target app will never receive the event. | ||
| 14 | + | ||
| 15 | + --- | ||
| 16 | + | ||
| 17 | + ### 2. The Solution: "Ghosting" and Focused Vision | ||
| 18 | + | ||
| 19 | + We solved these issues using a combination of Android 14 window APIs and dynamic flag management. | ||
| 20 | + | ||
| 21 | + #### A. Window-Specific Capture (AI Vision) | ||
| 22 | + Instead of capturing the full display, the service now performs the following steps for every "thought" cycle: | ||
| 23 | + 1. **Iterate Windows**: It uses `AccessibilityService.getWindows()` to find the window with `TYPE_APPLICATION`. | ||
| 24 | + 2. **Target ID**: It retrieves the specific `windowId` of that app. | ||
| 25 | + 3. **Filtered Screenshot**: It uses `takeScreenshotOfWindow(windowId, ...)` (introduced in API 34). | ||
| 26 | + 4. **Result**: The AI receives a clean screenshot of *only* the app it is automating. The chat window is completely invisible to the AI's "vision." | ||
| 27 | + | ||
| 28 | + #### B. The "Ghosting" System (Interactivity) | ||
| 29 | + To ensure AI clicks hit the target app even when covered by the chat, we implemented a **Ghosting Mode** during gesture injection: | ||
| 30 | + | ||
| 31 | + 1. **Flag Update**: Immediately before a click or swipe, the service applies the `FLAG_NOT_TOUCHABLE` flag to both the status bar and the chat window. | ||
| 32 | + 2. **Synchronization Delay**: A small 100ms delay is introduced to ensure the `WindowManager` has applied the new flags. | ||
| 33 | + 3. **Gesture Injection**: The `dispatchGesture()` API is called to perform the action. | ||
| 34 | + 4. **Callback Restoration**: We use a `GestureResultCallback`. Only once Android confirms the gesture is finished (or cancelled) do we remove the `FLAG_NOT_TOUCHABLE` flag. | ||
| 35 | + 5. **Opacity Tuning**: The chat window background is set to **60% opacity** (`#99111111`) to ensure compliance with Android 12+ "Untrusted Touch" security policies, preventing the system from ever blocking an AI-initiated gesture. | ||
| 36 | + | ||
| 37 | + --- | ||
| 38 | + | ||
| 39 | + ### 3. Technical Summary | ||
| 40 | + - **Primary API**: `takeScreenshotOfWindow` (API 34+) | ||
| 41 | + - **Fallback**: Standard `takeScreenshot` if window ID is unavailable. | ||
| 42 | + - **Gesture Reliability**: Verified via `GestureResultCallback` and temporary flag toggling. | ||
| 43 | + - **Visibility**: Overlays remain visible to the user at all times but are "ghosted" for the milliseconds during which an automated action occurs. | ||
| 44 | + | ||
| 45 | + --- | ||
| 46 | + | ||
| 1 | 47 | # Implementation Plan - Chat Overlay & Conversation History | |
| 2 | 48 | ||
| 3 | 49 | This plan transforms the current "one-off task" execution into a persistent conversation between the user and the AI agent, accessible via a floating chat window. | |
| Back | FazBrowse Home | New Git URL |
0 commit comments