Skip to content

Commit 8da093e

Browse files
committed
refactor(harmonyos): publish keyboard visibility only, never a band frame
The window table proves no band: on the measured build the panel row reports the whole display whether the keyboard shows or hides, so the rect branch of the producer was dead code on every build it could observe — and a future build with a partial container would have published that container as the key plane, which the tap guard trusts, reviving the false refusals #4285 set out to remove. The producer now reads the panel row's z-order alone: visible when any panel carries a non-negative z-order, hidden when none does. A readable-z-only guard answers unmeasurable (the rect column no longer backs any verdict), and panel rows that disagree across displays answer unmeasurable rather than pick a winner, since the probe has no display scope. The 0.9 display-share threshold, the largest-rect display derivation, and the multi-display scan are deleted — the guard cannot be handed a frame this table cannot prove.
1 parent ca02dfc commit 8da093e

2 files changed

Lines changed: 85 additions & 129 deletions

File tree

‎packages/platform-harmonyos/src/__tests__/keyboard-band.test.ts‎

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,16 @@ const WINDOW_TABLE_HEADER = [
2929
'SCBScenePanel12 0 5714 4 2109 1 0 100 0 [ 0 0 1080 2406 ] [ 0 0 ] [ 1 1 0.5 0.5 ]',
3030
'app-under-test0 0 8131 38 1 1 0 101 0 [ 0 0 1080 2406 ] [ 0 0 ] [ 1 1 0.5 0.5 ]',
3131
'---------------------------------------------------------------------------------------',
32-
'SCBStatusBar17 0 5714 23 2108 1 0 -1 0 [ 0 0 1080 108 ] [ 0 0 ] [ 1 1 0 0 ]',
33-
].join('\n');
34-
35-
/** The measured hidden keyboard panel: the zero rect, not mere presence, says "hidden". */
36-
const HIDDEN_KEYBOARD_TABLE = [
37-
...WINDOW_TABLE_HEADER.split('\n'),
38-
'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0 [ 0 0 0 0 ] [ 0 0 ] [ 1 1 0 0 ]',
32+
'SCBStatusBar17 0 5714 23 2108 1 0 -1 0 [ 0 0 1080 108 ] [ 0 0 ] [ 1 1 0 0 ]',
3933
].join('\n');
4034

4135
/**
4236
* The real JUY-AL50 7.0.0.111 rows, captured verbatim from
4337
* `hidumper -s WindowManagerService -a -a` with the Settings search field focused and the IME
4438
* raised, then after a Back dismissal. The panel and the IME's own `softKeyboard1` window both
4539
* hold the full display rect in both states; only the z-order column moves (102/103 up,
46-
* both -1 hidden). No column in the public table carries the key plane.
40+
* both -1 hidden). No column in the public table carries the key plane — the producer reads
41+
* z-order only and publishes visibility, never a frame.
4742
*/
4843
const REAL_KEYBOARD_UP_ROWS = [
4944
'softKeyboard1 0 6100 16 2105 102 0 103 0 [ 0 0 1080 2406 ] [ 0 0 ] [ 1 1 0.5 0.5 ]',
@@ -54,8 +49,8 @@ const REAL_KEYBOARD_HIDDEN_ROWS = [
5449
'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0 [ 0 0 1080 2406 ] [ 0 0 ] [ 1 1 0.5 0.5 ]',
5550
].join('\n');
5651

57-
/** A panel rect materially smaller than the display: the shape a true key-plane report would take. */
58-
const TRUE_BAND_KEYBOARD_ROW =
52+
/** A panel rect materially smaller than the display: what a true key-plane report would look like. */
53+
const PARTIAL_RECT_SHOWN_ROW =
5954
'SCBKeyboardPanel 0 5714 17 2134 1 0 102 0 [ 0 1546 1080 860 ] [ 0 0 ] [ 1 1 0.5 0.5 ]';
6055

6156
beforeEach(() => {
@@ -64,67 +59,78 @@ beforeEach(() => {
6459
runHarmonyShell.mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' });
6560
});
6661

67-
test('a zero-rect keyboard panel reads as hidden, not as a band', () => {
68-
assert.deepEqual(harmonyKeyboardBandFromWindowDump(HIDDEN_KEYBOARD_TABLE), {
62+
test('the real display-spanning up-state rows publish visibility-only true, never a band frame', () => {
63+
const dump = `${WINDOW_TABLE_HEADER}\n${REAL_KEYBOARD_UP_ROWS}`;
64+
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
6965
kind: 'visibility-only',
70-
visible: false,
66+
visible: true,
7167
});
7268
});
7369

74-
test('a zero rect on a positively ordered window still reads as hidden', () => {
75-
const zeroRectShownRow =
76-
'SCBKeyboardPanel 0 5714 17 2134 1 0 4400 0 [ 0 0 0 0 ] [ 0 0 ] [ 1 1 0 0 ]';
77-
const dump = `${WINDOW_TABLE_HEADER}\n${zeroRectShownRow}`;
70+
test('the real hidden-state rows publish visibility-only false', () => {
71+
const dump = `${WINDOW_TABLE_HEADER}\n${REAL_KEYBOARD_HIDDEN_ROWS}`;
7872
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
7973
kind: 'visibility-only',
8074
visible: false,
8175
});
8276
});
8377

84-
test('the real display-spanning up-state rows publish visibility-only, not a full-screen band', () => {
85-
const dump = `${WINDOW_TABLE_HEADER}\n${REAL_KEYBOARD_UP_ROWS}`;
78+
test('a partial panel rect would still publish visibility-only, never a band frame', () => {
79+
// The future-build risk from review: a partial container must not become the key plane.
80+
// The producer never reads the rect column, so no rect shape can publish a frame.
81+
const dump = `${WINDOW_TABLE_HEADER}\n${PARTIAL_RECT_SHOWN_ROW}`;
8682
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
8783
kind: 'visibility-only',
8884
visible: true,
8985
});
9086
});
9187

92-
test('the real hidden-state rows publish visibility-only false', () => {
93-
const dump = `${WINDOW_TABLE_HEADER}\n${REAL_KEYBOARD_HIDDEN_ROWS}`;
88+
test('a negatively ordered panel row stays hidden whatever its rect claims', () => {
89+
// A stale band-shaped rect on a hidden panel must not read as shown: z-order is the only
90+
// visibility signal.
91+
const staleRectRow =
92+
'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0 [ 0 1546 1080 860 ] [ 0 0 ] [ 1 1 0 0 ]';
93+
const dump = `${WINDOW_TABLE_HEADER}\n${staleRectRow}`;
9494
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
9595
kind: 'visibility-only',
9696
visible: false,
9797
});
9898
});
9999

100-
test('a panel rect materially smaller than the display publishes the band frame', () => {
101-
const dump = `${WINDOW_TABLE_HEADER}\n${TRUE_BAND_KEYBOARD_ROW}`;
100+
test('a zero-rect hidden panel row reads as hidden', () => {
101+
// The rect column is never read; this row class (some builds hide via zero rect) still
102+
// resolves through its -1 z-order.
103+
const hiddenZeroRectRow =
104+
'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0 [ 0 0 0 0 ] [ 0 0 ] [ 1 1 0 0 ]';
105+
const dump = `${WINDOW_TABLE_HEADER}\n${hiddenZeroRectRow}`;
102106
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
103-
kind: 'visible',
104-
frame: { x: 0, y: 1546, width: 1080, height: 860 },
107+
kind: 'visibility-only',
108+
visible: false,
105109
});
106110
});
107111

108-
test('a keyboard-only table with no display-sized row stays visibility-only', () => {
109-
// The panel's own rect is the only positive rect, so the display cannot be derived and a
110-
// frame must not be published from it: the band would be an unproven guess.
112+
test('a table whose panel is the only window row still publishes visibility-only', () => {
113+
// No display-sized row to compare against — irrelevant now: nothing is derived from the
114+
// table's other rows, so a bare panel table answers through its own z-order.
111115
const dump = [
112116
'WindowName DisplayId Pid WinId Type Mode Flag ZOrd Orientation [ x y w h ] [ OffsetX OffsetY ] [ ScaleX ScaleY PivotX PivotY ]',
113-
TRUE_BAND_KEYBOARD_ROW,
117+
PARTIAL_RECT_SHOWN_ROW,
114118
].join('\n');
115119
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
116120
kind: 'visibility-only',
117121
visible: true,
118122
});
119123
});
120124

121-
test('a positive rect on a negatively ordered window stays hidden', () => {
122-
const staleRectRow =
123-
'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0 [ 0 1546 1080 860 ] [ 0 0 ] [ 1 1 0 0 ]';
124-
const dump = `${WINDOW_TABLE_HEADER}\n${staleRectRow}`;
125+
test('conflicting panel rows across displays answer unmeasurable, not a guess', () => {
126+
// A multi-display table lists one panel per display and the probe has no display scope, so
127+
// disagreeing z-orders must not pick a winner — the guard's fail-open verdict stands.
128+
const foreignShownRow =
129+
'SCBKeyboardPanel 2 5714 17 2134 1 0 500 0 [ 0 0 1080 2406 ] [ 0 0 ] [ 1 1 0.5 0.5 ]';
130+
const dump = `${WINDOW_TABLE_HEADER}\n${REAL_KEYBOARD_HIDDEN_ROWS}\n${foreignShownRow}`;
125131
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
126-
kind: 'visibility-only',
127-
visible: false,
132+
kind: 'unmeasurable',
133+
reason: 'keyboard-window-zorder-conflict',
128134
});
129135
});
130136

@@ -135,12 +141,14 @@ test('a table without the keyboard panel answers unmeasurable, not hidden', () =
135141
});
136142
});
137143

138-
test('a panel row without a readable rect answers unmeasurable', () => {
139-
const rectlessRow = 'SCBKeyboardPanel 0 5714 17 2134 1 0 -1 0';
140-
const dump = `${WINDOW_TABLE_HEADER}\n${rectlessRow}`;
144+
test('a panel row without a readable z-order answers unmeasurable', () => {
145+
// With the rect column never read, an unreadable z-order means the probe measured nothing.
146+
// (A row lacking only the rect still reads through its z-order — that column is not used.)
147+
const truncatedRow = 'SCBKeyboardPanel 0 5714 17 2134 1 0';
148+
const dump = `${WINDOW_TABLE_HEADER}\n${truncatedRow}`;
141149
assert.deepEqual(harmonyKeyboardBandFromWindowDump(dump), {
142150
kind: 'unmeasurable',
143-
reason: 'keyboard-window-rect-unparseable',
151+
reason: 'keyboard-window-zorder-unreadable',
144152
});
145153
});
146154

@@ -161,7 +169,7 @@ test('a failed window probe answers unmeasurable and never throws', async () =>
161169
test('readHarmonyKeyboardBand dumps the window manager table', async () => {
162170
runHarmonyShell.mockImplementation(async (_device: unknown, words: string[]) => {
163171
if (words[0] === 'hidumper') {
164-
return { exitCode: 0, stdout: HIDDEN_KEYBOARD_TABLE, stderr: '' };
172+
return { exitCode: 0, stdout: REAL_KEYBOARD_HIDDEN_ROWS, stderr: '' };
165173
}
166174
return { exitCode: 0, stdout: '', stderr: '' };
167175
});

‎packages/platform-harmonyos/src/keyboard-band.ts‎

Lines changed: 37 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import 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';
33
import { 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
*/
1113
const KEYBOARD_PANEL_WINDOW_PREFIX = 'SCBKeyboardPanel';
1214

1315
const 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
*/
2325
export 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
*/
6557
export 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

10475
type 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-
12180
function 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+
15199
function zOrderFromWindowRow(line: string): number | undefined {
152100
const beforeRect = line.slice(0, line.indexOf('['));
153101
const columns = beforeRect.trim().split(/\s+/);

0 commit comments

Comments
 (0)