android_ui: never check or dismiss the keyboard on a virtual display
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

keyboard_is_up read the phone-wide mInputShown, but Android draws the IME
on display 0 even when its target window is on a virtual display, so it can
never cover a VD tap. With two sessions on one phone, the other session's
keyboard on the real screen made dismiss_keyboard press BACK on this
display, popping the route under test and then closing the app.

Prunes the oldest evidence artifact to stay under the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeVXsWTuwjmZK5uvbw6vqF
This commit is contained in:
Krzysztof Rudnicki 2026-09-27 17:34:25 +02:00
parent 146726b9d4
commit 375555fa37
4 changed files with 67 additions and 33 deletions

View File

@ -0,0 +1,40 @@
{
"intent": "Two Claude sessions sharing one phone must not disturb each other: a keyboard open on the real screen must never make android_ui press BACK on another session's virtual display.",
"scope": [
"python_pkg/android_ui/_text_entry.py (keyboard_is_up only)",
"Non-goal: no change to real-screen (display 0 / no display) behaviour, which still checks and dismisses the IME"
],
"changes": [
"keyboard_is_up returns False when the driver targets a virtual display (display not None and not 0), so dismiss_keyboard sends no ESCAPE/BACK there",
"Test: a VD driver with the phone-wide keyboard shown runs no adb command at all"
],
"verification": [
{
"command": "adb shell dumpsys input_method (live phone, 2026-09-27)",
"result": "pass",
"evidence": "every IME binding reads imeDisplayId=0, including one whose targetWin was com.kuhy.workout_app.sandbox on targetDisplayId=13: the IME is drawn on display 0 for VD windows"
},
{
"command": "python -m python_pkg.android_ui --display self tap ... (before the fix, while another session typed on the real screen)",
"result": "fail",
"evidence": "'the soft keyboard is still covering the screen after ESCAPE and BACK'; the BACK popped the workout route, a second run closed the app on the VD"
},
{
"command": "same taps after the fix",
"result": "pass",
"evidence": "tapped 'Dumbbell Lunge progression settings' / 'Warmup set' / 'Wins needed down' on the VD with the other session still active"
},
{
"command": "pytest python_pkg/android_ui --cov=python_pkg/android_ui --cov-branch",
"result": "pass",
"evidence": "87 passed; _text_entry.py 100% lines and branches"
}
],
"risks": [
"A future Android build that draws the IME on the VD itself would make taps under it land on keys; the phone would have to show imeDisplayId=<vd> for that, which it does not today"
],
"rollback": [
"git revert this commit",
"After rollback, VD taps work again only while no other session has a keyboard open on the real screen"
]
}

View File

@ -1,33 +0,0 @@
{
"intent": "Remove nvidia_config.sh's coverage exemption, which was reinstated when an earlier commit's allowlist deletion did not survive alongside the seam restore.",
"scope": [
"meta/shell-coverage-allowlist.txt (one line removed)",
"Non-goal: any code change; the lib and its tests are already correct"
],
"changes": [
"Deleted the fixes/lib/nvidia_config.sh line from the allowlist, taking it from 85 to 84 entries"
],
"verification": [
{
"command": "shell_coverage_jail.sh with the gate's own flags (--bind /etc --bind /usr/local/bin --bind /var/lib), in a detached worktree",
"result": "pass",
"evidence": "nvidia_config.sh: 76/76 lines = 100.00%."
},
{
"command": "The same measurement in the main working tree",
"result": "fail",
"evidence": "'the subject modified the working tree; a write escaped the jail', listing python_pkg/endurain_import files another agent was editing concurrently. This is a false positive from the jail's tree fingerprint, which is why the real measurement was taken in an isolated worktree."
},
{
"command": "grep for the four seams in nvidia_config.sh",
"result": "pass",
"evidence": "All four present at lines 26, 27, 60 and 85. An earlier `grep -c 'XORG_CONF:-'` returning 1 counted matching LINES, not seams, and briefly looked like a regression."
}
],
"risks": [
"The coverage gate cannot be trusted while another agent is writing to the same working tree; any measurement must be taken in a detached worktree or it may abort on their churn."
],
"rollback": [
"Re-add the nvidia_config.sh line to meta/shell-coverage-allowlist.txt"
]
}

View File

@ -28,6 +28,7 @@ class TextEntryMixin(ABC):
"""The text-entry half of :class:`~python_pkg.android_ui.driver.AndroidUi`.""" """The text-entry half of :class:`~python_pkg.android_ui.driver.AndroidUi`."""
_settle: float _settle: float
_display: int | None
@abstractmethod @abstractmethod
def _run(self, *args: str, timeout: float = 30.0) -> str: def _run(self, *args: str, timeout: float = 30.0) -> str:
@ -137,7 +138,17 @@ class TextEntryMixin(ABC):
Without this, a caller cannot tell "the button is absent" from "the Without this, a caller cannot tell "the button is absent" from "the
button is behind the keyboard", and the accessibility tree reports the button is behind the keyboard", and the accessibility tree reports the
button's laid-out position either way. button's laid-out position either way.
Always False on a virtual display: Android draws the IME on the
default display (``imeDisplayId=0``) even when its target window is
on a VD, so it can never cover a VD tap -- while ``mInputShown`` is
phone-wide. Checking it there meant another session typing on the
real screen made :meth:`dismiss_keyboard` press BACK on *this*
display, popping the route under test (2026-09-27, two sessions on
one phone).
""" """
if self._display not in (None, 0):
return False
out = self._run("shell", "dumpsys", "input_method") out = self._run("shell", "dumpsys", "input_method")
match = re.search(r"mInputShown=(\w+)", out) match = re.search(r"mInputShown=(\w+)", out)
return match is not None and match.group(1) == "true" return match is not None and match.group(1) == "true"

View File

@ -98,6 +98,22 @@ class TestKeyboard:
ui.device.keyboard_shown = False ui.device.keyboard_shown = False
assert not ui.ui.keyboard_is_up() assert not ui.ui.keyboard_is_up()
def test_never_up_on_a_virtual_display(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
# The IME draws on display 0 even for a VD's window, so it cannot
# cover a VD tap -- but mInputShown is phone-wide. Reading it there
# let another session's keyboard make dismiss_keyboard press BACK on
# this display and pop the route under test (2026-09-27).
device = FakeDevice()
device.keyboard_shown = True
vd = drv.AndroidUi(settle_seconds=0.0, display=7)
monkeypatch.setattr(vd, "_run", device.run)
monkeypatch.setattr(f"{MOD}.time.sleep", lambda _s: None)
assert not vd.keyboard_is_up()
vd.dismiss_keyboard()
assert device.calls == [], "no dumpsys, and above all no BACK"
def test_dismiss_is_a_no_op_when_already_closed(self, ui: Harness) -> None: def test_dismiss_is_a_no_op_when_already_closed(self, ui: Harness) -> None:
ui.ui.dismiss_keyboard() ui.ui.dismiss_keyboard()
assert not [ assert not [