Accessibility Service Breaking Compose Navigation inside TextFields While Xml is fine


Accessibility Service Causing DPAD Navigation Broken:
When flagRequestFilterKeyEvents is set, every hardware key is delivered to the service first, and re-injected into the input pipeline after the service returns. For XML, ViewRootImpl performs focus search on the re-injected
event and moves focus to the next focusable view. For Compose, BasicTextField traps DPAD UP/DOWN for cursor movement and never releases them to the framework focus-search path — so re-injected events get consumed by the field with no visible effect.
Only issue im having is when a text field is focused (cursor visible) i cant move up down with Dpad and while service is off then i can move even im not having up down events being consumed
Any Clues?
i tried to manullay find next focusable component in tree and it navigates fine but if a text field is multi-line then it directly jumps to next field (also i dont want to manually make if else)

Here's Service XML:

<?xml version="1.0" encoding="utf-8"?>
    <accessibility-service 

    `xmlns:android="http://schemas.android.com/apk/res/android"
            android:accessibilityEventTypes="typeViewFocused|typeViewTextSelectionChanged|typeWindowStateChanged|typeWindowsChanged"
            android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds|flagRequestFilterKeyEvents"
            android:canRequestFilterKeyEvents="true"
            android:canRetrieveWindowContent="true"
            android:description="@string/accessibility_service_description"
            android:notificationTimeout="100"
            android:summary="@string/accessibility_service_summary" />`

The Service Code is as :

`override fun onKeyEvent(event: KeyEvent): Boolean {
        val code = event.keyCode
        Log.v(
            TAG,
            "onKeyEvent code=$code action=${event.action} repeat=${event.repeatCount}" + " flags=0x${
                Integer.toHexString(event.flags)
            }" + " canceled=${(event.flags and KeyEvent.FLAG_CANCELED) != 0}" + " longPress=${(event.flags and KeyEvent.FLAG_LONG_PRESS) != 0}"
        )
    
        // Try to swallow BACK while we're active so the underlying app doesn't
        // also navigate. NOTE: some devices (and Android 13+ predictive-back)
        // bypass this callback for BACK; if so, the windows-changed fallback
        // in checkRecordingWindowGone() still cancels the operation.
        if (code == KeyEvent.KEYCODE_BACK) {
            val active = recording || transcribeJob?.isActive == true
            if (!active) return false
            Log.i(TAG, "BACK received in onKeyEvent action=${event.action} — consuming")
            if (event.action == KeyEvent.ACTION_UP && (event.flags and KeyEvent.FLAG_CANCELED) == 0) {
                cancelActiveOperation()
            }
            return true
        }
    
        if (code != KeyEvent.KEYCODE_DPAD_CENTER && code != KeyEvent.KEYCODE_ENTER) {
            return false
        }
    
        if (event.action == KeyEvent.ACTION_DOWN) {
            main.removeCallbacks(debouncedStop)
    
            if (recording || longPressTriggered) {
                dpadDown = true
                return true
            }
    
            if (!dpadDown) {
                val node = findFocusedEditable()
                if (node == null) {
                    Log.d(TAG, "DOWN: no focused editable, pass-through")
                    return false
                }
                Log.d(
                    TAG,
                    "DOWN on editable, arming long-press timer (${ViewConfiguration.getLongPressTimeout()}ms) — consuming"
                )
                dpadDown = true
                longPressTriggered = false
                main.postDelayed(
                    longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong()
                )
                return true
            }
            // Let short press proceed to target view; we only act if long press actually fires.
            return false
        }
    
        if (event.action == KeyEvent.ACTION_UP) {
            val canceled = (event.flags and KeyEvent.FLAG_CANCELED) != 0
            if (canceled) {
                if (longPressTriggered) {
                    Log.d(TAG, "UP canceled during long-press flow — consuming")
                    return true
                }
                Log.d(TAG, "UP canceled without long-press — pass-through")
                dpadDown = false
                main.removeCallbacks(longPressRunnable)
                return false
            }
            if (!dpadDown) return false
            dpadDown = false
            main.removeCallbacks(longPressRunnable)
            if (longPressTriggered) {
                Log.d(
                    TAG, "UP after long-press: scheduling debounced stop (${STOP_DEBOUNCE_MS}ms)"
                )
                main.postDelayed(debouncedStop, STOP_DEBOUNCE_MS)
                return true
            }
            Log.d(TAG, "UP short-press on editable: pass-through")
            return false
        }
        return false }`
1
May 14 at 7:03 PM
User AvatarMuhammad Awais Shan
#android#android-jetpack-compose#accessibilityservice#jetpack-compose-navigation#d-pad

No answer found for this question yet.