@@ -24,6 +24,14 @@ export type AndroidBlockingDialogRecoveryResult = 'absent' | 'recovered' | 'fail
2424export 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
2836export 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-
239222function 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+
269276function 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