@@ -5,6 +5,7 @@ import { emitAndroidAdbDiagnostic, requireAndroidAdbHost } from './adb-host.ts';
55import { resolveAndroidAdbExecutor , resolveAndroidAdbProvider } from './adb-provider-scope.ts' ;
66import { runAdbShell } from './adb-executor.ts' ;
77import type { AndroidAdbExecutor } from './adb-transport.ts' ;
8+ import type { AndroidImeHelperArtifact } from './helper-artifacts.ts' ;
89import {
910 ANDROID_IME_HELPER_SERVICE_COMPONENT ,
1011 ensureAndroidImeHelper ,
@@ -80,15 +81,20 @@ async function readActivationRestoreTarget(
8081 priorPersistedIme : string | undefined ,
8182) : Promise < string > {
8283 if ( priorPersistedIme === undefined ) return currentIme ;
83- return ( await readPersistedRebindDisplacement ( adb ) ) ? priorPersistedIme : currentIme ;
84+ // An unreadable record keeps the recorded target rather than risk recording the fallback.
85+ return ( await readPersistedRebindDisplacement ( adb ) ) === false ? currentIme : priorPersistedIme ;
8486}
8587
86- /** A confirmed activation settles any earlier unconfirmed rebind, which only a prior record can carry. */
88+ /**
89+ * A confirmed activation settles any earlier unconfirmed rebind, which only a prior record can carry.
90+ * Answers false when the record may still mark the rebind.
91+ */
8792async function settleRebindDisplacement (
8893 adb : AndroidAdbExecutor ,
8994 priorPersistedIme : string | undefined ,
90- ) : Promise < void > {
91- if ( priorPersistedIme !== undefined ) await clearPersistedRebindDisplacement ( adb ) ;
95+ ) : Promise < boolean > {
96+ if ( priorPersistedIme === undefined ) return true ;
97+ return await clearPersistedRebindDisplacement ( adb ) ;
9298}
9399
94100function androidTestImeUnavailableOutcome ( error : unknown ) : AndroidTestImeActivationResult {
@@ -123,23 +129,7 @@ async function activateAndroidTestImeAfterStartupRecovery(
123129
124130 const currentIme = await readAndroidDefaultInputMethod ( adb ) ;
125131 if ( currentIme === manifest . serviceComponent ) {
126- // Already active (idempotent call, or a previous crashed daemon left it active); keep the
127- // existing persisted previous-IME record rather than overwriting it, but make sure this
128- // process's crash is covered by a recovery marker.
129- const markerPersisted = await markers . write ( options . stateDir , device . id ) ;
130- const previousIme = await readPersistedPreviousIme ( adb ) ;
131- if ( markerPersisted ) {
132- activeTestImeDevices . set ( deviceKey , { stateDir : options . stateDir , rebindUnconfirmed : false } ) ;
133- }
134- return {
135- outcome : 'settled' ,
136- activated : false ,
137- alreadyActive : true ,
138- ...( markerPersisted ? { } : { persistFailed : true } ) ,
139- previousIme,
140- helperServiceComponent : manifest . serviceComponent ,
141- helperPackageName : manifest . packageName ,
142- } ;
132+ return await claimAlreadyActiveTestIme ( device , adb , manifest , options . stateDir ) ;
143133 }
144134
145135 // Durably record the restore target BEFORE the switch: confirm the settings write succeeded and
@@ -222,8 +212,11 @@ async function activateAndroidTestImeAfterStartupRecovery(
222212
223213 // The recovery lock spans both durable records and the switch, so this process only claims
224214 // active ownership after the helper is confirmed active on the device.
225- activeTestImeDevices . set ( deviceKey , { stateDir : options . stateDir , rebindUnconfirmed : false } ) ;
226- await settleRebindDisplacement ( adb , priorPersistedIme ) ;
215+ const rebindSettled = await settleRebindDisplacement ( adb , priorPersistedIme ) ;
216+ activeTestImeDevices . set ( deviceKey , {
217+ stateDir : options . stateDir ,
218+ rebindUnconfirmed : ! rebindSettled ,
219+ } ) ;
227220 emitAndroidAdbDiagnostic ( {
228221 phase : 'android_test_ime_activated' ,
229222 data : { device : device . id , previousIme } ,
@@ -238,6 +231,39 @@ async function activateAndroidTestImeAfterStartupRecovery(
238231 } ;
239232}
240233
234+ async function claimAlreadyActiveTestIme (
235+ device : DeviceInfo ,
236+ adb : AndroidAdbExecutor ,
237+ manifest : AndroidImeHelperArtifact [ 'manifest' ] ,
238+ stateDir : string ,
239+ ) : Promise < AndroidTestImeActivationResult > {
240+ const deviceKey = getAndroidImeHelperDeviceKey ( device ) ;
241+ // Already active (idempotent call, or a previous crashed daemon left it active); keep the
242+ // existing persisted previous-IME record rather than overwriting it, but make sure this
243+ // process's crash is covered by a recovery marker.
244+ const markerPersisted = await requireAndroidAdbHost ( ) . imeRecoveryMarkers . write (
245+ stateDir ,
246+ device . id ,
247+ ) ;
248+ const previousIme = await readPersistedPreviousIme ( adb ) ;
249+ if ( markerPersisted ) {
250+ // A rebind that a previous activation or process left unconfirmed still needs one.
251+ const rebindUnconfirmed =
252+ activeTestImeDevices . get ( deviceKey ) ?. rebindUnconfirmed === true ||
253+ ( await readPersistedRebindDisplacement ( adb ) ) !== false ;
254+ activeTestImeDevices . set ( deviceKey , { stateDir, rebindUnconfirmed } ) ;
255+ }
256+ return {
257+ outcome : 'settled' ,
258+ activated : false ,
259+ alreadyActive : true ,
260+ ...( markerPersisted ? { } : { persistFailed : true } ) ,
261+ previousIme,
262+ helperServiceComponent : manifest . serviceComponent ,
263+ helperPackageName : manifest . packageName ,
264+ } ;
265+ }
266+
241267/**
242268 * Rebinds the owned test IME so the focused field starts a fresh input session with it, and answers
243269 * whether the helper reads back as the selected IME. Serialized with activation and restore under
@@ -256,6 +282,7 @@ export async function rebindAndroidTestIme(device: DeviceInfo): Promise<boolean>
256282 phase : 'android_test_ime_rebind' ,
257283 data : { device : device . id } ,
258284 } ) ;
285+ ownership . rebindUnconfirmed = true ;
259286 if ( ! ( await writePersistedRebindDisplacement ( adb ) ) ) {
260287 emitAndroidAdbDiagnostic ( {
261288 level : 'warn' ,
@@ -264,11 +291,18 @@ export async function rebindAndroidTestIme(device: DeviceInfo): Promise<boolean>
264291 } ) ;
265292 return false ;
266293 }
267- ownership . rebindUnconfirmed = true ;
268294 const activeIme = await rebindAndReadSelectedIme ( adb ) ;
269295 if ( activeIme === ANDROID_IME_HELPER_SERVICE_COMPONENT ) {
270- ownership . rebindUnconfirmed = false ;
271- await clearPersistedRebindDisplacement ( adb ) ;
296+ // The helper holds a fresh session either way; an uncleared record keeps the next entry on
297+ // the rebind path, which retries the clear.
298+ ownership . rebindUnconfirmed = ! ( await clearPersistedRebindDisplacement ( adb ) ) ;
299+ if ( ownership . rebindUnconfirmed ) {
300+ emitAndroidAdbDiagnostic ( {
301+ level : 'warn' ,
302+ phase : 'android_test_ime_rebind_record_clear_failed' ,
303+ data : { device : device . id } ,
304+ } ) ;
305+ }
272306 return true ;
273307 }
274308 emitAndroidAdbDiagnostic ( {
0 commit comments