#!/usr/bin/env bash # Reproduce the gioui Android activity-recreation hang with a STOCK gioui # example app (the "hello" example from gioui.org/example). # # What it does: # 1. Builds gioui.org/example's hello app (unmodified) with gogio. # 2. Installs it on an emulator (boots one if needed). # 3. Recreates the activity via `cmd uimode night yes` (uiMode is not in # gioui's configChanges mask, so Android destroys/recreates the # activity in the same process). # 4. Taps the app and checks logcat for NO_INPUT_CHANNEL. # # The bug (gioui v0.10.x, app/os_android.go onCreateView): on re-attach the # second onCreateView sends the window-creation ack # (mainWindow.windows <- struct{}{}) on the UI thread, but its only # receiver (newWindow in Window.init) never re-runs on Android — the driver # is never reset, so w.Event() never returns to re-init. The UI thread # blocks forever, the activity never attaches an input channel, and the app # is black and unresponsive (process still alive). # # Usage: # scripts/repro_gioui_hang.sh [avd-name] [path-to-apk] # # Exit codes: 0 = bug REPRODUCED, 1 = not reproduced, 2 = setup failure. set -euo pipefail AVD=${1:-pad_avd} APK=${2:-} SDK=${ANDROID_HOME:-$HOME/android-sdk} export PATH=$PATH:$HOME/go/bin:$SDK/platform-tools:$SDK/build-tools/35.0.0 say() { printf '\n=== %s\n' "$*"; } die() { say "FAIL: $*"; exit 2; } # --- 1. Build the stock hello example, unless an APK was supplied --------- if [ -z "$APK" ]; then SRC=$(mktemp -d) trap 'rm -rf "$SRC"' EXIT say "building stock gioui.org/example hello app (v0.10.3)" go mod download gioui.org/example@v0.10.3 cp "$HOME/go/pkg/mod/gioui.org/example@v0.10.3/hello/hello.go" "$SRC/main.go" cat > "$SRC/go.mod" <<'EOF' module hello go 1.23 require gioui.org v0.10.3 EOF (cd "$SRC" && go mod tidy && gogio -target android -targetsdk 34 -arch amd64 -o /tmp/hello-gioui-repro.apk .) APK=/tmp/hello-gioui-repro.apk fi [ -f "$APK" ] || die "APK not found: $APK" # --- 2. Emulator: reuse a booted one or start $AVD ------------------------ say "waiting for emulator (avd $AVD)" EMU="" for _ in $(seq 1 60); do EMU=$(adb devices | awk '$1 ~ /^emulator-/ {print $1; exit}') [ -n "$EMU" ] && break emulator -avd "$AVD" -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect >/dev/null 2>&1 & sleep 5 done [ -n "$EMU" ] || die "no emulator available" for _ in $(seq 1 60); do bc=$(adb -s "$EMU" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') [ "$bc" = "1" ] && break sleep 5 done [ "$(adb -s "$EMU" shell getprop sys.boot_completed | tr -d '\r')" = "1" ] || die "emulator did not boot" say "emulator: $EMU" # --- 3. Install + launch --------------------------------------------------- APPID=localhost.hello # gogio's default app id for module "hello" adb -s "$EMU" install -r "$APK" >/dev/null adb -s "$EMU" shell am force-stop "$APPID" >/dev/null 2>&1 || true adb -s "$EMU" logcat -c adb -s "$EMU" shell am start -n "$APPID/org.gioui.GioActivity" >/dev/null sleep 5 adb -s "$EMU" shell pidof "$APPID" >/dev/null || die "app did not start" say "baseline: app running" # --- 4. Recreate the activity ---------------------------------------------- # uiMode is not in gioui's configChanges mask -> Android recreates the # activity. Force a real state change (a no-op toggle recreates nothing). cur=$(adb -s "$EMU" shell cmd uimode night | tr -d '\r') # "Night mode: yes/no" case $cur in *yes*) adb -s "$EMU" shell cmd uimode night no >/dev/null ;; *) adb -s "$EMU" shell cmd uimode night yes >/dev/null ;; esac sleep 5 adb -s "$EMU" logcat -c adb -s "$EMU" shell input tap 540 1200 sleep 2 # --- 5. Verdict ------------------------------------------------------------- adb -s "$EMU" exec-out screencap -p > /tmp/gioui-hang-repro.png if adb -s "$EMU" logcat -d | grep -q NO_INPUT_CHANNEL; then say "REPRODUCED: activity recreation left the window with NO_INPUT_CHANNEL (see /tmp/gioui-hang-repro.png)" exit 0 fi say "NOT reproduced: no NO_INPUT_CHANNEL after recreation (gioui handles re-attach correctly)" exit 1