11import type { DeviceInfo } from '@agent-device/kernel/device' ;
2- import type { Rect , SnapshotKeyboardBandFact } from '@agent-device/kernel/snapshot' ;
2+ import type { SnapshotKeyboardBandFact } from '@agent-device/kernel/snapshot' ;
33import { runHarmonyShell } from './hdc.ts' ;
44
55/**
66 * The system window that hosts the on-screen keyboard. HarmonyOS keeps this SceneBoard panel
7- * alive across captures, so presence alone says nothing: the z-order column decides visibility
8- * (negative while hidden), and the rect is a band only when the table proves it smaller than
9- * the display — current builds report the full display rect in both states.
7+ * alive across captures, so presence alone says nothing: the z-order column is the only
8+ * visibility signal (negative while hidden). The rect column is never read: on the measured
9+ * build the panel row reports the whole display in both states, so the table proves no band,
10+ * and a frame the table cannot prove would let the tap guard mark every on-screen point
11+ * keyboard-covered.
1012 */
1113const KEYBOARD_PANEL_WINDOW_PREFIX = 'SCBKeyboardPanel' ;
1214
1315const KEYBOARD_BAND_PROBE_TIMEOUT_MS = 5_000 ;
1416
1517/**
16- * The keyboard band this capture's producer measured, read from the window manager's public
17- * dump (`hidumper -s WindowManagerService -a -a`). The IME never appears in the ArkUI layout
18- * dump of the app behind it, so the tap guard's docked-tree derivation has no anchor to read;
19- * this probe is the only public HDC channel that answers where the keyboard is. Never throws:
20- * a failed probe publishes an `unmeasurable` fact, which leaves the guard's tree derivation
21- * and its fail-open verdict exactly as they were before this probe existed.
18+ * The keyboard visibility this capture's producer measured, read from the window manager's
19+ * public dump (`hidumper -s WindowManagerService -a -a`). The IME never appears in the ArkUI
20+ * layout dump of the app behind it, so the tap guard's docked-tree derivation has no anchor to
21+ * read; this probe is the only public HDC channel that answers whether the keyboard shows.
22+ * Never throws: a failed probe publishes an `unmeasurable` fact, which leaves the guard's tree
23+ * derivation and its fail-open verdict exactly as they were before this probe existed.
2224 */
2325export async function readHarmonyKeyboardBand (
2426 device : DeviceInfo ,
@@ -43,111 +45,57 @@ function keyboardWindowProbeFailed(): SnapshotKeyboardBandFact {
4345}
4446
4547/**
46- * A rect at or above this share of the display's area is the window manager's full-surface
47- * container, not the key plane. On JUY-AL50 7.0.0.111 the panel row reports the whole display
48- * whether the keyboard shows or hides, and the key plane lives inside the window's own surface
49- * where no public dump reads it — `softKeyboard1` and `SCBKeyboardPanel` both answer
50- * `[0 0 1080 2406]`. A frame this table cannot prove to be a band must not become one: the
51- * tap guard measures a published frame directly, so a display-spanning frame would mark every
52- * on-screen point keyboard-covered.
53- */
54- const DISPLAY_SPANNING_RECT_SHARE = 0.9 ;
55-
56- /**
57- * Reads the keyboard band out of a WindowManagerService window table. The table lists the
58- * panel on every device that shows a software keyboard; a device whose table omits it, or
59- * whose panel row carries no readable rect, answers `unmeasurable` rather than a guess. The
60- * z-order column is the visibility signal (negative while hidden); the rect is trusted as a
61- * band only when the table proves it smaller than the display, which current builds do not —
62- * a display-spanning rect publishes `visibility-only` and leaves the band to the guard's
63- * tree derivation.
48+ * Reads keyboard visibility out of a WindowManagerService window table. The table lists the
49+ * panel on every device that shows a software keyboard; a device whose table omits it answers
50+ * `unmeasurable` rather than hidden, because a missing row is a dump-shape fact, not a
51+ * visibility measurement. A listed panel whose z-order column is unreadable also answers
52+ * `unmeasurable`: with the rect column never read, an unreadable z-order means the probe
53+ * measured nothing, and panel rows that disagree across displays mean it cannot say which
54+ * display it measured. The fact is always `visibility-only` — every measured build publishes
55+ * display-sized panel rows, so no public table carries a provable band and none is guessed.
6456 */
6557export function harmonyKeyboardBandFromWindowDump ( raw : string ) : SnapshotKeyboardBandFact {
6658 const rows = parseKeyboardPanelRows ( raw ) ;
6759 if ( rows . length === 0 ) {
6860 return { kind : 'unmeasurable' , reason : 'keyboard-window-not-listed' } ;
6961 }
70- const measured = rows . filter ( ( row ) => row . rect !== undefined ) ;
71- if ( measured . length === 0 ) {
72- return { kind : 'unmeasurable' , reason : 'keyboard-window-rect-unparseable ' } ;
62+ const zOrders = rows . map ( ( row ) => row . zOrder ) ;
63+ if ( ! zOrders . every ( readableZOrder ) ) {
64+ return { kind : 'unmeasurable' , reason : 'keyboard-window-zorder-unreadable ' } ;
7365 }
74- const visible = measured . find ( isVisibleKeyboardPanelRow ) ;
75- if ( ! visible ?. rect ) return { kind : 'visibility-only' , visible : false } ;
76- const display = displayRectFromWindowDump ( raw ) ;
77- if ( ! display || rectAreaShare ( visible . rect , display ) >= DISPLAY_SPANNING_RECT_SHARE ) {
78- return { kind : 'visibility-only ' , visible : true } ;
66+ // Multi-display tables list one panel per display and the probe has no display scope, so
67+ // disagreeing rows mean the probe cannot say which display's keyboard it measured.
68+ const shown = zOrders . some ( ( z ) => z >= 0 ) ;
69+ if ( shown && zOrders . some ( ( z ) => z < 0 ) ) {
70+ return { kind : 'unmeasurable ' , reason : 'keyboard-window-zorder-conflict' } ;
7971 }
80- return { kind : 'visible' , frame : visible . rect } ;
81- }
82-
83- /** The largest positive rect in the table, which the display-sized system windows hold. */
84- function displayRectFromWindowDump ( raw : string ) : Rect | undefined {
85- let display : Rect | undefined ;
86- let largestArea = 0 ;
87- for ( const line of raw . split ( '\n' ) ) {
88- const rect = rectFromWindowRow ( line ) ;
89- if ( ! rect || rect . width <= 0 || rect . height <= 0 ) continue ;
90- const area = rect . width * rect . height ;
91- if ( area > largestArea ) {
92- display = rect ;
93- largestArea = area ;
94- }
95- }
96- return display ;
97- }
98-
99- function rectAreaShare ( rect : Rect , display : Rect ) : number {
100- if ( display . width <= 0 || display . height <= 0 ) return 0 ;
101- return ( rect . width * rect . height ) / ( display . width * display . height ) ;
72+ return { kind : 'visibility-only' , visible : shown } ;
10273}
10374
10475type KeyboardPanelRow = {
105- rect : Rect | undefined ;
10676 /** The window's z-order, when the table reports a readable one. */
10777 zOrder : number | undefined ;
10878} ;
10979
110- /**
111- * A positive rect usually means the keyboard is showing, but the hidden panel must not
112- * publish a phantom band from a stale rect. The dump reports hidden windows with a negative
113- * z-order; when that column is readable, both signals must agree on "shown".
114- */
115- function isVisibleKeyboardPanelRow ( row : KeyboardPanelRow ) : boolean {
116- const rect = row . rect ;
117- if ( ! rect || rect . width <= 0 || rect . height <= 0 ) return false ;
118- return row . zOrder === undefined || row . zOrder >= 0 ;
119- }
120-
12180function parseKeyboardPanelRows ( raw : string ) : KeyboardPanelRow [ ] {
12281 const rows : KeyboardPanelRow [ ] = [ ] ;
12382 for ( const line of raw . split ( '\n' ) ) {
12483 const name = line . trim ( ) . split ( / \s + / ) [ 0 ] ?? '' ;
12584 if ( ! name . startsWith ( KEYBOARD_PANEL_WINDOW_PREFIX ) ) continue ;
126- rows . push ( { rect : rectFromWindowRow ( line ) , zOrder : zOrderFromWindowRow ( line ) } ) ;
85+ rows . push ( { zOrder : zOrderFromWindowRow ( line ) } ) ;
12786 }
12887 return rows ;
12988}
13089
131- /** The first bracketed group of a window row: `[ x y w h ]`, ahead of offset and scale groups. */
132- const WINDOW_ROW_RECT = / \[ \s * ( - ? \d + ) \s + ( - ? \d + ) \s + ( - ? \d + ) \s + ( - ? \d + ) \s * \] / ;
133-
134- function rectFromWindowRow ( line : string ) : Rect | undefined {
135- const match = WINDOW_ROW_RECT . exec ( line ) ;
136- if ( ! match ) return undefined ;
137- const [ , x , y , width , height ] = match ;
138- return {
139- x : Number ( x ) ,
140- y : Number ( y ) ,
141- width : Number ( width ) ,
142- height : Number ( height ) ,
143- } ;
144- }
145-
14690/**
147- * The z-order column sits between the orientation column and the rect group. The name, six
148- * id/flag columns, z-order, and orientation precede the rect, so the eighth token is the
149- * z-order; anything else reads as unknown and leaves the rect as the only visibility signal .
91+ * The z-order column sits between the flag columns and the orientation column, ahead of the
92+ * rect group. The name, six id/flag columns, z-order, and orientation precede the rect, so the
93+ * eighth token is the z-order; anything else reads as unknown.
15094 */
95+ function readableZOrder ( z : number | undefined ) : z is number {
96+ return z !== undefined ;
97+ }
98+
15199function zOrderFromWindowRow ( line : string ) : number | undefined {
152100 const beforeRect = line . slice ( 0 , line . indexOf ( '[' ) ) ;
153101 const columns = beforeRect . trim ( ) . split ( / \s + / ) ;
0 commit comments