@@ -16,15 +16,15 @@ const requireRunningAndroid =
1616 process . env . SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID === "1" ||
1717 process . env . CI === "true" ;
1818const requestedAvd = process . env . SIMDECK_INTEGRATION_ANDROID_AVD ;
19- const androidLaunchTarget =
20- process . env . SIMDECK_INTEGRATION_ANDROID_LAUNCH_TARGET ??
21- "com.android.settings/.Settings" ;
19+ const requestedAndroidLaunchTarget =
20+ process . env . SIMDECK_INTEGRATION_ANDROID_LAUNCH_TARGET ;
2221const defaultStepTimeoutMs = Number (
2322 process . env . SIMDECK_INTEGRATION_STEP_TIMEOUT_MS ?? "180000" ,
2423) ;
2524
2625let session = null ;
2726let androidUDID = "" ;
27+ let androidLaunchTarget = null ;
2828let shutdownAndroidAfterRun = false ;
2929const stepTimings = [ ] ;
3030
@@ -106,6 +106,12 @@ async function main() {
106106 { timeoutMs : target . isBooted ? 120_000 : 300_000 } ,
107107 ) ;
108108
109+ androidLaunchTarget = await measuredStep (
110+ "resolve Android launch target" ,
111+ ( ) => resolveAndroidLaunchTarget ( ) ,
112+ { timeoutMs : 60_000 } ,
113+ ) ;
114+
109115 await runCliSurface ( ) ;
110116 await runJsSurface ( ) ;
111117 console . log ( "SimDeck Android integration suite passed" ) ;
@@ -216,12 +222,35 @@ async function runCliSurface() {
216222 }
217223 } ) ;
218224 await measuredStep ( "CLI app launch and URL" , ( ) => {
219- simdeckJson ( [ "launch" , androidUDID , androidLaunchTarget ] , {
220- timeoutMs : 60_000 ,
221- } ) ;
222- simdeckJson ( [ "open-url" , androidUDID , "https://example.com" ] , {
223- timeoutMs : 60_000 ,
224- } ) ;
225+ if ( androidLaunchTarget ) {
226+ try {
227+ simdeckJson ( [ "launch" , androidUDID , androidLaunchTarget ] , {
228+ timeoutMs : 60_000 ,
229+ } ) ;
230+ } catch ( error ) {
231+ if (
232+ requestedAndroidLaunchTarget ||
233+ ! isAndroidIntentUnavailable ( error )
234+ ) {
235+ throw error ;
236+ }
237+ console . log (
238+ `Android image rejected discovered launch target ${ androidLaunchTarget } ; continuing with URL/home coverage.` ,
239+ ) ;
240+ }
241+ } else {
242+ console . log ( "Android image did not expose a launchable activity." ) ;
243+ }
244+ try {
245+ simdeckJson ( [ "open-url" , androidUDID , "https://example.com" ] , {
246+ timeoutMs : 60_000 ,
247+ } ) ;
248+ } catch ( error ) {
249+ if ( ! isAndroidIntentUnavailable ( error ) ) {
250+ throw error ;
251+ }
252+ console . log ( "Android image did not expose an https URL handler." ) ;
253+ }
225254 simdeckJson ( [ "home" , androidUDID ] ) ;
226255 } ) ;
227256 await measuredStep ( "CLI pointer gestures" , ( ) => {
@@ -358,8 +387,29 @@ async function runJsSurface() {
358387 }
359388 } ) ;
360389 await measuredStep ( "JS app launch and URL" , async ( ) => {
361- await session . launch ( androidUDID , androidLaunchTarget ) ;
362- await session . openUrl ( androidUDID , "https://example.com" ) ;
390+ if ( androidLaunchTarget ) {
391+ try {
392+ await session . launch ( androidUDID , androidLaunchTarget ) ;
393+ } catch ( error ) {
394+ if (
395+ requestedAndroidLaunchTarget ||
396+ ! isAndroidIntentUnavailable ( error )
397+ ) {
398+ throw error ;
399+ }
400+ console . log (
401+ `Android image rejected discovered launch target ${ androidLaunchTarget } ; continuing with URL/home coverage.` ,
402+ ) ;
403+ }
404+ }
405+ try {
406+ await session . openUrl ( androidUDID , "https://example.com" ) ;
407+ } catch ( error ) {
408+ if ( ! isAndroidIntentUnavailable ( error ) ) {
409+ throw error ;
410+ }
411+ console . log ( "Android image did not expose an https URL handler." ) ;
412+ }
363413 await session . home ( androidUDID ) ;
364414 } ) ;
365415 await measuredStep ( "JS pointer gestures" , async ( ) => {
@@ -461,19 +511,128 @@ function resolveAndroidDevice() {
461511 : null ;
462512}
463513
514+ function resolveAndroidLaunchTarget ( ) {
515+ if ( requestedAndroidLaunchTarget ) {
516+ return requestedAndroidLaunchTarget ;
517+ }
518+ const adb = androidSdkTool ( "platform-tools/adb" ) ;
519+ if ( ! adb ) {
520+ return null ;
521+ }
522+ const serial = onlineAndroidSerials ( adb ) [ 0 ] ;
523+ if ( ! serial ) {
524+ return null ;
525+ }
526+ const queries = [
527+ [
528+ "cmd" ,
529+ "package" ,
530+ "query-activities" ,
531+ "--brief" ,
532+ "--components" ,
533+ "-a" ,
534+ "android.intent.action.MAIN" ,
535+ "-c" ,
536+ "android.intent.category.LAUNCHER" ,
537+ ] ,
538+ [
539+ "cmd" ,
540+ "package" ,
541+ "query-activities" ,
542+ "--brief" ,
543+ "--components" ,
544+ "-a" ,
545+ "android.intent.action.MAIN" ,
546+ ] ,
547+ [
548+ "cmd" ,
549+ "package" ,
550+ "resolve-activity" ,
551+ "--brief" ,
552+ "--components" ,
553+ "-a" ,
554+ "android.intent.action.MAIN" ,
555+ "-c" ,
556+ "android.intent.category.LAUNCHER" ,
557+ ] ,
558+ [
559+ "cmd" ,
560+ "package" ,
561+ "resolve-activity" ,
562+ "--brief" ,
563+ "--components" ,
564+ "-a" ,
565+ "android.intent.action.MAIN" ,
566+ ] ,
567+ ] ;
568+ for ( const query of queries ) {
569+ try {
570+ const output = runText ( adb , [ "-s" , serial , "shell" , ...query ] , {
571+ timeoutMs : 30_000 ,
572+ } ) ;
573+ const target = chooseAndroidLaunchComponent (
574+ parseAndroidActivityComponents ( output ) ,
575+ ) ;
576+ if ( target ) {
577+ if ( verbose ) {
578+ console . log ( `Android launch target ${ target } ` ) ;
579+ }
580+ return target ;
581+ }
582+ } catch ( error ) {
583+ if ( verbose ) {
584+ console . log (
585+ `Android launch target query failed: ${ error . message . split ( "\n" ) [ 0 ] } ` ,
586+ ) ;
587+ }
588+ }
589+ }
590+ return null ;
591+ }
592+
593+ function parseAndroidActivityComponents ( output ) {
594+ const components = [ ] ;
595+ for ( const line of output . split ( / \r ? \n / ) ) {
596+ for ( const token of line . trim ( ) . split ( / \s + / ) ) {
597+ if (
598+ / ^ [ A - Z a - z 0 - 9 _ . ] + \/ (?: [ A - Z a - z 0 - 9 _ . $ ] + | \. [ A - Z a - z 0 - 9 _ . $ ] + ) $ / . test ( token )
599+ ) {
600+ components . push ( token ) ;
601+ }
602+ }
603+ }
604+ return [ ...new Set ( components ) ] ;
605+ }
606+
607+ function chooseAndroidLaunchComponent ( components ) {
608+ const preferredPackages = [
609+ "com.android.settings/" ,
610+ "com.google.android.apps.nexuslauncher/" ,
611+ "com.android.launcher3/" ,
612+ ] ;
613+ for ( const packagePrefix of preferredPackages ) {
614+ const component = components . find ( ( value ) =>
615+ value . startsWith ( packagePrefix ) ,
616+ ) ;
617+ if ( component ) {
618+ return component ;
619+ }
620+ }
621+ return (
622+ components . find (
623+ ( value ) =>
624+ ! value . startsWith ( "com.android.systemui/" ) &&
625+ ! value . startsWith ( "android/" ) ,
626+ ) ?? null
627+ ) ;
628+ }
629+
464630function runningAndroidAvds ( fallbackAvdName = "" ) {
465631 const adb = androidSdkTool ( "platform-tools/adb" ) ;
466632 if ( ! adb ) {
467633 return new Set ( ) ;
468634 }
469- const devices = runText ( adb , [ "devices" ] , { timeoutMs : 30_000 } )
470- . split ( / \r ? \n / )
471- . map ( ( line ) => line . trim ( ) . split ( / \s + / ) )
472- . filter (
473- ( [ serial , state ] ) =>
474- serial ?. startsWith ( "emulator-" ) && state === "device" ,
475- )
476- . map ( ( [ serial ] ) => serial ) ;
635+ const devices = onlineAndroidSerials ( adb ) ;
477636 const avds = new Set ( ) ;
478637 for ( const serial of devices ) {
479638 const name = androidAvdNameForSerial ( adb , serial ) ;
@@ -487,6 +646,17 @@ function runningAndroidAvds(fallbackAvdName = "") {
487646 return avds ;
488647}
489648
649+ function onlineAndroidSerials ( adb ) {
650+ return runText ( adb , [ "devices" ] , { timeoutMs : 30_000 } )
651+ . split ( / \r ? \n / )
652+ . map ( ( line ) => line . trim ( ) . split ( / \s + / ) )
653+ . filter (
654+ ( [ serial , state ] ) =>
655+ serial ?. startsWith ( "emulator-" ) && state === "device" ,
656+ )
657+ . map ( ( [ serial ] ) => serial ) ;
658+ }
659+
490660function androidAvdNameForSerial ( adb , serial ) {
491661 for ( const property of [ "ro.boot.qemu.avd_name" , "ro.kernel.qemu.avd_name" ] ) {
492662 try {
@@ -716,6 +886,13 @@ function assertClipboardUnsupported(errorOrResult) {
716886 ) ;
717887}
718888
889+ function isAndroidIntentUnavailable ( error ) {
890+ const message = error instanceof Error ? error . message : String ( error ) ;
891+ return / u n a b l e t o r e s o l v e I n t e n t | N o A c t i v i t y f o u n d | A c t i v i t y c l a s s .* d o e s n o t e x i s t / i. test (
892+ message ,
893+ ) ;
894+ }
895+
719896function assertAndroidListed ( payload , udid ) {
720897 assert . ok (
721898 simulatorList ( payload ) . some (
0 commit comments