diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml index f1bebfe..fb0f1e9 100644 --- a/.github/workflows/run.yml +++ b/.github/workflows/run.yml @@ -31,7 +31,7 @@ jobs: image: system-images;android-34;google_apis;x86_64 abi: google_apis/x86_64 cores: 2 - cpu_threshold: 3 + cpu_threshold: 25 timeout_seconds: 1800 runs-on: ${{ matrix.os }} env: @@ -163,23 +163,30 @@ jobs: - name: Validate Launch test app timeout-minutes: 10 run: | - FOUND=0 + FOUND_APP=0 + FOUND_DISPLAYED=0 for i in $(seq 1 30); do dotnet $TOOL device logcat --output artifacts/device-logcat.txt - if grep -q "$APP_START_MARKER" artifacts/device-logcat.txt; then - echo "✅ Marker found on attempt $i" - FOUND=1 + if [ "$FOUND_APP" -eq 0 ] && grep -q "$APP_START_MARKER" artifacts/device-logcat.txt; then + echo "✅ App marker '$APP_START_MARKER' found on attempt $i" + FOUND_APP=1 + fi + if [ "$FOUND_DISPLAYED" -eq 0 ] && grep -q "Displayed $TEST_APP_PACKAGE" artifacts/device-logcat.txt; then + echo "✅ System 'Displayed' marker found on attempt $i" + FOUND_DISPLAYED=1 + fi + if [ "$FOUND_APP" -eq 1 ] && [ "$FOUND_DISPLAYED" -eq 1 ]; then break fi - echo "Attempt $i/30 - marker not found yet, waiting 5s..." + echo "Attempt $i/30 - waiting 5s... (app_marker=$FOUND_APP, displayed=$FOUND_DISPLAYED)" sleep 5 done - if [ "$FOUND" -ne 1 ]; then - echo "❌ Marker '$APP_START_MARKER' not found in logcat after 30 attempts" + if [ "$FOUND_APP" -ne 1 ] || [ "$FOUND_DISPLAYED" -ne 1 ]; then + echo "❌ Not all markers found after 30 attempts (app_marker=$FOUND_APP, displayed=$FOUND_DISPLAYED)" cat artifacts/device-logcat.txt exit 1 fi - echo "✅ Launch validated via logcat marker" + echo "✅ Launch validated — both markers found" env: ANDROID_TOOL_PROCESS_RUNNER_LOG_PATH: artifacts/device-launch-validate.log diff --git a/AndroidSdk/Emulator/Emulator.cs b/AndroidSdk/Emulator/Emulator.cs index d0fd0ff..73c70c8 100644 --- a/AndroidSdk/Emulator/Emulator.cs +++ b/AndroidSdk/Emulator/Emulator.cs @@ -373,6 +373,7 @@ public bool WaitForBootComplete(CancellationToken token) } // Keep trying to see if the boot complete prop is set + var booted = false; while (!token.IsCancellationRequested) { if (process.HasExited) @@ -381,14 +382,62 @@ public bool WaitForBootComplete(CancellationToken token) if (adb.Shell("getprop dev.bootcomplete", Serial).Any(l => l.Contains("1")) || adb.Shell("getprop sys.boot_completed", Serial).Any(l => l.Contains("1"))) { - return true; + booted = true; + break; } if (token.WaitHandle.WaitOne(1000)) return false; } - return false; + // After boot_completed=1, wait for the launcher to gain focus. + // On slow emulators (e.g. macOS without KVM) ANR dialogs can + // steal mCurrentFocus and block the launcher from appearing. + // Dismiss them so the launcher can take focus. Use a hard 120s + // timeout so we never block indefinitely — if the launcher + // doesn't appear, proceed anyway since boot IS complete. + if (booted && !token.IsCancellationRequested) + { + var launcherTimeout = TimeSpan.FromSeconds(120); + var launcherSw = System.Diagnostics.Stopwatch.StartNew(); + + while (launcherSw.Elapsed < launcherTimeout && !token.IsCancellationRequested) + { + if (process.HasExited) + break; + + try + { + var output = adb.Shell("dumpsys window displays", Serial); + + if (output.Any(l => + l.Contains("mCurrentFocus", StringComparison.OrdinalIgnoreCase) && + l.Contains("launcher", StringComparison.OrdinalIgnoreCase))) + { + break; + } + + // ANR dialogs block the launcher from getting focus. + // Dismiss them by pressing BACK so the launcher can appear. + if (output.Any(l => + l.Contains("mCurrentFocus", StringComparison.OrdinalIgnoreCase) && + l.Contains("Application Not Responding", StringComparison.OrdinalIgnoreCase))) + { + try { adb.Shell("input keyevent KEYCODE_BACK", Serial); } + catch (SdkToolFailedExitException) { } + } + } + catch (SdkToolFailedExitException) + { + // adb can transiently fail; retry on next iteration. + } + + if (token.WaitHandle.WaitOne(2000)) + break; + } + } + + return booted; } internal bool WaitForCpuLoadBelow(double threshold, TimeSpan timeout, TimeSpan settleDelay, CancellationToken token)