fix(android/input): stop mouse back/forward leaking to nav + IME popping on hardware typing
apple / swift (push) Successful in 1m25s
apple / screenshots (push) Successful in 7m4s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m14s
android / android (push) Successful in 11m54s
arch / build-publish (push) Successful in 11m58s
ci / bench (push) Successful in 5m36s
decky / build-publish (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Successful in 11m52s
docker / deploy-docs (push) Successful in 11s
deb / build-publish-host (push) Successful in 10m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
ci / rust (push) Successful in 26m1s
apple / swift (push) Successful in 1m25s
apple / screenshots (push) Successful in 7m4s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m14s
android / android (push) Successful in 11m54s
arch / build-publish (push) Successful in 11m58s
ci / bench (push) Successful in 5m36s
decky / build-publish (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Successful in 11m52s
docker / deploy-docs (push) Successful in 11s
deb / build-publish-host (push) Successful in 10m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
ci / rust (push) Successful in 26m1s
Two Android TV M&K regressions from the c90343c2 overhaul, both user-reported:
1. Mouse back/forward buttons doubled as Android navigation while streaming.
The BUTTON_BACK/FORWARD motion edges already go over the wire as X1/X2, but
Android's input reader ALSO synthesizes KEYCODE_BACK/FORWARD key events from
those buttons unconditionally (stamped SOURCE_MOUSE, no FLAG_FALLBACK) — and
the guard only swallowed the FLAG_FALLBACK variant, so the duplicate reached
the BackHandler and yanked the user out of the stream. Swallow mouse-sourced
BACK/FORWARD too; a remote/keyboard BACK is never mouse-sourced and still
exits.
2. The soft keyboard popped up on physical-keyboard typing. KeyCaptureView (the
always-focused IME/capture anchor) unconditionally claimed to be a text
editor with a live editable InputConnection, so the TV IME counted input as
active and showed its UI the moment hardware keys arrived. Gate the editor
identity on imeShown (the user actually summoning the keyboard via gesture /
remote toggle), with restartInput on both edges so the gate is re-read, and
an onKeyPreIme BACK hook so an IME self-dismissal can't leave the gate
stale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -355,11 +355,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
when (event.keyCode) {
|
when (event.keyCode) {
|
||||||
// A mouse back/forward button whose BUTTON_* press went unconsumed makes the
|
// A mouse's back/forward buttons already go over the wire as X1/X2 via their
|
||||||
// framework synthesize a FALLBACK BACK — the button already went over the wire
|
// BUTTON_* motion edges — but Android ALSO delivers them as key events: the input
|
||||||
// as X1/X2, and it must never yank the user out of the stream.
|
// reader synthesizes KEYCODE_BACK/FORWARD (stamped SOURCE_MOUSE) unconditionally,
|
||||||
KeyEvent.KEYCODE_BACK ->
|
// and a view-level FALLBACK BACK appears when the BUTTON_* press goes unconsumed.
|
||||||
if (event.flags and KeyEvent.FLAG_FALLBACK != 0) return true
|
// Swallow every such duplicate or it doubles as Android navigation and yanks the
|
||||||
|
// user out of the stream. A remote/keyboard BACK is never mouse-sourced, so it
|
||||||
|
// still falls through to the BackHandler and exits.
|
||||||
|
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_FORWARD ->
|
||||||
|
if (event.isFromSource(InputDevice.SOURCE_MOUSE) ||
|
||||||
|
event.flags and KeyEvent.FLAG_FALLBACK != 0
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// Leave these to the system even while streaming.
|
// Leave these to the system even while streaming.
|
||||||
// (BACK above → BackHandler leaves the stream.)
|
// (BACK above → BackHandler leaves the stream.)
|
||||||
KeyEvent.KEYCODE_VOLUME_UP,
|
KeyEvent.KEYCODE_VOLUME_UP,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import android.net.wifi.WifiManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -559,9 +560,15 @@ private class KeyCaptureView(context: Context) : View(context) {
|
|||||||
var imeShown = false
|
var imeShown = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun onCheckIsTextEditor(): Boolean = true
|
override fun onCheckIsTextEditor(): Boolean = imeShown
|
||||||
|
|
||||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||||
|
// Only an editor while the user has SUMMONED the keyboard (gesture / remote toggle).
|
||||||
|
// This view holds focus for the whole stream (it's the capture anchor), and with an
|
||||||
|
// always-live editable connection the IME counts input as active on it — TV IMEs then
|
||||||
|
// pop their UI the moment a PHYSICAL keyboard key arrives. With no connection, hardware
|
||||||
|
// typing stays on the raw dispatchKeyEvent → Keymap → wire path and no keyboard appears.
|
||||||
|
if (!imeShown) return null
|
||||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
||||||
EditorInfo.IME_FLAG_NO_FULLSCREEN or EditorInfo.IME_FLAG_NO_ENTER_ACTION
|
EditorInfo.IME_FLAG_NO_FULLSCREEN or EditorInfo.IME_FLAG_NO_ENTER_ACTION
|
||||||
return if (textHandle != 0L) {
|
return if (textHandle != 0L) {
|
||||||
@@ -580,11 +587,29 @@ private class KeyCaptureView(context: Context) : View(context) {
|
|||||||
imeShown = show
|
imeShown = show
|
||||||
if (show) {
|
if (show) {
|
||||||
requestFocus()
|
requestFocus()
|
||||||
|
// The view may already be focused from a null-connection state — restart so the
|
||||||
|
// framework re-queries onCreateInputConnection with the gate now open.
|
||||||
|
imm.restartInput(this)
|
||||||
imm.showSoftInput(this, 0)
|
imm.showSoftInput(this, 0)
|
||||||
} else {
|
} else {
|
||||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
imm.restartInput(this) // gate closed — drop the editable connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BACK while the summoned keyboard is up: the IME consumes it pre-IME to dismiss itself, so
|
||||||
|
* [setImeVisible] never hears about it — sync the gate here or a stale `imeShown` leaves the
|
||||||
|
* editable connection live and physical typing re-pops the keyboard.
|
||||||
|
*/
|
||||||
|
override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && imeShown && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
imeShown = false
|
||||||
|
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)
|
||||||
|
?.restartInput(this)
|
||||||
|
}
|
||||||
|
return super.onKeyPreIme(keyCode, event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user