@@ -3,8 +3,10 @@ import os from 'node:os';
33import path from 'node:path' ;
44import { resolveFileOverridePath , runCmd , whichCmd } from '../../utils/exec.ts' ;
55import { AppError } from '../../utils/errors.ts' ;
6+ import { createTtlCache } from '../../utils/ttl-cache.ts' ;
67import type { DeviceInfo } from '../../utils/device.ts' ;
78import { isDeepLinkTarget } from '../../core/open-target.ts' ;
9+ import { APP_RESOLUTION_CACHE_TTL_MS } from '../app-resolution-cache.ts' ;
810import { waitForAndroidBoot } from './devices.ts' ;
911import { adbArgs } from './adb.ts' ;
1012import { classifyAndroidAppTarget } from './open-target.ts' ;
@@ -34,16 +36,51 @@ const ANDROID_APPS_DISCOVERY_HINT =
3436const ANDROID_AMBIGUOUS_APP_HINT =
3537 'Run agent-device apps --platform android to see the exact installed package names before retrying open.' ;
3638
39+ type AndroidAppResolution = { type : 'intent' | 'package' ; value : string } ;
40+
41+ const androidAppResolutionCache = createTtlCache < AndroidAppResolution > ( ) ;
42+
43+ function buildAndroidAppResolutionCacheKey ( device : DeviceInfo , target : string ) : string {
44+ return [ 'android' , device . id , device . target ?? '' , target . trim ( ) . toLowerCase ( ) ] . join ( '\0' ) ;
45+ }
46+
47+ function readCachedAndroidAppResolution (
48+ device : DeviceInfo ,
49+ target : string ,
50+ ) : AndroidAppResolution | undefined {
51+ return androidAppResolutionCache . get ( buildAndroidAppResolutionCacheKey ( device , target ) ) ;
52+ }
53+
54+ function cacheAndroidAppResolution (
55+ device : DeviceInfo ,
56+ target : string ,
57+ resolved : AndroidAppResolution ,
58+ ) : AndroidAppResolution {
59+ return androidAppResolutionCache . set (
60+ buildAndroidAppResolutionCacheKey ( device , target ) ,
61+ resolved ,
62+ APP_RESOLUTION_CACHE_TTL_MS ,
63+ ) ;
64+ }
65+
66+ function clearAndroidAppResolutionCache ( device : DeviceInfo ) : void {
67+ const prefix = [ 'android' , device . id , '' ] . join ( '\0' ) ;
68+ androidAppResolutionCache . deleteWhere ( ( key ) => key . startsWith ( prefix ) ) ;
69+ }
70+
3771export async function resolveAndroidApp (
3872 device : DeviceInfo ,
3973 app : string ,
40- ) : Promise < { type : 'intent' | 'package' ; value : string } > {
74+ ) : Promise < AndroidAppResolution > {
4175 const trimmed = app . trim ( ) ;
4276 if ( classifyAndroidAppTarget ( trimmed ) === 'package' ) return { type : 'package' , value : trimmed } ;
4377
4478 const alias = ALIASES [ trimmed . toLowerCase ( ) ] ;
4579 if ( alias ) return alias ;
4680
81+ const cached = readCachedAndroidAppResolution ( device , trimmed ) ;
82+ if ( cached ) return cached ;
83+
4784 const result = await runCmd ( 'adb' , adbArgs ( device , [ 'shell' , 'pm' , 'list' , 'packages' ] ) ) ;
4885 const packages = result . stdout
4986 . split ( '\n' )
@@ -54,7 +91,7 @@ export async function resolveAndroidApp(
5491 pkg . toLowerCase ( ) . includes ( trimmed . toLowerCase ( ) ) ,
5592 ) ;
5693 if ( matches . length === 1 ) {
57- return { type : 'package' , value : matches [ 0 ] } ;
94+ return cacheAndroidAppResolution ( device , trimmed , { type : 'package' , value : matches [ 0 ] } ) ;
5895 }
5996
6097 if ( matches . length > 1 ) {
@@ -560,10 +597,16 @@ export async function installAndroidInstallablePath(
560597 device : DeviceInfo ,
561598 installablePath : string ,
562599) : Promise < void > {
600+ clearAndroidAppResolutionCache ( device ) ;
563601 if ( ! device . booted ) {
564602 await waitForAndroidBoot ( device . id ) ;
565603 }
566- await installAndroidAppFiles ( device , installablePath ) ;
604+ try {
605+ await installAndroidAppFiles ( device , installablePath ) ;
606+ } finally {
607+ // A concurrent name lookup can finish after the initial clear and repopulate stale data.
608+ clearAndroidAppResolutionCache ( device ) ;
609+ }
567610}
568611
569612export async function installAndroidInstallablePathAndResolvePackageName (
@@ -617,18 +660,23 @@ export async function reinstallAndroidApp(
617660 app : string ,
618661 appPath : string ,
619662) : Promise < { package : string } > {
663+ clearAndroidAppResolutionCache ( device ) ;
620664 if ( ! device . booted ) {
621665 await waitForAndroidBoot ( device . id ) ;
622666 }
623- const { package : pkg } = await uninstallAndroidApp ( device , app ) ;
624- const prepared = await prepareAndroidInstallArtifact (
625- { kind : 'path' , path : appPath } ,
626- { resolveIdentity : false } ,
627- ) ;
628667 try {
629- await installAndroidInstallablePath ( device , prepared . installablePath ) ;
668+ const { package : pkg } = await uninstallAndroidApp ( device , app ) ;
669+ const prepared = await prepareAndroidInstallArtifact (
670+ { kind : 'path' , path : appPath } ,
671+ { resolveIdentity : false } ,
672+ ) ;
673+ try {
674+ await installAndroidInstallablePath ( device , prepared . installablePath ) ;
675+ } finally {
676+ await prepared . cleanup ( ) ;
677+ }
630678 return { package : pkg } ;
631679 } finally {
632- await prepared . cleanup ( ) ;
680+ clearAndroidAppResolutionCache ( device ) ;
633681 }
634682}
0 commit comments