diff --git a/.github/workflows/android-launch-smoke.yml b/.github/workflows/android-launch-smoke.yml new file mode 100644 index 000000000..d69fe475e --- /dev/null +++ b/.github/workflows/android-launch-smoke.yml @@ -0,0 +1,54 @@ +name: Android TWA startup smoke (emulator) + +on: + pull_request: + paths: + - android/** + - scripts/android/launch-smoke.sh + - .github/workflows/android-launch-smoke.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + startup: + name: Launch Android TWA on a real emulator and capture crashes + runs-on: ubuntu-24.04 + timeout-minutes: 35 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: android-actions/setup-android@v4 + - run: sdkmanager "platforms;android-36" "build-tools;36.0.0" + - uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.13" + - name: Assemble unsigned debug APK with real launcher configuration + working-directory: android + run: gradle --no-daemon --stacktrace :app:assembleDebug + - name: Enable hardware virtualization for emulator + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | + sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Install, launch, and inspect TWA in Google Play emulator + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 + with: + api-level: 35 + target: google_apis_playstore + arch: x86_64 + profile: pixel_7 + script: bash scripts/android/launch-smoke.sh + - name: Preserve startup logs for debugging even after a failure + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: menuminute-android-startup-logcat + path: android-launch-smoke/ + if-no-files-found: warn + retention-days: 14 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c71ec1405..5f30c53fd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,11 +11,24 @@ android:usesCleartextTraffic="false" android:label="@string/app_name" android:icon="@drawable/ic_launcher" + android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity" android:supportsRtl="true" android:theme="@android:style/Theme.Translucent.NoTitleBar"> + + + + + + "$out/packages.txt" +adb shell dumpsys package "$pkg" > "$out/installed-package.txt" +adb shell getprop ro.build.version.release > "$out/android-version.txt" +adb shell settings put global window_animation_scale 0 +adb logcat -c +adb shell am force-stop "$pkg" +echo "Launching $pkg/$activity in the clean emulator..." +if ! adb shell am start -W -n "$pkg/$activity" > "$out/launch-result.txt" 2>&1; then + cat "$out/launch-result.txt" + exit 1 +fi +sleep 22 +adb logcat -d -v threadtime -b crash > "$out/crash-buffer.txt" 2>&1 || true +adb logcat -d -v threadtime AndroidRuntime:E TWALauncherActivity:V TwaLauncher:V QualityEnforcer:V ActivityTaskManager:W '*:S' > "$out/twa-startup-log.txt" 2>&1 || true +adb shell dumpsys activity activities > "$out/activity-state.txt" || true +cat "$out/launch-result.txt" +if grep -Eq "(Process: $pkg|Process $pkg .* has died|FATAL EXCEPTION)" "$out/crash-buffer.txt"; then + echo "::error::Android launcher reported a Java crash. See captured crash-buffer.txt artifact." + sed -n '1,100p' "$out/crash-buffer.txt" + exit 1 +fi +echo "Clean-emulator startup did not report a Java fatal exception." +echo "NOTE: This checks the unsigned debug APK, not Google Play's re-signed bundle on the user's device." diff --git a/tests/android-launch-manifest.test.ts b/tests/android-launch-manifest.test.ts new file mode 100644 index 000000000..47b2db4d9 --- /dev/null +++ b/tests/android-launch-manifest.test.ts @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const manifest = readFileSync(path.resolve("android/app/src/main/AndroidManifest.xml"), "utf8"); +const application = manifest.split("")[0] ?? ""; + +describe("Android Browser Helper startup manifest completeness", () => { + it("declares the helper's site-settings activity before the TWA launcher", () => { + // LauncherActivity calls ManageDataLauncherActivity.addSiteSettingsShortcut() + // unconditionally; Android throws if its component is missing from the manifest. + expect(application).toContain( + 'android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"' + ); + expect(application).toContain( + 'android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"' + ); + expect(application).toContain('android:exported="false"'); + expect(application).toContain( + 'android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"' + ); + expect(application.indexOf("ManageDataLauncherActivity")).toBeLessThan( + application.indexOf('android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"') + ); + }); + + it("preserves the installed package, Play Billing integrations, and trusted origin", () => { + expect(application).toContain("@string/asset_statements"); + expect(application).toContain("@string/launch_url"); + expect(application).toContain('android:name=".ExtraFeaturesService"'); + expect(application).toContain( + 'android:name="com.google.androidbrowserhelper.playbilling.provider.PaymentActivity"' + ); + }); +});