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 }`