Skip to content

Commit 1656494

Browse files
committed
refactor: tidy Android ANR recovery helpers
1 parent b1be819 commit 1656494

1 file changed

Lines changed: 58 additions & 40 deletions

File tree

src/daemon/android-system-dialog.ts

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export type AndroidBlockingDialogRecoveryResult = 'absent' | 'recovered' | 'fail
2424
export type AndroidBlockingDialogReadinessResult =
2525
| { status: 'clear' }
2626
| { status: 'recovered'; warning: string };
27+
type AndroidDialogButtonTapResult =
28+
| { ok: true; x: number; y: number }
29+
| {
30+
ok: false;
31+
exitCode: number;
32+
stdout: string;
33+
stderr: string;
34+
};
2735

2836
export async function recoverAndroidBlockingSystemDialog(params: {
2937
session: SessionState;
@@ -41,13 +49,8 @@ export async function recoverAndroidBlockingSystemDialog(params: {
4149
return 'absent';
4250
}
4351

44-
const { x, y } = centerOfRect(closeAppButton.rect);
45-
const tapResult = await runAndroidAdb(
46-
session.device,
47-
['shell', 'input', 'tap', String(Math.round(x)), String(Math.round(y))],
48-
{ allowFailure: true },
49-
);
50-
if (tapResult.exitCode !== 0) {
52+
const tapResult = await tapAndroidDialogButton(session, closeAppButton);
53+
if (!tapResult.ok) {
5154
emitDiagnostic({
5255
level: 'warn',
5356
phase: 'android_blocking_dialog_tap_failed',
@@ -77,7 +80,7 @@ export async function recoverAndroidBlockingSystemDialog(params: {
7780

7881
if (session.appBundleId) {
7982
await openAndroidApp(session.device, session.appBundleId);
80-
const focused = await waitForFocusedAndroidApp(session, session.appBundleId);
83+
const focused = await waitForAndroidAppFocus(session, session.appBundleId);
8184
if (!focused) {
8285
emitDiagnostic({
8386
level: 'warn',
@@ -99,8 +102,8 @@ export async function recoverAndroidBlockingSystemDialog(params: {
99102
session: session.name,
100103
deviceId: session.device.id,
101104
appBundleId: session.appBundleId,
102-
x,
103-
y,
105+
x: tapResult.x,
106+
y: tapResult.y,
104107
},
105108
});
106109
return 'recovered';
@@ -193,16 +196,13 @@ async function recoverAppOwnedAndroidBlockingSystemDialog(session: SessionState)
193196
const closeAppButton = findCloseAppButton(nodes, { requireDialogSignal: false });
194197
if (!closeAppButton?.rect) return false;
195198

196-
const { x, y } = centerOfRect(closeAppButton.rect);
197-
const tapResult = await runAndroidAdb(
198-
session.device,
199-
['shell', 'input', 'tap', String(Math.round(x)), String(Math.round(y))],
200-
{ allowFailure: true },
201-
);
202-
if (tapResult.exitCode !== 0) return false;
199+
const tapResult = await tapAndroidDialogButton(session, closeAppButton);
200+
if (!tapResult.ok) return false;
203201

204202
await openAndroidApp(session.device, session.appBundleId);
205-
const focused = await waitForRecoveredAndroidAppFocus(session, session.appBundleId);
203+
const focused = await waitForAndroidAppFocus(session, session.appBundleId, {
204+
requireNoBlockingDialog: true,
205+
});
206206
if (focused) {
207207
emitDiagnostic({
208208
level: 'warn',
@@ -211,31 +211,14 @@ async function recoverAppOwnedAndroidBlockingSystemDialog(session: SessionState)
211211
session: session.name,
212212
deviceId: session.device.id,
213213
appBundleId: session.appBundleId,
214-
x,
215-
y,
214+
x: tapResult.x,
215+
y: tapResult.y,
216216
},
217217
});
218218
}
219219
return focused;
220220
}
221221

222-
async function waitForRecoveredAndroidAppFocus(
223-
session: SessionState,
224-
appBundleId: string,
225-
): Promise<boolean> {
226-
for (let attempt = 0; attempt < ANDROID_MODAL_POLL_ATTEMPTS; attempt += 1) {
227-
const blockingFocus = await getAndroidBlockingDialogFocus(session.device);
228-
const state = await getAndroidAppState(session.device);
229-
if (!blockingFocus && state.package === appBundleId) {
230-
return true;
231-
}
232-
await sleep(ANDROID_MODAL_POLL_MS);
233-
}
234-
const blockingFocus = await getAndroidBlockingDialogFocus(session.device);
235-
const state = await getAndroidAppState(session.device);
236-
return !blockingFocus && state.package === appBundleId;
237-
}
238-
239222
function androidBlockingDialogError(params: {
240223
session: SessionState;
241224
command: string;
@@ -266,6 +249,30 @@ async function readAndroidSnapshotNodes(session: SessionState): Promise<Snapshot
266249
return attachRefs(pruneGroupNodes(rawSnapshot.nodes));
267250
}
268251

252+
async function tapAndroidDialogButton(
253+
session: SessionState,
254+
button: SnapshotNode,
255+
): Promise<AndroidDialogButtonTapResult> {
256+
if (!button.rect) {
257+
return { ok: false, exitCode: 1, stdout: '', stderr: 'button has no rect' };
258+
}
259+
const { x, y } = centerOfRect(button.rect);
260+
const result = await runAndroidAdb(
261+
session.device,
262+
['shell', 'input', 'tap', String(Math.round(x)), String(Math.round(y))],
263+
{ allowFailure: true },
264+
);
265+
if (result.exitCode !== 0) {
266+
return {
267+
ok: false,
268+
exitCode: result.exitCode,
269+
stdout: result.stdout.trim(),
270+
stderr: result.stderr.trim(),
271+
};
272+
}
273+
return { ok: true, x, y };
274+
}
275+
269276
function findCloseAppButton(
270277
nodes: SnapshotNode[],
271278
options: { requireDialogSignal?: boolean } = {},
@@ -292,17 +299,28 @@ async function waitForBlockingDialogToDismiss(session: SessionState): Promise<bo
292299
return !containsBlockingDialog(nodes);
293300
}
294301

295-
async function waitForFocusedAndroidApp(
302+
async function waitForAndroidAppFocus(
296303
session: SessionState,
297304
appBundleId: string,
305+
options: { requireNoBlockingDialog?: boolean } = {},
298306
): Promise<boolean> {
299307
for (let attempt = 0; attempt < ANDROID_MODAL_POLL_ATTEMPTS; attempt += 1) {
300-
const state = await getAndroidAppState(session.device);
301-
if (state.package === appBundleId) {
308+
if (await isAndroidAppFocused(session, appBundleId, options)) {
302309
return true;
303310
}
304311
await sleep(ANDROID_MODAL_POLL_MS);
305312
}
313+
return await isAndroidAppFocused(session, appBundleId, options);
314+
}
315+
316+
async function isAndroidAppFocused(
317+
session: SessionState,
318+
appBundleId: string,
319+
options: { requireNoBlockingDialog?: boolean },
320+
): Promise<boolean> {
321+
if (options.requireNoBlockingDialog && (await getAndroidBlockingDialogFocus(session.device))) {
322+
return false;
323+
}
306324
const state = await getAndroidAppState(session.device);
307325
return state.package === appBundleId;
308326
}

0 commit comments

Comments
 (0)