@@ -84,50 +84,65 @@ async function settleTestImeFlushWindow(key: string, signal?: AbortSignal): Prom
8484 return true ;
8585}
8686
87+ // Whether this call proved the device needs no further recovery. A reason from an inspected
88+ // device answers directly; a nothing-inspected `not-activated-here` close earns it only by
89+ // consuming a deadline a confirmed restore opened. The window must be covered either way — an
90+ // aborted settle is never a completed settle.
91+ function isRecoveryMarkerClearEarned (
92+ reason : AndroidTestImeRestoreReason ,
93+ settleOutcome : FlushSettleOutcome ,
94+ flushWindowCovered : boolean ,
95+ ) : boolean {
96+ if ( ! flushWindowCovered ) return false ;
97+ if ( isDeviceRecoveryComplete ( reason ) ) return true ;
98+ return reason === 'not-activated-here' && settleOutcome === 'consumed' ;
99+ }
100+
87101export async function restoreAndroidTestIme (
88102 device : DeviceInfo ,
89103 options : { stateDir : string ; shutdownTarget ?: boolean ; signal ?: AbortSignal } ,
90104) : Promise < AndroidTestImeRestoreResult > {
91105 return await withAndroidTestImeRecoveryLock ( options . stateDir , device . id , async ( ) => {
92- const deviceKey = getAndroidImeHelperDeviceKey ( device ) ;
93- const settleKey = flushSettleKey ( options . stateDir , device . id ) ;
94- const killingTarget = options . shutdownTarget === true && device . kind === 'emulator' ;
106+ const settling = options . shutdownTarget === true && device . kind === 'emulator' ;
95107 // This call returns before the close finalizer may start the kill, so a window opened by an
96108 // earlier aborted close is consumed here rather than outrunning this call's own restore.
97109 // An ordinary close starts no kill, so it neither consumes the window nor needs it covered;
98110 // the pending deadline stays for the next shutdown-bound close.
99- const settleOutcome = killingTarget
100- ? await awaitPendingFlushSettle ( settleKey , options . signal )
111+ const settleOutcome = settling
112+ ? await awaitPendingFlushSettle ( flushSettleKey ( options . stateDir , device . id ) , options . signal )
101113 : 'none' ;
102- let flushWindowCovered = settleOutcome !== 'aborted' ;
103- let result : AndroidTestImeRestoreResult ;
104- if ( ! activeTestImeDevices . has ( deviceKey ) ) {
105- // Skip devices this process never activated (orphans from another process are handled by
106- // restoreOrphanedAndroidTestImeOnDaemonStartup and the doctor check). Nothing was inspected,
107- // so the device's recovery status stays unknown and its pending marker is none of ours —
108- // unless this call just consumed a deadline opened by a confirmed restore.
109- result = { restored : false , reason : 'not-activated-here' } ;
110- } else {
111- // Drop the owned-flag first so restoreAndroidTestImeFor's "owned by a live session" guard
112- // does not skip this intentional close-time restore. The IME really is back on the previous
113- // keyboard, so text routing must stop preferring the helper channel even on an abort.
114- activeTestImeDevices . delete ( deviceKey ) ;
115- const adb = resolveAndroidAdbExecutor ( device ) ;
116- result = await restoreAndroidTestImeFor ( adb , device ) ;
117- if ( result . restored && killingTarget ) {
118- flushWindowCovered = await settleTestImeFlushWindow ( settleKey , options . signal ) ;
119- }
120- }
121- const recoveryComplete =
122- isDeviceRecoveryComplete ( result . reason ) ||
123- ( result . reason === 'not-activated-here' && settleOutcome === 'consumed' ) ;
124- if ( flushWindowCovered && recoveryComplete ) {
114+ const result = await restoreOwnedAndroidTestIme ( device ) ;
115+ const flushWindowCovered =
116+ result . restored && settling
117+ ? await settleTestImeFlushWindow (
118+ flushSettleKey ( options . stateDir , device . id ) ,
119+ options . signal ,
120+ )
121+ : settleOutcome !== 'aborted' ;
122+ if ( isRecoveryMarkerClearEarned ( result . reason , settleOutcome , flushWindowCovered ) ) {
125123 await requireAndroidAdbHost ( ) . imeRecoveryMarkers . clear ( options . stateDir , device . id ) ;
126124 }
127125 return result ;
128126 } ) ;
129127}
130128
129+ // Restore this process's own device, or report that it never activated one. For an unactivated
130+ // device nothing is inspected (orphans from another process are handled by
131+ // restoreOrphanedAndroidTestImeOnDaemonStartup and the doctor check), so recovery stays unknown.
132+ async function restoreOwnedAndroidTestIme (
133+ device : DeviceInfo ,
134+ ) : Promise < AndroidTestImeRestoreResult > {
135+ const deviceKey = getAndroidImeHelperDeviceKey ( device ) ;
136+ if ( ! activeTestImeDevices . has ( deviceKey ) ) {
137+ return { restored : false , reason : 'not-activated-here' } ;
138+ }
139+ // Drop the owned-flag first so restoreAndroidTestImeFor's "owned by a live session" guard does
140+ // not skip this intentional close-time restore. The IME really is back on the previous keyboard,
141+ // so text routing must stop preferring the helper channel even on an abort.
142+ activeTestImeDevices . delete ( deviceKey ) ;
143+ return await restoreAndroidTestImeFor ( resolveAndroidAdbExecutor ( device ) , device ) ;
144+ }
145+
131146// The device was inspected and the helper is confirmed off it (restored, already not the active
132147// IME, or no rebind record). A `set-failed` (still stuck), `record-unreadable` (the device may be
133148// on Android's fallback IME), `owned-by-live-session` (a live session will restore it on close)
0 commit comments