From d8c7bb3b831ac49d58eb535af266af3faee28e04 Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 22 Jul 2026 21:34:32 -0700 Subject: [PATCH 1/3] Add Android CLI experiment job and CI badge/details to README --- .github/workflows/android.yml | 145 ++++++++++++++++++++++++++++++++++ README.md | 21 +++++ 2 files changed, 166 insertions(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9f4c5b1..f395abb 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -276,3 +276,148 @@ jobs: with: name: instrumented-test-results-api${{ matrix.api-level }}${{ matrix.suffix && format('-{0}', matrix.suffix) || '' }} path: app/build/reports/androidTests/connected/ + + # Experiment: drive the whole emulator flow with the new Android CLI + # (https://d.android.com/tools/agents/android-cli) instead of + # sdkmanager/avdmanager and the emulator-runner action. + android-cli-experiment: + name: Android CLI experiment (API 37.0 canary emulator) + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') + runs-on: ubuntu-latest + timeout-minutes: 30 + continue-on-error: true + + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: temurin + cache: gradle + + - uses: gradle/actions/setup-gradle@v6 + + - name: Install Boost via apt + run: sudo apt-get install -y libboost-dev + + - name: Resolve Boost dirs + run: | + BOOST_DIR=$(find /usr/lib -maxdepth 4 -name "Boost-*" -type d 2>/dev/null \ + | sort -V | tail -1) + echo "BOOST_CMAKE_DIR=$BOOST_DIR" >> "$GITHUB_ENV" + mkdir -p /tmp/boost-headers + ln -sfn /usr/include/boost /tmp/boost-headers/boost + echo "BOOST_INCLUDE_DIR=/tmp/boost-headers" >> "$GITHUB_ENV" + + - name: Enable KVM + 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 Android CLI + run: | + curl -fsSL https://dl.google.com/android/cli/latest/linux_x86_64/install.sh | bash + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Android CLI diagnostics + run: | + android --version + android info + android sdk list | head -40 + + - name: Install canary emulator and API 37.0 ps16k image with the CLI + run: | + android sdk install --canary emulator platform-tools system-images/android-37.0/google_apis_ps16k/x86_64 + android sdk list | grep -iE "^ emulator|android-37" || true + + # 'android emulator create' is profile-based with no way to pin an image (it + # picks google_apis_playstore, whose first-boot Play overlays steal window + # focus and break Espresso). An AVD is just ini files, so create the profile + # with the CLI and retarget it at the pinned image — no sdkmanager/avdmanager. + - name: Create AVD with the CLI and retarget it to API 37.0 ps16k + run: | + android emulator create medium_phone + DEVICE="$(android emulator list | head -n1)" + AVD_DIR="$HOME/.android/avd/$DEVICE.avd" + if [ -z "$DEVICE" ] || [ ! -d "$AVD_DIR" ]; then + echo "ERROR: created AVD not found (DEVICE='$DEVICE')" + ls -la "$HOME/.android/avd/" || true + exit 1 + fi + echo "CLI created AVD: $DEVICE" + CFG="$AVD_DIR/config.ini" + # Replace-or-append so a key missing from the CLI-written config cannot + # silently keep its profile default. + set_ini() { + if grep -q "^$1=" "$CFG"; then sed -i "s#^$1=.*#$1=$2#" "$CFG"; else echo "$1=$2" >> "$CFG"; fi + } + sed -i 's/^target=.*/target=android-37.0/' "$HOME/.android/avd/$DEVICE.ini" + set_ini image.sysdir.1 'system-images/android-37.0/google_apis_ps16k/x86_64/' + set_ini tag.id google_apis + set_ini tag.display 'Google APIs' + set_ini PlayStore.enabled false + set_ini hw.ramSize 4096 + set_ini hw.cpu.ncore 4 + set_ini disk.dataPartition.size 8192M + # Optional plural tag keys, if the profile wrote them, must not keep + # claiming the playstore image. + if grep -q '^tag.ids=' "$CFG"; then sed -i 's/^tag.ids=.*/tag.ids=google_apis,page_size_16kb/' "$CFG"; fi + if grep -q '^tag.displaynames=' "$CFG"; then sed -i 's/^tag.displaynames=.*/tag.displaynames=Google APIs,16 KB Page Size/' "$CFG"; fi + echo "===== root ini ====="; cat "$HOME/.android/avd/$DEVICE.ini" + echo "===== config.ini ====="; cat "$CFG" + echo "DEVICE=$DEVICE" >> "$GITHUB_ENV" + + # 'android emulator start' passes neither -noaudio nor -no-window, so the + # runner needs libpulse and an X display. + - name: Install emulator host dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq libpulse0 xvfb + Xvfb :99 -screen 0 1280x800x24 & + echo "DISPLAY=:99" >> "$GITHUB_ENV" + + - name: Start emulator with the CLI + timeout-minutes: 15 + run: | + SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}" + export PATH="$SDK/platform-tools:$PATH" + echo "Starting AVD: $DEVICE" + # start returns 0 even if the emulator process dies; verify via adb. + android emulator start --cold "$DEVICE" + for _ in $(seq 1 30); do adb get-state >/dev/null 2>&1 && break; sleep 2; done + adb get-state >/dev/null 2>&1 || { echo "===== emulator.log ====="; cat "$HOME/.android/$DEVICE/emulator.log" 2>/dev/null || echo "(no emulator.log)"; exit 1; } + adb devices + adb shell getprop ro.build.version.sdk + adb shell getprop sys.boot_completed + # The CLI has no disable-animations equivalent (emulator-runner sets it); + # Espresso needs animations off. + adb shell settings put global window_animation_scale 0 + adb shell settings put global transition_animation_scale 0 + adb shell settings put global animator_duration_scale 0 + adb logcat -v time > "${{ github.workspace }}/emulator_logcat.txt" & + + - name: Dump emulator.log on failure + if: failure() + run: cat "$HOME/.android/$DEVICE/emulator.log" 2>/dev/null || echo "(no emulator.log)" + + - name: Run instrumented tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + + - name: Stop emulator + if: always() + run: android emulator stop "$DEVICE" || true + + - name: Dump logcat on failure + if: failure() + run: tail -n 500 "${{ github.workspace }}/emulator_logcat.txt" 2>/dev/null || echo "no logcat captured" + + - name: Upload test report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: instrumented-test-report-android-cli-experiment + path: app/build/reports/androidTests/connected/ diff --git a/README.md b/README.md index 4e6eb51..68fc758 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,26 @@ # SimpleTorrent +[![Android CI](https://github.com/jpcottin/SimpleTorrent/actions/workflows/android.yml/badge.svg?branch=master)](https://github.com/jpcottin/SimpleTorrent/actions/workflows/android.yml) + +
+CI details — emulator matrix, API 35 → 37.1, plus an Android CLI leg + +Instrumented legs run only on pushes to master (PRs run unit tests, lint, and both APK builds). + +| Legs | Image | Emulator channel | GPU | Gating | +|---|---|---|---|---| +| API 35 | `google_apis` x86_64 | stable | swiftshader | ✅ blocking | +| API 37.0 | `google_apis_ps16k` (16 KB page size) | stable | lavapipe | non-blocking | +| API 37.0 | `google_apis_ps16k` | canary (`--channel=3`) | lavapipe, auto | non-blocking | +| API 37.1 | `google_apis_ps16k` | canary | lavapipe, auto | non-blocking | +| Android CLI experiment | `google_apis_ps16k` 37.0 | canary | emulator default | non-blocking | + +The Android CLI leg drives the whole flow with the [`android` CLI](https://d.android.com/tools/agents/android-cli) (`android sdk install --canary`, `android emulator create/start/stop`) instead of `sdkmanager`/`avdmanager` and the emulator-runner action. + +All emulator-runner legs use full diagnostics (`-verbose -show-kernel -debug-metrics -metrics-collection`) and a `cmdline-tools;latest` update so `avdmanager` writes a valid `target=android-37.x` (the runner's preinstalled version writes `android-0`, which the emulator clamps to API 3, disabling the Vulkan/GLDirectMem auto-enable the ps16k images need). + +
+ A minimal, fully native BitTorrent client for Android, built as a learning project to explore the Android NDK and the libtorrent-rasterbar C++ library. From e9d3661b40223cc34b62ae9dff4156b5300035e2 Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 22 Jul 2026 21:45:15 -0700 Subject: [PATCH 2/3] Run Compose Preview screenshot validation in CI The screenshot plugin, tests, and reference images were all in place but validateDebugScreenshotTest was never wired into a workflow. --- .github/workflows/android.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f395abb..1e92fce 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -31,6 +31,16 @@ jobs: - name: Run unit tests run: ./gradlew testDebugUnitTest --no-daemon + - name: Validate Compose Preview Screenshots + run: ./gradlew :app:validateDebugScreenshotTest --no-daemon + + - name: Upload screenshot test report on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: screenshot-test-report + path: app/build/reports/screenshotTest/preview/debug/ + - name: Upload test results if: always() uses: actions/upload-artifact@v7 From 164c6a478e9a902fb4e9ebab7ea884c329e7214e Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 22 Jul 2026 21:56:00 -0700 Subject: [PATCH 3/3] Regenerate drifted MagnetInputBar screenshot golden First CI validation of the screenshot suite caught a stale reference: the empty-state MagnetInputBar golden no longer matched the current composable. --- ...t input \342\200\224 empty_03c8f9f1_0.png" | Bin 9486 -> 9802 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git "a/app/src/screenshotTestDebug/reference/com/jpcottin/simpletorrent/ScreenshotTestsKt/MagnetInputBarScreenshot_Magnet input \342\200\224 empty_03c8f9f1_0.png" "b/app/src/screenshotTestDebug/reference/com/jpcottin/simpletorrent/ScreenshotTestsKt/MagnetInputBarScreenshot_Magnet input \342\200\224 empty_03c8f9f1_0.png" index d46afa73460ce63aba55390cc660680cb1333607..1840f967327121f590d0855a2eedc2de1d6886df 100644 GIT binary patch literal 9802 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 D_%rB& literal 9486 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 g5cJ