From 45626d525462c80479764b0325c28fe5d414815a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:07:50 +0300 Subject: [PATCH 01/35] Force cold-boot emulator and rely on file-path installs --- .github/workflows/scripts-android.yml | 7 + .gitignore | 1 + scripts/build-android-app.sh | 1371 ++++++++++++++++- .../HelloCodenameOneUiTest.java.tmpl | 315 ++++ scripts/update_android_ui_test_gradle.py | 261 ++++ 5 files changed, 1947 insertions(+), 8 deletions(-) create mode 100644 scripts/templates/HelloCodenameOneUiTest.java.tmpl create mode 100755 scripts/update_android_ui_test_gradle.py diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index b1ba7def43..84a9bd1e10 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -23,3 +23,10 @@ jobs: run: ./scripts/build-android-port.sh -q -DskipTests - name: Build Hello Codename One Android app run: ./scripts/build-android-app.sh -q -DskipTests + - name: Upload UI test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: hello-codenameone-ui-test-artifacts + path: ${{ env.CN1_UI_TEST_ARTIFACT_DIR }} + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 0d12a7309f..f88f65279d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ **/.idea/* **/build/* **/dist/* +build-artifacts/ *.zip CodenameOneDesigner/src/version.properties /Ports/iOSPort/build/ diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index f424546247..c9daec9498 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -4,6 +4,16 @@ set -euo pipefail ba_log() { echo "[build-android-app] $1"; } +run_with_timeout() { + local duration="$1" + shift + if command -v timeout >/dev/null 2>&1; then + timeout "$duration" "$@" + else + "$@" + fi +} + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" @@ -265,21 +275,1366 @@ if [ -z "$GRADLE_PROJECT_DIR" ]; then exit 1 fi -ba_log "Invoking Gradle build in $GRADLE_PROJECT_DIR" +# --- Inject instrumentation UI test into Gradle project --- +APP_MODULE_DIR=$(find "$GRADLE_PROJECT_DIR" -maxdepth 1 -type d -name "app" | head -n 1 || true) +if [ -z "$APP_MODULE_DIR" ]; then + ba_log "Unable to locate Gradle app module inside $GRADLE_PROJECT_DIR" >&2 + exit 1 +fi + +UI_TEST_TEMPLATE="$SCRIPT_DIR/templates/HelloCodenameOneUiTest.java.tmpl" +if [ ! -f "$UI_TEST_TEMPLATE" ]; then + ba_log "UI test template not found: $UI_TEST_TEMPLATE" >&2 + exit 1 +fi + +UI_TEST_DIR="$APP_MODULE_DIR/src/androidTest/java/${PACKAGE_PATH}" +mkdir -p "$UI_TEST_DIR" +UI_TEST_FILE="$UI_TEST_DIR/${MAIN_NAME}UiTest.java" + +sed -e "s|@PACKAGE@|$PACKAGE_NAME|g" \ + -e "s|@MAIN_NAME@|$MAIN_NAME|g" \ + "$UI_TEST_TEMPLATE" > "$UI_TEST_FILE" +ba_log "Created instrumentation UI test at $UI_TEST_FILE" + +STUB_SRC_DIR="$APP_MODULE_DIR/src/main/java/${PACKAGE_PATH}" +mkdir -p "$STUB_SRC_DIR" +STUB_SRC_FILE="$STUB_SRC_DIR/${MAIN_NAME}Stub.java" +if [ ! -f "$STUB_SRC_FILE" ]; then + cat >"$STUB_SRC_FILE" <"$MANIFEST_FILE" <<'EOF_MANIFEST' + + + +EOF_MANIFEST + ba_log "Created minimal Android manifest at $MANIFEST_FILE" +fi + +grep -q '# \n#' "$MANIFEST_FILE" + +# Remove deprecated package attribute and inline declarations +perl -0777 -pe 's/\s+package="[^"]*"//; s#]*/>\s*##g' -i "$MANIFEST_FILE" + +# Ensure tools namespace for tools:node annotations +if ! grep -Fq 'xmlns:tools=' "$MANIFEST_FILE"; then + perl -0777 -pe 's#]*)>##' -i "$MANIFEST_FILE" +fi + +# Normalize existing stub declarations rather than inserting new ones +python3 - "$MANIFEST_FILE" "$FQCN" "$PACKAGE_NAME" "$MAIN_NAME" <<'PY' +import re +import sys +from pathlib import Path + +manifest_path, fqcn, package_name, main_name = sys.argv[1:5] +manifest = Path(manifest_path) +text = manifest.read_text() + +text = re.sub(r'.*?', '', text, flags=re.S) + +name_pattern = re.compile( + r'(android:name=")(?:(?:%s)|(?:\.?%sStub)|(?:%s\.%sStub))"' % ( + re.escape(fqcn), re.escape(main_name), re.escape(package_name), re.escape(main_name) + ) +) +text = name_pattern.sub(r'\1%s"' % fqcn, text) + +activity_pattern = re.compile( + r']*android:name="%s"[^>]*>(?:.*?)|]*android:name="%s"[^>]*/>' % ( + re.escape(fqcn), re.escape(fqcn) + ), + flags=re.S, +) + +seen = {"value": False} + +def replace_activity(match): + body = match.group(0) + if "tools:node=" in body: + body = re.sub(r'tools:node="[^"]*"', 'tools:node="replace"', body, count=1) + else: + close = body.find('>') + if close != -1: + body = body[:close] + ' tools:node="replace"' + body[close:] + if seen["value"]: + return '' + seen["value"] = True + return body + +text = activity_pattern.sub(replace_activity, text) + +if not seen["value"]: + raise SystemExit(f"Stub activity declaration not found in manifest: {manifest_path}") + +manifest.write_text(text) +PY + +STUB_DECL_COUNT=$(grep -c "android:name=\"$FQCN\"" "$MANIFEST_FILE" || true) +ba_log "Stub activity declarations present after normalization: $STUB_DECL_COUNT" +if [ "$STUB_DECL_COUNT" -ne 1 ]; then + ba_log "Expected exactly one stub activity declaration after normalization" >&2 + sed -n '1,160p' "$MANIFEST_FILE" | sed 's/^/[build-android-app] manifest: /' + exit 1 +fi + +if [ ! -f "$STUB_SRC_FILE" ]; then + ba_log "Missing stub activity source at $STUB_SRC_FILE" >&2 + exit 1 +fi + +APP_BUILD_GRADLE="$APP_MODULE_DIR/build.gradle" +if [ ! -f "$APP_BUILD_GRADLE" ]; then + ba_log "Expected Gradle build file not found at $APP_BUILD_GRADLE" >&2 + exit 1 +fi + +if ! grep -q "android[[:space:]]*{" "$APP_BUILD_GRADLE"; then + ba_log "Gradle build file at $APP_BUILD_GRADLE is missing an android { } block" >&2 + exit 1 +fi + +ensure_gradle_package_config() { + python3 - "$APP_BUILD_GRADLE" "$PACKAGE_NAME" <<'PY' +import sys +import re +from pathlib import Path + +path = Path(sys.argv[1]) +package_name = sys.argv[2] +text = path.read_text() +original = text +messages = [] + +def ensure_application_plugin(source: str) -> str: + plugin_id = "com.android.application" + if plugin_id in source: + return source + if "plugins" in source: + updated = re.sub(r"(plugins\s*\{)", r"\1\n id \"%s\"" % plugin_id, source, count=1) + if updated != source: + messages.append(f"Applied {plugin_id} via plugins block") + return updated + messages.append(f"Applied {plugin_id} via legacy apply plugin syntax") + return f"apply plugin: \"{plugin_id}\"\n" + source + +def ensure_android_block(source: str) -> str: + if re.search(r"android\s*\{", source): + return source + messages.append("Inserted android block") + return source + "\nandroid {\n}\n" + +def ensure_namespace(source: str) -> str: + pattern = re.compile(r"\bnamespace\s+[\"']([^\"']+)[\"']") + match = pattern.search(source) + if match: + if match.group(1) != package_name: + start, end = match.span() + source = source[:start] + f"namespace \"{package_name}\"" + source[end:] + messages.append(f"Updated namespace to {package_name}") + return source + android_match = re.search(r"android\s*\{", source) + if not android_match: + raise SystemExit("Unable to locate android block when inserting namespace") + insert = android_match.end() + source = source[:insert] + f"\n namespace \"{package_name}\"" + source[insert:] + messages.append(f"Inserted namespace {package_name}") + return source + +def ensure_default_config(source: str) -> str: + if re.search(r"defaultConfig\s*\{", source): + return source + android_match = re.search(r"android\s*\{", source) + if not android_match: + raise SystemExit("Unable to locate android block when creating defaultConfig") + insert = android_match.end() + snippet = "\n defaultConfig {\n }" + messages.append("Inserted defaultConfig block") + return source[:insert] + snippet + source[insert:] + +def ensure_application_id(source: str) -> str: + pattern = re.compile(r"\bapplicationId\s+[\"']([^\"']+)[\"']") + match = pattern.search(source) + if match: + if match.group(1) != package_name: + start, end = match.span() + indent = source[:start].split("\n")[-1].split("applicationId")[0] + source = source[:start] + f"{indent}applicationId \"{package_name}\"" + source[end:] + messages.append(f"Updated applicationId to {package_name}") + return source + default_match = re.search(r"defaultConfig\s*\{", source) + if not default_match: + raise SystemExit("Unable to locate defaultConfig when inserting applicationId") + insert = default_match.end() + source = source[:insert] + f"\n applicationId \"{package_name}\"" + source[insert:] + messages.append(f"Inserted applicationId {package_name}") + return source + +def ensure_compile_sdk(source: str) -> str: + pattern = re.compile(r"\bcompileSdk(?:Version)?\s+(\d+)") + match = pattern.search(source) + desired = "compileSdkVersion 35" + if match: + start, end = match.span() + current = match.group(0) + if current != desired: + source = source[:start] + desired + source[end:] + messages.append("Updated compileSdkVersion to 35") + return source + android_match = re.search(r"android\s*\{", source) + if not android_match: + raise SystemExit("Unable to locate android block when inserting compileSdkVersion") + insert = android_match.end() + source = source[:insert] + f"\n {desired}" + source[insert:] + messages.append("Inserted compileSdkVersion 35") + return source + +def ensure_default_config_value(source: str, key: str, value: str) -> str: + pattern = re.compile(rf"{key}\s+[\"']?([^\"'\s]+)[\"']?") + default_match = re.search(r"defaultConfig\s*\{", source) + if not default_match: + raise SystemExit(f"Unable to locate defaultConfig when inserting {key}") + block_start = default_match.end() + block_end = _find_matching_brace(source, default_match.start()) + block = source[block_start:block_end] + match = pattern.search(block) + replacement = f" {key} {value}" + if match: + if match.group(1) != value: + start = block_start + match.start() + end = block_start + match.end() + source = source[:start] + replacement + source[end:] + messages.append(f"Updated {key} to {value}") + return source + insert = block_start + source = source[:insert] + "\n" + replacement + source[insert:] + messages.append(f"Inserted {key} {value}") + return source + +def _find_matching_brace(source: str, start: int) -> int: + depth = 0 + for index in range(start, len(source)): + char = source[index] + if char == '{': + depth += 1 + elif char == '}': + depth -= 1 + if depth == 0: + return index + raise SystemExit("Failed to locate matching brace for defaultConfig block") + +def ensure_test_instrumentation_runner(source: str) -> str: + default_match = re.search(r"defaultConfig\s*\{", source) + if not default_match: + raise SystemExit("Unable to locate defaultConfig when inserting testInstrumentationRunner") + block_start = default_match.end() + block_end = _find_matching_brace(source, default_match.start()) + block = source[block_start:block_end] + pattern = re.compile(r"testInstrumentationRunner\s+[\"']([^\"']+)[\"']") + desired = "androidx.test.runner.AndroidJUnitRunner" + match = pattern.search(block) + replacement = f" testInstrumentationRunner \"{desired}\"" + if match: + if match.group(1) != desired: + start = block_start + match.start() + end = block_start + match.end() + source = source[:start] + replacement + source[end:] + messages.append("Updated testInstrumentationRunner to AndroidJUnitRunner") + return source + insert = block_start + source = source[:insert] + "\n" + replacement + source[insert:] + messages.append("Inserted testInstrumentationRunner AndroidJUnitRunner") + return source + +text = ensure_application_plugin(text) +text = ensure_android_block(text) +text = ensure_namespace(text) +text = ensure_default_config(text) +text = ensure_application_id(text) +text = ensure_compile_sdk(text) +text = ensure_default_config_value(text, "minSdkVersion", "19") +text = ensure_default_config_value(text, "targetSdkVersion", "35") +text = ensure_test_instrumentation_runner(text) + +if text != original: + path.write_text(text) + +for message in messages: + print(message) +PY +} + +if ! GRADLE_PACKAGE_LOG=$(ensure_gradle_package_config); then + ba_log "Failed to align namespace/applicationId with Codename One package" >&2 + exit 1 +fi +if [ -n "$GRADLE_PACKAGE_LOG" ]; then + while IFS= read -r line; do + [ -n "$line" ] && ba_log "$line" + done <<<"$GRADLE_PACKAGE_LOG" +fi + +ba_log "app/build.gradle head after package alignment:" +sed -n '1,80p' "$APP_BUILD_GRADLE" | sed 's/^/[build-android-app] app.gradle: /' + chmod +x "$GRADLE_PROJECT_DIR/gradlew" + +GRADLE_UPDATE_OUTPUT="$("$SCRIPT_DIR/update_android_ui_test_gradle.py" "$APP_BUILD_GRADLE")" +if [ -n "$GRADLE_UPDATE_OUTPUT" ]; then + while IFS= read -r line; do + [ -n "$line" ] && ba_log "$line" + done <<<"$GRADLE_UPDATE_OUTPUT" +fi + +ba_log "Dependencies block after instrumentation update:" +awk '/^\s*dependencies\s*\{/{flag=1} flag{print} /^\s*\}/{if(flag){exit}}' "$APP_BUILD_GRADLE" \ + | sed 's/^/[build-android-app] | /' + +# Final manifest sanity before Gradle preflight +if [ -f "$MANIFEST_FILE" ]; then + tmp_manifest_pruned="$(mktemp)" + FQCN="$FQCN" perl -0777 -pe ' + my $fq = quotemeta($ENV{FQCN}); + my $seen = 0; + s{ + (]*android:name="$fq"[^>]*/>\s*) + | + (]*android:name="$fq"[^>]*>.*?\s*) + }{ + $seen++ ? "" : $& + }gsxe; + ' "$MANIFEST_FILE" >"$tmp_manifest_pruned" + mv "$tmp_manifest_pruned" "$MANIFEST_FILE" + STUB_COUNT=$(grep -c "android:name=\"$FQCN\"" "$MANIFEST_FILE" || true) + ba_log "Stub declarations in manifest after pruning: $STUB_COUNT" + ba_log "Dumping manifest contents prior to preflight" + nl -ba "$MANIFEST_FILE" | sed 's/^/[build-android-app] manifest: /' + grep -n "android:name=\"$FQCN\"" "$MANIFEST_FILE" | sed 's/^/[build-android-app] manifest-match: /' || true +fi + +ba_log "Validating manifest merge before assemble" +if ! ( + cd "$GRADLE_PROJECT_DIR" && + JAVA_HOME="$JAVA17_HOME" PATH="$JAVA17_HOME/bin:$PATH" ./gradlew --no-daemon :app:processDebugMainManifest +); then + ba_log ":app:processDebugMainManifest failed during preflight" >&2 + dump_manifest_merger_reports + exit 1 +fi + +FINAL_ARTIFACT_DIR="${CN1_TEST_SCREENSHOT_EXPORT_DIR:-$REPO_ROOT/build-artifacts}" +mkdir -p "$FINAL_ARTIFACT_DIR" +if [ -n "${GITHUB_ENV:-}" ]; then + printf 'CN1_UI_TEST_ARTIFACT_DIR=%s\n' "$FINAL_ARTIFACT_DIR" >> "$GITHUB_ENV" +fi + +ba_log "Invoking Gradle build in $GRADLE_PROJECT_DIR" ORIGINAL_JAVA_HOME="$JAVA_HOME" export JAVA_HOME="$JAVA17_HOME" +export PATH="$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH" + +SDKMANAGER_BIN="" +if [ -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then + SDKMANAGER_BIN="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" +elif [ -x "$ANDROID_SDK_ROOT/cmdline-tools/bin/sdkmanager" ]; then + SDKMANAGER_BIN="$ANDROID_SDK_ROOT/cmdline-tools/bin/sdkmanager" +elif command -v sdkmanager >/dev/null 2>&1; then + SDKMANAGER_BIN="$(command -v sdkmanager)" +fi + +AVDMANAGER_BIN="" +if [ -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" ]; then + AVDMANAGER_BIN="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/avdmanager" +elif [ -x "$ANDROID_SDK_ROOT/cmdline-tools/bin/avdmanager" ]; then + AVDMANAGER_BIN="$ANDROID_SDK_ROOT/cmdline-tools/bin/avdmanager" +elif command -v avdmanager >/dev/null 2>&1; then + AVDMANAGER_BIN="$(command -v avdmanager)" +fi + +install_android_packages() { + local manager="$1" + if [ -z "$manager" ]; then + ba_log "sdkmanager not available; cannot install system images" >&2 + exit 1 + fi + yes | "$manager" --licenses >/dev/null 2>&1 || true + "$manager" --install \ + "platform-tools" \ + "emulator" \ + "platforms;android-35" \ + "system-images;android-35;google_apis;x86_64" >/dev/null 2>&1 || true +} + +create_avd() { + local manager="$1" + local name="$2" + local image="$3" + local avd_dir="$4" + if [ -z "$manager" ]; then + ba_log "avdmanager not available; cannot create emulator" >&2 + exit 1 + fi + mkdir -p "$avd_dir" + local ini_file="$avd_dir/$name.ini" + local image_dir="$avd_dir/$name.avd" + if [ -f "$ini_file" ] && [ -d "$image_dir" ]; then + if grep -F -q "$image" "$ini_file" 2>/dev/null; then + ba_log "Reusing existing Android Virtual Device $name" + configure_avd "$avd_dir" "$name" + return + fi + ba_log "Existing Android Virtual Device $name uses a different system image; recreating" + rm -f "$ini_file" + rm -rf "$image_dir" + fi + if ! ANDROID_AVD_HOME="$avd_dir" "$manager" create avd -n "$name" -k "$image" --device "2.7in QVGA" --force >/dev/null <<<'no' + then + ba_log "Failed to create Android Virtual Device $name using image $image" >&2 + find "$avd_dir" -maxdepth 2 -mindepth 1 -print | sed 's/^/[build-android-app] AVD: /' >&2 || true + exit 1 + fi + if [ ! -f "$ini_file" ]; then + ba_log "AVD $name was created but configuration file $ini_file is missing" >&2 + find "$avd_dir" -maxdepth 1 -mindepth 1 -print | sed 's/^/[build-android-app] AVD: /' >&2 || true + exit 1 + fi + configure_avd "$avd_dir" "$name" +} + +configure_avd() { + local avd_dir="$1" + local name="$2" + local cfg="$avd_dir/$name.avd/config.ini" + if [ ! -f "$cfg" ]; then + return + fi + declare -A settings=( + ["hw.ramSize"]=4096 + ["disk.dataPartition.size"]=8192M + ["fastboot.forceColdBoot"]=yes + ["hw.bluetooth"]=no + ["hw.camera.back"]=none + ["hw.camera.front"]=none + ["hw.audioInput"]=no + ["hw.audioOutput"]=no + ) + local key value + for key in "${!settings[@]}"; do + value="${settings[$key]}" + if grep -q "^${key}=" "$cfg" 2>/dev/null; then + sed -i "s/^${key}=.*/${key}=${value}/" "$cfg" + else + echo "${key}=${value}" >>"$cfg" + fi + done +} + +wait_for_emulator() { + local serial="$1" + "$ADB_BIN" start-server >/dev/null + "$ADB_BIN" -s "$serial" wait-for-device + + local boot_timeout="${EMULATOR_BOOT_TIMEOUT_SECONDS:-900}" + if ! [[ "$boot_timeout" =~ ^[0-9]+$ ]] || [ "$boot_timeout" -le 0 ]; then + ba_log "Invalid EMULATOR_BOOT_TIMEOUT_SECONDS=$boot_timeout provided; falling back to 900" + boot_timeout=900 + fi + local poll_interval="${EMULATOR_BOOT_POLL_INTERVAL_SECONDS:-5}" + if ! [[ "$poll_interval" =~ ^[0-9]+$ ]] || [ "$poll_interval" -le 0 ]; then + poll_interval=5 + fi + local status_log_interval="${EMULATOR_BOOT_STATUS_LOG_INTERVAL_SECONDS:-30}" + if ! [[ "$status_log_interval" =~ ^[0-9]+$ ]] || [ "$status_log_interval" -le 0 ]; then + status_log_interval=30 + fi + + local deadline=$((SECONDS + boot_timeout)) + local last_log=$SECONDS + local boot_completed="0" + local dev_boot_completed="0" + local bootanim="" + local bootanim_exit="" + local device_state="" + local boot_ready=0 + + while [ $SECONDS -lt $deadline ]; do + device_state="$($ADB_BIN -s "$serial" get-state 2>/dev/null | tr -d '\r')" + if [ "$device_state" != "device" ]; then + if [ $((SECONDS - last_log)) -ge $status_log_interval ]; then + ba_log "Waiting for emulator $serial to become ready (state=$device_state)" + last_log=$SECONDS + fi + sleep "$poll_interval" + continue + fi + + boot_completed="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + dev_boot_completed="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" + bootanim="$($ADB_BIN -s "$serial" shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r')" + bootanim_exit="$($ADB_BIN -s "$serial" shell getprop service.bootanim.exit 2>/dev/null | tr -d '\r')" + + if { [ "$boot_completed" = "1" ] || [ "$boot_completed" = "true" ]; } \ + && { [ -z "$dev_boot_completed" ] || [ "$dev_boot_completed" = "1" ] || [ "$dev_boot_completed" = "true" ]; }; then + boot_ready=1 + break + fi + + if [ "$bootanim" = "stopped" ] || [ "$bootanim_exit" = "1" ]; then + boot_ready=2 + break + fi + + if [ $((SECONDS - last_log)) -ge $status_log_interval ]; then + ba_log "Waiting for emulator $serial to boot (sys.boot_completed=${boot_completed:-} dev.bootcomplete=${dev_boot_completed:-} bootanim=${bootanim:-} bootanim_exit=${bootanim_exit:-})" + last_log=$SECONDS + fi + sleep "$poll_interval" + done + + if [ $boot_ready -eq 0 ]; then + ba_log "Emulator $serial failed to boot within ${boot_timeout}s (sys.boot_completed=${boot_completed:-} dev.bootcomplete=${dev_boot_completed:-} bootanim=${bootanim:-} bootanim_exit=${bootanim_exit:-} state=${device_state:-})" >&2 + return 1 + elif [ $boot_ready -eq 2 ]; then + ba_log "Emulator $serial reported boot animation stopped; proceeding without bootcomplete properties" + fi + + "$ADB_BIN" -s "$serial" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true + "$ADB_BIN" -s "$serial" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true + "$ADB_BIN" -s "$serial" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true + "$ADB_BIN" -s "$serial" shell input keyevent 82 >/dev/null 2>&1 || true + "$ADB_BIN" -s "$serial" shell wm dismiss-keyguard >/dev/null 2>&1 || true + return 0 +} + +wait_for_package_service() { + local serial="$1" + local timeout="${PACKAGE_SERVICE_TIMEOUT_SECONDS:-${PACKAGE_SERVICE_TIMEOUT:-600}}" + local per_try="${PACKAGE_SERVICE_PER_TRY_TIMEOUT_SECONDS:-${PACKAGE_SERVICE_PER_TRY_TIMEOUT:-5}}" + if ! [[ "$timeout" =~ ^[0-9]+$ ]] || [ "$timeout" -le 0 ]; then + timeout=600 + fi + if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then + per_try=5 + fi + + local deadline=$((SECONDS + timeout)) + local last_log=$SECONDS + + while [ $SECONDS -lt $deadline ]; do + local boot_ok ce_ok + boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + ce_ok="$($ADB_BIN -s "$serial" shell getprop sys.user.0.ce_available 2>/dev/null | tr -d '\r')" + + if timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd package path android >/dev/null 2>&1 \ + || timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1 \ + || timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd package list packages >/dev/null 2>&1 \ + || timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm list packages >/dev/null 2>&1 \ + || timeout "$per_try" "$ADB_BIN" -s "$serial" shell dumpsys package >/dev/null 2>&1; then + return 0 + fi + + if [ $((SECONDS - last_log)) -ge 10 ]; then + ba_log "Waiting for package manager service on $serial (boot_ok=${boot_ok:-?} ce_ok=${ce_ok:-?})" + last_log=$SECONDS + fi + sleep 2 + done + + ba_log "Package manager service not ready on $serial after ${timeout}s" >&2 + return 1 +} + +wait_for_api_level() { + local serial="$1" + local timeout="${API_LEVEL_TIMEOUT_SECONDS:-600}" + local per_try="${API_LEVEL_PER_TRY_TIMEOUT_SECONDS:-5}" + if ! [[ "$timeout" =~ ^[0-9]+$ ]] || [ "$timeout" -le 0 ]; then + timeout=600 + fi + if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then + per_try=5 + fi + + local deadline=$((SECONDS + timeout)) + local last_log=$SECONDS + local sdk="" + + while [ $SECONDS -lt $deadline ]; do + if sdk="$(timeout "$per_try" "$ADB_BIN" -s "$serial" shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\r' | tr -d '\n')"; then + if [[ "$sdk" =~ ^[0-9]+$ ]]; then + ba_log "Device API level is $sdk" + return 0 + fi + fi + if [ $((SECONDS - last_log)) -ge 10 ]; then + ba_log "Waiting for ro.build.version.sdk on $serial" + last_log=$SECONDS + fi + sleep 2 + done + + ba_log "ro.build.version.sdk not available after ${timeout}s" >&2 + return 1 +} + +framework_try_ready() { + local serial="$1" + local per_try="$2" + local phase_timeout="${FRAMEWORK_READY_PHASE_TIMEOUT_SECONDS:-180}" + local log_interval="${FRAMEWORK_READY_STATUS_LOG_INTERVAL_SECONDS:-10}" + + if ! [[ "$phase_timeout" =~ ^[0-9]+$ ]] || [ "$phase_timeout" -le 0 ]; then + phase_timeout=180 + fi + if ! [[ "$log_interval" =~ ^[0-9]+$ ]] || [ "$log_interval" -le 0 ]; then + log_interval=10 + fi + + local deadline=$((SECONDS + phase_timeout)) + local last_log=$SECONDS + local boot_ok="" ce_ok="" + + while [ $SECONDS -lt $deadline ]; do + boot_ok="" + ce_ok="" + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server >/dev/null 2>&1; then + boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + ce_ok="$($ADB_BIN -s "$serial" shell getprop sys.user.0.ce_available 2>/dev/null | tr -d '\r')" + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1 \ + && run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then + return 0 + fi + fi + + if [ $((SECONDS - last_log)) -ge $log_interval ]; then + ba_log "Waiting for Android framework on $serial (system_server=$([ -n "$boot_ok" ] && echo up || echo down) boot_ok=${boot_ok:-?} ce_ok=${ce_ok:-?})" + last_log=$SECONDS + fi + sleep 2 + done + + return 1 +} + +ensure_framework_ready() { + local serial="$1" + "$ADB_BIN" -s "$serial" wait-for-device >/dev/null 2>&1 || return 1 + + local per_try="${FRAMEWORK_READY_PER_TRY_TIMEOUT_SECONDS:-5}" + if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then + per_try=5 + fi + + if framework_try_ready "$serial" "$per_try"; then + return 0 + fi + + ba_log "Framework not ready on $serial. Attempting framework restart (stop/start)…" + "$ADB_BIN" -s "$serial" shell stop >/dev/null 2>&1 || true + sleep 2 + "$ADB_BIN" -s "$serial" shell start >/dev/null 2>&1 || true + + if framework_try_ready "$serial" "$per_try"; then + return 0 + fi + + ba_log "Framework still unavailable on $serial. Rebooting device…" + "$ADB_BIN" -s "$serial" reboot >/dev/null 2>&1 || return 1 + "$ADB_BIN" -s "$serial" wait-for-device >/dev/null 2>&1 || return 1 + + if framework_try_ready "$serial" "$per_try"; then + return 0 + fi + + ba_log "ERROR: Android framework/package manager not available on $serial" >&2 + return 1 +} + +dump_emulator_diagnostics() { + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell getprop | sed 's/^/[build-android-app] getprop: /' || true + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell logcat -d -t 2000 \ + | grep -v -E 'com\\.android\\.bluetooth|BtGd|bluetooth' \ + | tail -n 200 | sed 's/^/[build-android-app] logcat: /' || true +} + +log_instrumentation_state() { + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path android | sed 's/^/[build-android-app] pm path android: /' || true + + local runtime_pkg="${RUNTIME_PACKAGE:-$PACKAGE_NAME}" + local test_pkg="${TEST_RUNTIME_PACKAGE:-${runtime_pkg}.test}" + + local instrumentation_list + instrumentation_list="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list instrumentation 2>/dev/null || true)" + if [ -n "$instrumentation_list" ]; then + printf '%s\n' "$instrumentation_list" | sed 's/^/[build-android-app] instrumentation: /' + else + ba_log "No instrumentation targets reported on $EMULATOR_SERIAL before installation" + fi + + local have_test_apk=0 + if [ -n "$test_pkg" ] && "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path "$test_pkg" >/dev/null 2>&1; then + have_test_apk=1 + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path "$test_pkg" \ + | sed 's/^/[build-android-app] test-apk: /' + else + ba_log "Test APK for $test_pkg not yet installed on $EMULATOR_SERIAL" + fi + + local package_regex package_list package_matches + package_regex="${runtime_pkg//./\.}" + package_list="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list packages 2>/dev/null || true)" + if [ -n "$package_list" ]; then + package_matches="$(printf '%s\n' "$package_list" | grep -E "${package_regex}|${package_regex}\.test" || true)" + if [ -n "$package_matches" ]; then + printf '%s\n' "$package_matches" | sed 's/^/[build-android-app] package: /' + else + ba_log "Packages matching $runtime_pkg not yet installed on $EMULATOR_SERIAL" + fi + else + ba_log "Package manager returned no packages on $EMULATOR_SERIAL" + fi +} + +collect_instrumentation_crash() { + local attempt="$1" + local crash_log="$FINAL_ARTIFACT_DIR/ui-test-crash-attempt-$attempt.log" + local latest_log="$FINAL_ARTIFACT_DIR/ui-test-crash.log" + local log_tmp filtered_tmp tomb_tmp tombstones + + log_tmp="$(mktemp "${TMPDIR:-/tmp}/ui-test-logcat.XXXXXX")" + filtered_tmp="$(mktemp "${TMPDIR:-/tmp}/ui-test-logcat-filtered.XXXXXX")" + tomb_tmp="$(mktemp "${TMPDIR:-/tmp}/ui-test-tombstones.XXXXXX")" + + : >"$crash_log" + + if "$ADB_BIN" -s "$EMULATOR_SERIAL" shell logcat -d -t 2000 >"$log_tmp" 2>/dev/null; then + if grep -E "FATAL EXCEPTION|AndroidRuntime|Process: ${RUNTIME_PACKAGE}(\\.test)?|Abort message:" "$log_tmp" >"$filtered_tmp" 2>/dev/null; then + if [ -s "$filtered_tmp" ]; then + while IFS= read -r line; do + echo "[build-android-app] crash: $line" + done <"$filtered_tmp" + cat "$filtered_tmp" >>"$crash_log" + fi + fi + fi + + tombstones="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell ls /data/tombstones 2>/dev/null | tail -n 3 || true)" + if [ -n "$tombstones" ]; then + printf '%s\n' "$tombstones" | sed 's/^/[build-android-app] tombstone: /' + printf '%s\n' "$tombstones" >"$tomb_tmp" + cat "$tomb_tmp" >>"$crash_log" + fi + + if [ -s "$crash_log" ]; then + cp "$crash_log" "$latest_log" 2>/dev/null || true + else + rm -f "$crash_log" + fi + + rm -f "$log_tmp" "$filtered_tmp" "$tomb_tmp" +} + +stop_emulator() { + if [ -n "${EMULATOR_SERIAL:-}" ]; then + "$ADB_BIN" -s "$EMULATOR_SERIAL" emu kill >/dev/null 2>&1 || true + fi + if [ -n "${EMULATOR_PID:-}" ]; then + kill "$EMULATOR_PID" >/dev/null 2>&1 || true + wait "$EMULATOR_PID" 2>/dev/null || true + fi +} + +install_android_packages "$SDKMANAGER_BIN" + +ADB_BIN="$ANDROID_SDK_ROOT/platform-tools/adb" +if [ ! -x "$ADB_BIN" ]; then + if command -v adb >/dev/null 2>&1; then + ADB_BIN="$(command -v adb)" + else + ba_log "adb not found in Android SDK. Ensure platform-tools are installed." >&2 + exit 1 + fi +fi + +EMULATOR_BIN="$ANDROID_SDK_ROOT/emulator/emulator" +if [ ! -x "$EMULATOR_BIN" ]; then + if command -v emulator >/dev/null 2>&1; then + EMULATOR_BIN="$(command -v emulator)" + else + ba_log "Android emulator binary not found" >&2 + exit 1 + fi +fi + +AVD_NAME="cn1UiTestAvd" +SYSTEM_IMAGE="system-images;android-35;google_apis;x86_64" +AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-${RUNNER_TEMP:-$HOME}/cn1-android-avd}" +mkdir -p "$AVD_CACHE_ROOT" +AVD_HOME="$AVD_CACHE_ROOT" +ba_log "Using AVD home at $AVD_HOME" +create_avd "$AVDMANAGER_BIN" "$AVD_NAME" "$SYSTEM_IMAGE" "$AVD_HOME" + +ANDROID_AVD_HOME="$AVD_HOME" "$ADB_BIN" start-server >/dev/null + +mapfile -t EXISTING_EMULATORS < <("$ADB_BIN" devices | awk '/^emulator-/{print $1}') + +EMULATOR_PORT="${EMULATOR_PORT:-5560}" +if ! [[ "$EMULATOR_PORT" =~ ^[0-9]+$ ]]; then + EMULATOR_PORT=5560 +elif [ $((EMULATOR_PORT % 2)) -ne 0 ] || [ $EMULATOR_PORT -lt 5554 ] || [ $EMULATOR_PORT -gt 5584 ]; then + # emulator requires an even console port between 5554-5584; fall back if invalid + EMULATOR_PORT=5560 +fi +EMULATOR_SERIAL="emulator-$EMULATOR_PORT" + +EMULATOR_LOG="$GRADLE_PROJECT_DIR/emulator.log" +ba_log "Starting headless Android emulator $AVD_NAME on port $EMULATOR_PORT" +ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_PORT" \ + -no-window -no-snapshot -no-snapshot-load -no-snapshot-save -wipe-data \ + -gpu swiftshader_indirect -no-audio -no-boot-anim \ + -accel off -no-accel -camera-back none -camera-front none -skip-adb-auth \ + -feature -Vulkan -netfast -memory 4096 >"$EMULATOR_LOG" 2>&1 & +EMULATOR_PID=$! +trap stop_emulator EXIT + +sleep 5 + +detect_emulator_serial() { + local deadline current_devices serial existing + deadline=$((SECONDS + 180)) + while [ $SECONDS -lt $deadline ]; do + mapfile -t current_devices < <("$ADB_BIN" devices | awk '/^emulator-/{print $1}') + for serial in "${current_devices[@]}"; do + for existing in "${EXISTING_EMULATORS[@]}"; do + if [ "$serial" = "$existing" ]; then + # already present before launch; ignore unless it matches requested serial + if [ "$serial" = "$EMULATOR_SERIAL" ]; then + EMULATOR_SERIAL="$serial" + return 0 + fi + serial="" + break + fi + done + if [ -n "$serial" ]; then + EMULATOR_SERIAL="$serial" + return 0 + fi + done + sleep 2 + done + return 1 +} + +if ! detect_emulator_serial; then + mapfile -t CURRENT_EMULATORS < <("$ADB_BIN" devices | awk '/^emulator-/{print $1}') + if [ -z "${EMULATOR_SERIAL:-}" ] && [ ${#CURRENT_EMULATORS[@]} -gt 0 ]; then + EMULATOR_SERIAL="${CURRENT_EMULATORS[0]}" + fi + if [ -z "${EMULATOR_SERIAL:-}" ] || ! printf '%s\n' "${CURRENT_EMULATORS[@]}" | grep -Fxq "$EMULATOR_SERIAL"; then + ba_log "Failed to detect emulator serial after launch" >&2 + if [ -f "$EMULATOR_LOG" ]; then + ba_log "Emulator log tail:" >&2 + tail -n 40 "$EMULATOR_LOG" | sed 's/^/[build-android-app] | /' >&2 + fi + stop_emulator + exit 1 + fi +fi +ba_log "Using emulator serial $EMULATOR_SERIAL" + +if ! wait_for_emulator "$EMULATOR_SERIAL"; then + stop_emulator + exit 1 +fi + +if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pidof system_server >/dev/null 2>&1; then + ba_log "system_server not running after boot; restarting framework" + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell stop >/dev/null 2>&1 || true + sleep 2 + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell start >/dev/null 2>&1 || true +fi + +if ! ensure_framework_ready "$EMULATOR_SERIAL"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +POST_BOOT_GRACE="${EMULATOR_POST_BOOT_GRACE_SECONDS:-20}" +if ! [[ "$POST_BOOT_GRACE" =~ ^[0-9]+$ ]] || [ "$POST_BOOT_GRACE" -lt 0 ]; then + POST_BOOT_GRACE=20 +fi +if [ "$POST_BOOT_GRACE" -gt 0 ]; then + ba_log "Waiting ${POST_BOOT_GRACE}s for emulator system services to stabilize" + sleep "$POST_BOOT_GRACE" +fi + +if ! wait_for_package_service "$EMULATOR_SERIAL"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true + +if ! wait_for_api_level "$EMULATOR_SERIAL"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path android | sed 's/^/[build-android-app] pm path android: /' || true + +"$ADB_BIN" kill-server >/dev/null 2>&1 || true +"$ADB_BIN" start-server >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" wait-for-device +export ANDROID_SERIAL="$EMULATOR_SERIAL" + +ASSEMBLE_TIMEOUT_SECONDS="${ASSEMBLE_TIMEOUT_SECONDS:-900}" +if ! [[ "$ASSEMBLE_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || [ "$ASSEMBLE_TIMEOUT_SECONDS" -le 0 ]; then + ASSEMBLE_TIMEOUT_SECONDS=900 +fi + +GRADLE_ASSEMBLE_CMD=( + "./gradlew" + "--no-daemon" + ":app:assembleDebug" + ":app:assembleDebugAndroidTest" + "-x" + "lint" + "-x" + "test" +) +if command -v timeout >/dev/null 2>&1; then + ba_log "Building app and androidTest APKs with external timeout of ${ASSEMBLE_TIMEOUT_SECONDS}s" + GRADLE_ASSEMBLE_CMD=("timeout" "$ASSEMBLE_TIMEOUT_SECONDS" "${GRADLE_ASSEMBLE_CMD[@]}") +else + ba_log "timeout command not found; running Gradle assemble tasks without external watchdog" +fi + +GRADLE_ASSEMBLE_LOG="$GRADLE_PROJECT_DIR/gradle-ui-assemble.log" +set +e ( cd "$GRADLE_PROJECT_DIR" - if command -v sdkmanager >/dev/null 2>&1; then - yes | sdkmanager --licenses >/dev/null 2>&1 || true - elif [ -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then - yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --licenses >/dev/null 2>&1 || true - fi - ./gradlew --no-daemon assembleDebug + "${GRADLE_ASSEMBLE_CMD[@]}" | tee "$GRADLE_ASSEMBLE_LOG" + exit "${PIPESTATUS[0]}" ) +ASSEMBLE_EXIT_CODE=$? +set -e + +if [ -f "$GRADLE_ASSEMBLE_LOG" ]; then + cp "$GRADLE_ASSEMBLE_LOG" "$FINAL_ARTIFACT_DIR/ui-test-assemble.log" + ba_log "Gradle assemble log saved to $FINAL_ARTIFACT_DIR/ui-test-assemble.log" +fi + +if [ "$ASSEMBLE_EXIT_CODE" -ne 0 ]; then + ba_log "Gradle assemble tasks exited with status $ASSEMBLE_EXIT_CODE" + dump_manifest_merger_reports + stop_emulator + exit 1 +fi + +adb_install_file_path() { + local serial="$1" apk="$2" + local remote_tmp="/data/local/tmp/$(basename "$apk")" + + if ! ensure_framework_ready "$serial"; then + return 1 + fi + + "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + if ! "$ADB_BIN" -s "$serial" push "$apk" "$remote_tmp" >/dev/null 2>&1; then + ba_log "Failed to push $(basename "$apk") to $remote_tmp" >&2 + return 1 + fi + + if "$ADB_BIN" -s "$serial" shell pm install -r -t -g "$remote_tmp"; then + "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + return 0 + fi + + "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + return 1 +} + +ba_log "Inspecting Gradle application identifiers" + +APP_PROPERTIES_RAW=$(cd "$GRADLE_PROJECT_DIR" && ./gradlew -q :app:properties 2>/dev/null || true) +if [ -n "$APP_PROPERTIES_RAW" ]; then + set +o pipefail + MATCHED_PROPS=$(printf '%s\n' "$APP_PROPERTIES_RAW" | grep -E '^(applicationId|testApplicationId|namespace):' || true) + set -o pipefail + if [ -n "$MATCHED_PROPS" ]; then + printf '%s\n' "$MATCHED_PROPS" | sed 's/^/[build-android-app] props: /' + fi +fi + +APP_ID="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^applicationId:/{print $2; found=1} END{if(!found) print ""}' || true)" +NS_VALUE="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^namespace:/{print $2; found=1} END{if(!found) print ""}' || true)" + +if [ -z "$APP_ID" ] || [ "$APP_ID" != "$PACKAGE_NAME" ] || [ -z "$NS_VALUE" ] || [ "$NS_VALUE" != "$PACKAGE_NAME" ]; then + ba_log "applicationId/namespace mismatch (applicationId='${APP_ID:-}' namespace='${NS_VALUE:-}'), patching" + if ! GRADLE_PACKAGE_LOG=$(ensure_gradle_package_config); then + ba_log "Failed to align namespace/applicationId with Codename One package" >&2 + stop_emulator + exit 1 + fi + if [ -n "$GRADLE_PACKAGE_LOG" ]; then + while IFS= read -r line; do + [ -n "$line" ] && ba_log "$line" + done <<<"$GRADLE_PACKAGE_LOG" + fi + APP_PROPERTIES_RAW=$(cd "$GRADLE_PROJECT_DIR" && ./gradlew -q :app:properties 2>/dev/null || true) + if [ -n "$APP_PROPERTIES_RAW" ]; then + set +o pipefail + MATCHED_PROPS=$(printf '%s\n' "$APP_PROPERTIES_RAW" | grep -E '^(applicationId|testApplicationId|namespace):' || true) + set -o pipefail + if [ -n "$MATCHED_PROPS" ]; then + printf '%s\n' "$MATCHED_PROPS" | sed 's/^/[build-android-app] props: /' + fi + fi + APP_ID="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^applicationId:/{print $2; found=1} END{if(!found) print ""}' || true)" + NS_VALUE="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^namespace:/{print $2; found=1} END{if(!found) print ""}' || true)" + if [ -z "$APP_ID" ] || [ "$APP_ID" != "$PACKAGE_NAME" ] || [ -z "$NS_VALUE" ] || [ "$NS_VALUE" != "$PACKAGE_NAME" ]; then + ba_log "WARNING: applicationId/namespace remain misaligned (applicationId='${APP_ID:-}' namespace='${NS_VALUE:-}'); continuing with APK inspection" >&2 + fi +fi + +MERGED_MANIFEST="$APP_MODULE_DIR/build/intermediates/packaged_manifests/debug/AndroidManifest.xml" +if [ -f "$MERGED_MANIFEST" ]; then + if grep -Fq "android:name=\"$FQCN\"" "$MERGED_MANIFEST"; then + grep -Fn "android:name=\"$FQCN\"" "$MERGED_MANIFEST" | sed 's/^/[build-android-app] merged-manifest: /' + else + ba_log "ERROR: merged manifest missing $FQCN" + sed -n '1,160p' "$MERGED_MANIFEST" | sed 's/^/[build-android-app] merged: /' + stop_emulator + exit 1 + fi +else + ba_log "WARN: merged manifest not found at $MERGED_MANIFEST" +fi + +if [ -z "$APP_PROPERTIES_RAW" ]; then + ba_log "Warning: unable to query :app:properties" >&2 +fi + +APP_APK="$(find "$GRADLE_PROJECT_DIR/app/build/outputs/apk/debug" -maxdepth 1 -name '*-debug.apk' | head -n1 || true)" +TEST_APK="$(find "$GRADLE_PROJECT_DIR/app/build/outputs/apk/androidTest/debug" -maxdepth 1 -name '*-debug-androidTest.apk' | head -n1 || true)" + +if [ -z "$APP_APK" ] || [ ! -f "$APP_APK" ]; then + ba_log "App APK not found after identifier patch assemble" >&2 + stop_emulator + exit 1 +fi +if [ -z "$TEST_APK" ] || [ ! -f "$TEST_APK" ]; then + ba_log "androidTest APK not found after identifier patch assemble" >&2 + stop_emulator + exit 1 +fi + +AAPT_BIN="" +if [ -d "$ANDROID_SDK_ROOT/build-tools" ]; then + while IFS= read -r dir; do + if [ -x "$dir/aapt" ]; then + AAPT_BIN="$dir/aapt" + break + fi + done < <(find "$ANDROID_SDK_ROOT/build-tools" -maxdepth 1 -mindepth 1 -type d | sort -Vr) +fi + +RUNTIME_PACKAGE="$PACKAGE_NAME" +if [ -n "$AAPT_BIN" ] && [ -x "$AAPT_BIN" ]; then + APK_PACKAGE="$($AAPT_BIN dump badging "$APP_APK" 2>/dev/null | awk -F"'" '/^package: name=/{print $2; exit}')" + if [ -n "$APK_PACKAGE" ]; then + ba_log "aapt reported application package: $APK_PACKAGE" + if [ "$APK_PACKAGE" != "$PACKAGE_NAME" ]; then + ba_log "WARNING: APK package ($APK_PACKAGE) differs from Codename One package ($PACKAGE_NAME)" >&2 + fi + RUNTIME_PACKAGE="$APK_PACKAGE" + else + ba_log "WARN: Unable to extract application package using $AAPT_BIN" >&2 + fi +else + ba_log "WARN: aapt binary not found under $ANDROID_SDK_ROOT/build-tools; skipping APK package verification" >&2 +fi + +TEST_RUNTIME_PACKAGE="${RUNTIME_PACKAGE}.test" +RUNTIME_STUB_FQCN="${RUNTIME_PACKAGE}.${MAIN_NAME}Stub" + +ba_log "Preparing device for APK installation" +if ! ensure_framework_ready "$EMULATOR_SERIAL"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +if "$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package list sessions >/dev/null 2>&1; then + SESSION_IDS="$($ADB_BIN -s "$EMULATOR_SERIAL" shell cmd package list sessions 2>/dev/null | awk '{print $1}' | sed 's/sessionId=//g' || true)" + if [ -n "$SESSION_IDS" ]; then + while IFS= read -r sid; do + [ -z "$sid" ] && continue + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package abort-session "$sid" >/dev/null 2>&1 || true + done <<<"$SESSION_IDS" + fi +fi + +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm uninstall "$RUNTIME_PACKAGE" >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm uninstall "$TEST_RUNTIME_PACKAGE" >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package bg-dexopt-job >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell rm -f /data/local/tmp/*.apk >/dev/null 2>&1 || true + +ba_log "Installing app APK: $APP_APK" +if ! adb_install_file_path "$EMULATOR_SERIAL" "$APP_APK"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +ba_log "Installing androidTest APK: $TEST_APK" +if ! adb_install_file_path "$EMULATOR_SERIAL" "$TEST_APK"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +if ! ensure_framework_ready "$EMULATOR_SERIAL"; then + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list instrumentation | sed "s/^/[build-android-app] instrumentation: /" || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list packages | grep -E "${RUNTIME_PACKAGE//./\.}|${RUNTIME_PACKAGE//./\.}\\.test" | sed "s/^/[build-android-app] package: /" || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package resolve-activity --brief "$RUNTIME_PACKAGE/$RUNTIME_STUB_FQCN" | sed "s/^/[build-android-app] resolve-stub (pre-test): /" || true + +APP_PACKAGE_PATH="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path "$RUNTIME_PACKAGE" 2>/dev/null | tr -d '\r' || true)" +if [ -n "$APP_PACKAGE_PATH" ]; then + printf '%s\n' "$APP_PACKAGE_PATH" | sed 's/^/[build-android-app] app-apk: /' +else + ba_log "App package $RUNTIME_PACKAGE not yet reported by pm path on $EMULATOR_SERIAL" +fi + +TEST_PACKAGE_PATH="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path "$TEST_RUNTIME_PACKAGE" 2>/dev/null | tr -d '\r' || true)" +if [ -n "$TEST_PACKAGE_PATH" ]; then + printf '%s\n' "$TEST_PACKAGE_PATH" | sed 's/^/[build-android-app] test-apk: /' +else + ba_log "Test package $TEST_RUNTIME_PACKAGE not yet reported by pm path on $EMULATOR_SERIAL" +fi + +log_instrumentation_state + +RUNNER="$( + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list instrumentation \ + | tr -d '\r' \ + | grep -F "(target=$RUNTIME_PACKAGE)" \ + | head -n1 \ + | sed -E 's/^instrumentation:([^ ]+).*/\1/' +)" +if [ -z "$RUNNER" ]; then + ba_log "No instrumentation runner found for $RUNTIME_PACKAGE" + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list instrumentation | sed 's/^/[build-android-app] instrumentation: /' + dump_emulator_diagnostics + stop_emulator + exit 1 +fi +ba_log "Using instrumentation runner: $RUNNER" + +ba_log "Inspecting launcher activities for $PACKAGE_NAME" +LAUNCH_RESOLVE_OUTPUT="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package resolve-activity --brief "$PACKAGE_NAME" 2>&1 || true)" +if [ -n "$LAUNCH_RESOLVE_OUTPUT" ]; then + printf '%s\n' "$LAUNCH_RESOLVE_OUTPUT" | sed 's/^/[build-android-app] resolve-launch: /' +fi +STUB_ACTIVITY_FQCN="$RUNTIME_PACKAGE/$RUNTIME_STUB_FQCN" +STUB_RESOLVE_OUTPUT="$( + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package resolve-activity --brief "$STUB_ACTIVITY_FQCN" 2>&1 || true + )" +if [ -n "$STUB_RESOLVE_OUTPUT" ]; then + printf '%s\n' "$STUB_RESOLVE_OUTPUT" | sed 's/^/[build-android-app] resolve-stub: /' +else + ba_log "Unable to resolve stub activity $STUB_ACTIVITY_FQCN on device" +fi +if [[ "$STUB_RESOLVE_OUTPUT" == *"No activity found"* ]] || [ -z "$STUB_RESOLVE_OUTPUT" ]; then + ba_log "Stub activity $STUB_ACTIVITY_FQCN is not resolvable on the device" + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am force-stop "$RUNTIME_PACKAGE" >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am force-stop "$TEST_RUNTIME_PACKAGE" >/dev/null 2>&1 || true + +UI_TEST_TIMEOUT_SECONDS="${UI_TEST_TIMEOUT_SECONDS:-900}" +if ! [[ "$UI_TEST_TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || [ "$UI_TEST_TIMEOUT_SECONDS" -le 0 ]; then + ba_log "Invalid UI_TEST_TIMEOUT_SECONDS=$UI_TEST_TIMEOUT_SECONDS provided; falling back to 900" + UI_TEST_TIMEOUT_SECONDS=900 +fi + +INSTRUMENT_EXIT_CODE=1 + +run_instrumentation() { + local tries=3 delay=15 attempt exit_code=1 + local args=(-w -r -e clearPackageData true -e log true -e class "${PACKAGE_NAME}.${MAIN_NAME}UiTest") + for attempt in $(seq 1 "$tries"); do + local cmd=("$ADB_BIN" "-s" "$EMULATOR_SERIAL" "shell" "am" "instrument" "${args[@]}" "$RUNNER") + if command -v timeout >/dev/null 2>&1; then + cmd=("timeout" "$UI_TEST_TIMEOUT_SECONDS" "${cmd[@]}") + ba_log "Instrumentation attempt $attempt/$tries with external timeout of ${UI_TEST_TIMEOUT_SECONDS}s" + else + ba_log "Instrumentation attempt $attempt/$tries without external watchdog" + fi + local attempt_log="$FINAL_ARTIFACT_DIR/ui-test-instrumentation-attempt-$attempt.log" + "$ADB_BIN" -s "$EMULATOR_SERIAL" logcat -c >/dev/null 2>&1 || true + set +e + "${cmd[@]}" | tee "$attempt_log" + exit_code=${PIPESTATUS[0]} + set -e + cp "$attempt_log" "$FINAL_ARTIFACT_DIR/ui-test-instrumentation.log" 2>/dev/null || true + if [ "$exit_code" -ne 0 ]; then + collect_instrumentation_crash "$attempt" + fi + if [ "$exit_code" -eq 0 ]; then + INSTRUMENT_EXIT_CODE=0 + return 0 + fi + if grep -q "INSTRUMENTATION_ABORTED: System has crashed." "$attempt_log" 2>/dev/null && [ "$attempt" -lt "$tries" ]; then + ba_log "System crashed during instrumentation attempt $attempt; retrying after ${delay}s" + sleep "$delay" + continue + fi + INSTRUMENT_EXIT_CODE=$exit_code + return $exit_code + done + INSTRUMENT_EXIT_CODE=$exit_code + return $exit_code +} + +if ! run_instrumentation; then + ba_log "Instrumentation command exited with status $INSTRUMENT_EXIT_CODE" + dump_emulator_diagnostics +fi + +copy_device_file() { + local src="$1" + local dest="$2" + if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell run-as "$RUNTIME_PACKAGE" ls "$src" >/dev/null 2>&1; then + return 1 + fi + if "$ADB_BIN" -s "$EMULATOR_SERIAL" exec-out run-as "$RUNTIME_PACKAGE" cat "$src" >"$dest"; then + return 0 + fi + rm -f "$dest" + return 1 +} + +SCREENSHOT_STATUS=0 +ANDROID_SCREENSHOT="" +CODENAMEONE_SCREENSHOT="" +DEFAULT_SCREENSHOT="" + +SCREENSHOT_DIR_ON_DEVICE="files/ui-test-screenshots" +ANDROID_SCREENSHOT_NAME="${MAIN_NAME}-android-ui.png" +CODENAMEONE_SCREENSHOT_NAME="${MAIN_NAME}-codenameone-ui.png" + +ANDROID_SCREENSHOT_PATH_DEVICE="$SCREENSHOT_DIR_ON_DEVICE/$ANDROID_SCREENSHOT_NAME" +CODENAMEONE_SCREENSHOT_PATH_DEVICE="$SCREENSHOT_DIR_ON_DEVICE/$CODENAMEONE_SCREENSHOT_NAME" + +ANDROID_SCREENSHOT_DEST="$FINAL_ARTIFACT_DIR/$ANDROID_SCREENSHOT_NAME" +CODENAMEONE_SCREENSHOT_DEST="$FINAL_ARTIFACT_DIR/$CODENAMEONE_SCREENSHOT_NAME" + +if copy_device_file "$ANDROID_SCREENSHOT_PATH_DEVICE" "$ANDROID_SCREENSHOT_DEST"; then + ba_log "Android screenshot copied to $ANDROID_SCREENSHOT_DEST" + ANDROID_SCREENSHOT="$ANDROID_SCREENSHOT_DEST" + DEFAULT_SCREENSHOT="$ANDROID_SCREENSHOT_DEST" +else + ba_log "Android screenshot not found at $ANDROID_SCREENSHOT_PATH_DEVICE" >&2 + SCREENSHOT_STATUS=1 +fi + +if copy_device_file "$CODENAMEONE_SCREENSHOT_PATH_DEVICE" "$CODENAMEONE_SCREENSHOT_DEST"; then + ba_log "Codename One screenshot copied to $CODENAMEONE_SCREENSHOT_DEST" + CODENAMEONE_SCREENSHOT="$CODENAMEONE_SCREENSHOT_DEST" + if [ -z "$DEFAULT_SCREENSHOT" ]; then + DEFAULT_SCREENSHOT="$CODENAMEONE_SCREENSHOT_DEST" + fi +else + ba_log "Codename One screenshot not found at $CODENAMEONE_SCREENSHOT_PATH_DEVICE" >&2 + SCREENSHOT_STATUS=1 +fi + +if [ -f "$EMULATOR_LOG" ]; then + cp "$EMULATOR_LOG" "$FINAL_ARTIFACT_DIR/emulator.log" || true +fi + +if [ -n "${GITHUB_ENV:-}" ]; then + if [ -n "$DEFAULT_SCREENSHOT" ]; then + printf 'CN1_UI_TEST_SCREENSHOT=%s\n' "$DEFAULT_SCREENSHOT" >> "$GITHUB_ENV" + fi + if [ -n "$ANDROID_SCREENSHOT" ]; then + printf 'CN1_UI_TEST_ANDROID_SCREENSHOT=%s\n' "$ANDROID_SCREENSHOT" >> "$GITHUB_ENV" + fi + if [ -n "$CODENAMEONE_SCREENSHOT" ]; then + printf 'CN1_UI_TEST_CODENAMEONE_SCREENSHOT=%s\n' "$CODENAMEONE_SCREENSHOT" >> "$GITHUB_ENV" + fi +fi + export JAVA_HOME="$ORIGINAL_JAVA_HOME" +stop_emulator +trap - EXIT + +if [ "$INSTRUMENT_EXIT_CODE" -ne 0 ]; then + exit "$INSTRUMENT_EXIT_CODE" +fi + +if [ "$SCREENSHOT_STATUS" -ne 0 ]; then + exit 1 +fi + APK_PATH=$(find "$GRADLE_PROJECT_DIR" -path "*/outputs/apk/debug/*.apk" | head -n 1 || true) [ -n "$APK_PATH" ] || { ba_log "Gradle build completed but no APK was found" >&2; exit 1; } -ba_log "Successfully built Android APK at $APK_PATH" \ No newline at end of file +ba_log "Successfully built Android APK at $APK_PATH" diff --git a/scripts/templates/HelloCodenameOneUiTest.java.tmpl b/scripts/templates/HelloCodenameOneUiTest.java.tmpl new file mode 100644 index 0000000000..032eb0ed62 --- /dev/null +++ b/scripts/templates/HelloCodenameOneUiTest.java.tmpl @@ -0,0 +1,315 @@ +package @PACKAGE@; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.view.PixelCopy; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import androidx.annotation.Nullable; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.codename1.ui.Display; +import com.codename1.ui.Form; +import com.codename1.ui.Image; +import com.codename1.ui.util.ImageIO; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(AndroidJUnit4.class) +public class @MAIN_NAME@UiTest { + + private static final long STARTUP_TIMEOUT_MS = 30_000L; + private static final long RENDER_TIMEOUT_MS = 15_000L; + private static final long EDT_TIMEOUT_MS = 10_000L; + + @Rule + public final ActivityScenarioRule<@MAIN_NAME@Stub> scenarioRule = + new ActivityScenarioRule<>(@MAIN_NAME@Stub.class); + + @Test + public void mainFormScreenshotContainsRenderedContent() throws Exception { + ActivityScenario<@MAIN_NAME@Stub> scenario = scenarioRule.getScenario(); + assertNotNull("ActivityScenario should be initialized", scenario); + + Form form = waitForMainForm(); + assertNotNull("Codename One main form should be available", form); + + Bitmap androidScreenshot = waitForAndroidScreenshot(scenario); + assertTrue("Android screenshot should show rendered content", hasRenderableContent(androidScreenshot)); + saveBitmap(androidScreenshot, "@MAIN_NAME@-android-ui.png"); + + Image codenameOneScreenshot = waitForCodenameOneScreenshot(); + assertTrue("Codename One screenshot should show rendered content", hasRenderableContent(codenameOneScreenshot)); + saveCodenameOneScreenshot(codenameOneScreenshot, "@MAIN_NAME@-codenameone-ui.png"); + } + + private Form waitForMainForm() throws Exception { + long deadline = SystemClock.uptimeMillis() + STARTUP_TIMEOUT_MS; + while (SystemClock.uptimeMillis() < deadline) { + if (Display.isInitialized()) { + Form current = callSeriallyAndWait(() -> Display.getInstance().getCurrent()); + if (current != null) { + return current; + } + } + SystemClock.sleep(50L); + } + throw new AssertionError("Timed out waiting for Codename One form to appear"); + } + + private Bitmap waitForAndroidScreenshot(ActivityScenario scenario) throws Exception { + AtomicReference lastCapture = new AtomicReference<>(); + long deadline = SystemClock.uptimeMillis() + RENDER_TIMEOUT_MS; + while (SystemClock.uptimeMillis() < deadline) { + scenario.onActivity(activity -> { + try { + Bitmap capture = captureAndroidScreenshot(activity); + if (capture != null) { + lastCapture.set(capture); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + Bitmap candidate = lastCapture.get(); + if (candidate != null && hasRenderableContent(candidate)) { + return candidate; + } + SystemClock.sleep(100L); + } + Bitmap fallback = lastCapture.get(); + if (fallback != null) { + return fallback; + } + throw new AssertionError("Timed out waiting for Android screenshot to contain rendered content"); + } + + private static Bitmap captureAndroidScreenshot(Activity activity) throws Exception { + Window window = activity.getWindow(); + View decor = window.getDecorView(); + ensureMeasured(decor); + + Bitmap viaPixelCopy = pixelCopy(window); + if (viaPixelCopy != null && hasRenderableContent(viaPixelCopy)) { + return viaPixelCopy; + } + + Bitmap viaTexture = captureTextureView(decor); + if (viaTexture != null && hasRenderableContent(viaTexture)) { + return viaTexture; + } + + return drawToBitmap(decor); + } + + private static void ensureMeasured(View view) { + if (view.getWidth() > 0 && view.getHeight() > 0) { + return; + } + int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + view.measure(widthSpec, heightSpec); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } + + @Nullable + private static Bitmap pixelCopy(Window window) throws InterruptedException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return null; + } + View decor = window.getDecorView(); + if (decor.getWidth() <= 0 || decor.getHeight() <= 0) { + return null; + } + Bitmap bitmap = Bitmap.createBitmap(decor.getWidth(), decor.getHeight(), Bitmap.Config.ARGB_8888); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(PixelCopy.ERROR_SOURCE_INVALID); + PixelCopy.request(window, bitmap, copyResult -> { + result.set(copyResult); + latch.countDown(); + }, new Handler(Looper.getMainLooper())); + if (latch.await(EDT_TIMEOUT_MS, TimeUnit.MILLISECONDS) && result.get() == PixelCopy.SUCCESS) { + return bitmap; + } + return null; + } + + @Nullable + private static Bitmap captureTextureView(View view) { + if (view instanceof TextureView) { + Bitmap bitmap = ((TextureView) view).getBitmap(); + if (bitmap != null) { + return bitmap; + } + } + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + Bitmap child = captureTextureView(group.getChildAt(i)); + if (child != null) { + return child; + } + } + } + return null; + } + + private static Bitmap drawToBitmap(View view) { + int width = Math.max(1, view.getWidth()); + int height = Math.max(1, view.getHeight()); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + return bitmap; + } + + private Image waitForCodenameOneScreenshot() throws Exception { + Image lastCapture = null; + long deadline = SystemClock.uptimeMillis() + RENDER_TIMEOUT_MS; + while (SystemClock.uptimeMillis() < deadline) { + Image capture = captureCodenameOneScreenshot(); + if (capture != null) { + lastCapture = capture; + if (hasRenderableContent(capture)) { + return capture; + } + } + SystemClock.sleep(100L); + } + if (lastCapture != null) { + return lastCapture; + } + throw new AssertionError("Timed out waiting for Codename One screenshot to contain rendered content"); + } + + @Nullable + private Image captureCodenameOneScreenshot() throws Exception { + if (!Display.isInitialized()) { + return null; + } + return callSeriallyAndWait(() -> Display.getInstance().captureScreen()); + } + + private static boolean hasRenderableContent(Bitmap bitmap) { + if (bitmap == null || bitmap.getWidth() <= 1 || bitmap.getHeight() <= 1) { + return false; + } + int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + return hasRenderablePixels(pixels); + } + + private static boolean hasRenderableContent(Image image) throws Exception { + if (image == null || image.getWidth() <= 1 || image.getHeight() <= 1) { + return false; + } + int[] pixels = callSeriallyAndWait(image::getRGB); + return hasRenderablePixels(pixels); + } + + private static boolean hasRenderablePixels(int[] pixels) { + if (pixels == null || pixels.length == 0) { + return false; + } + int reference = pixels[0]; + int differing = 0; + for (int argb : pixels) { + int alpha = (argb >>> 24) & 0xFF; + if (alpha == 0) { + continue; + } + if (argb != reference) { + differing++; + if (differing > pixels.length / 100) { + return true; + } + } + } + return false; + } + + private static File saveBitmap(Bitmap bitmap, String fileName) throws IOException { + File dir = resolveArtifactDirectory(); + File destination = new File(dir, fileName); + try (FileOutputStream out = new FileOutputStream(destination)) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + } + return destination; + } + + private static File saveCodenameOneScreenshot(Image screenshot, String fileName) throws Exception { + File dir = resolveArtifactDirectory(); + File destination = new File(dir, fileName); + ImageIO imageIO = callSeriallyAndWait(() -> Display.getInstance().getImageIO()); + assertNotNull("Codename One ImageIO should be available", imageIO); + try (FileOutputStream out = new FileOutputStream(destination)) { + final FileOutputStream target = out; + callSeriallyAndWait(() -> { + imageIO.save(screenshot, target, ImageIO.FORMAT_PNG, 1.0f); + return null; + }); + } + return destination; + } + + private static File resolveArtifactDirectory() throws IOException { + File filesDir = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getFilesDir(); + File output = new File(filesDir, "ui-test-screenshots"); + if (!output.exists() && !output.mkdirs()) { + throw new IOException("Failed to create " + output.getAbsolutePath()); + } + return output; + } + + private static T callSeriallyAndWait(Callable task) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(); + AtomicReference error = new AtomicReference<>(); + Display.getInstance().callSerially(() -> { + try { + result.set(task.call()); + } catch (Throwable t) { + error.set(t); + } finally { + latch.countDown(); + } + }); + if (!latch.await(EDT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new AssertionError("Timed out waiting for Codename One EDT"); + } + if (error.get() != null) { + Throwable throwable = error.get(); + if (throwable instanceof Exception) { + throw (Exception) throwable; + } + throw new RuntimeException(throwable); + } + return result.get(); + } +} diff --git a/scripts/update_android_ui_test_gradle.py b/scripts/update_android_ui_test_gradle.py new file mode 100755 index 0000000000..4e732c23f0 --- /dev/null +++ b/scripts/update_android_ui_test_gradle.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Apply minimal instrumentation test configuration to the generated Gradle project.""" +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +COMPILE_SDK_LINE = " compileSdkVersion 35\n" + +TEST_OPTIONS_SNIPPET = """ testOptions {\n animationsDisabled = true\n }\n\n""" + +DEFAULT_CONFIG_SNIPPET = """ defaultConfig {\n testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n }\n\n""" + +ANDROID_TEST_DEPENDENCIES = ( + "androidx.test:core:1.5.0", + "androidx.test.ext:junit:1.1.5", + "androidx.test:rules:1.5.0", + "androidx.test:runner:1.5.2", + "androidx.test.espresso:espresso-core:3.5.1", + "androidx.test.uiautomator:uiautomator:2.2.0", +) + + +class GradleFile: + def __init__(self, content: str) -> None: + self.content = content + self.configuration_used: str | None = None + self.added_dependencies: list[str] = [] + + def _iter_blocks(self): + content = self.content + length = len(content) + stack: list[tuple[str, int]] = [] + i = 0 + while i < length: + if content.startswith("//", i): + end = content.find("\n", i) + if end == -1: + break + i = end + 1 + continue + if content.startswith("/*", i): + end = content.find("*/", i + 2) + if end == -1: + break + i = end + 2 + continue + char = content[i] + if char in ('"', "'"): + quote = char + i += 1 + while i < length: + if content[i] == "\\": + i += 2 + continue + if content[i] == quote: + i += 1 + break + i += 1 + continue + if char == '}': + if stack: + name, start = stack.pop() + parents = [entry[0] for entry in stack] + yield name, start, i + 1, parents + i += 1 + continue + match = re.match(r'[A-Za-z_][A-Za-z0-9_\.]*', content[i:]) + if match: + name = match.group(0) + j = i + len(name) + while j < length and content[j].isspace(): + j += 1 + if j < length and content[j] == '{': + stack.append((name, i)) + i = j + 1 + continue + i += 1 + + def _find_block(self, name: str, *, parent: str | None = None) -> tuple[int, int] | None: + for block_name, start, end, parents in self._iter_blocks(): + if block_name != name: + continue + if parent is None: + if not parents: + return start, end + else: + if parents and parents[-1] == parent: + return start, end + return None + + def ensure_compile_sdk(self) -> None: + if re.search(r"compileSdkVersion\s+\d+", self.content): + return + android_block = self._find_block("android") + if not android_block: + raise SystemExit("Unable to locate android block in Gradle file") + insert = self.content.find('\n', android_block[0]) + 1 + if insert <= 0: + insert = android_block[0] + len("android {") + self.content = self.content[:insert] + COMPILE_SDK_LINE + self.content[insert:] + + def ensure_test_options(self) -> None: + if "animationsDisabled" in self.content: + return + test_block = self._find_block("testOptions", parent="android") + if test_block: + insert = self.content.find('\n', test_block[0]) + 1 + body = " animationsDisabled = true\n" + self.content = ( + self.content[:insert] + body + self.content[insert:test_block[1]] + self.content[test_block[1]:] + ) + return + android_block = self._find_block("android") + if not android_block: + raise SystemExit("Unable to locate android block in Gradle file") + insert = self.content.find('\n', android_block[0]) + 1 + if insert <= 0: + insert = android_block[0] + len("android {") + self.content = self.content[:insert] + TEST_OPTIONS_SNIPPET + self.content[insert:] + + def ensure_instrumentation_runner(self) -> None: + if "testInstrumentationRunner" in self.content: + return + default_block = self._find_block("defaultConfig", parent="android") + if default_block: + insert = self.content.find('\n', default_block[0]) + 1 + line = " testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n" + self.content = ( + self.content[:insert] + line + self.content[insert:default_block[1]] + self.content[default_block[1]:] + ) + return + android_block = self._find_block("android") + if not android_block: + raise SystemExit("Unable to locate android block in Gradle file") + insert = self.content.find('\n', android_block[0]) + 1 + if insert <= 0: + insert = android_block[0] + len("android {") + self.content = self.content[:insert] + DEFAULT_CONFIG_SNIPPET + self.content[insert:] + + def ensure_dependencies(self) -> None: + block = self._find_dependencies_block() + if not block: + self._append_dependencies_block() + block = self._find_dependencies_block() + if not block: + raise SystemExit("Unable to locate dependencies block in Gradle file after insertion") + existing_block = self.content[block[0]:block[1]] + configuration = self._select_configuration(existing_block, self.content) + self.configuration_used = configuration + insertion_point = block[1] - 1 + for coordinate in ANDROID_TEST_DEPENDENCIES: + if self._has_dependency(existing_block, coordinate): + continue + combined = f" {configuration} \"{coordinate}\"\n" + self.content = ( + self.content[:insertion_point] + + combined + + self.content[insertion_point:] + ) + insertion_point += len(combined) + existing_block += combined + self.added_dependencies.append(coordinate) + + @staticmethod + def _has_dependency(block: str, coordinate: str) -> bool: + escaped = re.escape(coordinate) + return bool( + re.search(rf"androidTestImplementation\s+['\"]{escaped}['\"]", block) + or re.search(rf"androidTestCompile\s+['\"]{escaped}['\"]", block) + ) + + def _find_dependencies_block(self) -> tuple[int, int] | None: + preferred: tuple[int, int] | None = None + fallback: tuple[int, int] | None = None + for name, start, end, parents in self._iter_blocks(): + if name != "dependencies": + continue + if parents and parents[-1] in {"buildscript", "pluginManagement"}: + continue + block_content = self.content[start:end] + if re.search(r"^\s*(implementation|api|compile|androidTestImplementation|androidTestCompile)\b", block_content, re.MULTILINE): + preferred = (start, end) + break + if "classpath" in block_content and not re.search(r"^\s*(implementation|api|compile)\b", block_content, re.MULTILINE): + continue + if fallback is None: + fallback = (start, end) + return preferred or fallback + + def _append_dependencies_block(self) -> None: + if not self.content.endswith("\n"): + self.content += "\n" + self.content += "\ndependencies {\n}\n" + + @staticmethod + def _select_configuration(block: str, content: str) -> str: + if re.search(r"^\s*androidTestImplementation\b", block, re.MULTILINE): + return "androidTestImplementation" + if re.search(r"^\s*androidTestCompile\b", block, re.MULTILINE): + return "androidTestCompile" + if re.search(r"^\s*androidTestImplementation\b", content, re.MULTILINE): + return "androidTestImplementation" + if re.search(r"^\s*androidTestCompile\b", content, re.MULTILINE): + return "androidTestCompile" + if re.search(r"^\s*implementation\b", block, re.MULTILINE) or re.search( + r"^\s*implementation\b", content, re.MULTILINE + ): + return "androidTestImplementation" + if re.search(r"^\s*compile\b", block, re.MULTILINE) or re.search( + r"^\s*compile\b", content, re.MULTILINE + ): + return "androidTestCompile" + return "androidTestImplementation" + + def apply(self) -> None: + self.ensure_compile_sdk() + self.ensure_test_options() + self.ensure_instrumentation_runner() + self.ensure_dependencies() + + def summary(self) -> str: + configuration = self.configuration_used or "" + if self.added_dependencies: + deps = ", ".join(self.added_dependencies) + else: + deps = "none (already present)" + return ( + "Instrumentation dependency configuration: " + f"{configuration}; added dependencies: {deps}" + ) + + +def process(path: pathlib.Path) -> None: + content = path.read_text(encoding="utf-8") + if "android {" not in content: + raise SystemExit( + "Selected Gradle file doesn't contain an android { } block. Check module path." + ) + editor = GradleFile(content) + editor.apply() + path.write_text(editor.content, encoding="utf-8") + print(editor.summary()) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("gradle_file", type=pathlib.Path) + args = parser.parse_args(argv) + + if not args.gradle_file.is_file(): + parser.error(f"Gradle file not found: {args.gradle_file}") + + process(args.gradle_file) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From f2356e463080b5206f704c8172794abd7478b4a7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:45:25 +0300 Subject: [PATCH 02/35] Harden emulator installs before running instrumentation --- scripts/build-android-app.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index c9daec9498..64b5541c9a 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1293,13 +1293,19 @@ adb_install_file_path() { return 1 fi + local install_status=1 if "$ADB_BIN" -s "$serial" shell pm install -r -t -g "$remote_tmp"; then - "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true - return 0 + install_status=0 + else + local apk_size + apk_size=$(stat -c%s "$apk" 2>/dev/null || wc -c <"$apk") + if [ -n "$apk_size" ] && "$ADB_BIN" -s "$serial" shell "cat '$remote_tmp' | pm install -r -t -g -S $apk_size"; then + install_status=0 + fi fi "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true - return 1 + return $install_status } ba_log "Inspecting Gradle application identifiers" From 37c0edd8d389fb1727aa2087e01004ee436ca2bf Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:20:52 +0300 Subject: [PATCH 03/35] Harden framework readiness and Gradle ID alignment --- scripts/build-android-app.sh | 118 ++++++++++++++++------------------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 64b5541c9a..355bac2292 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -514,24 +514,23 @@ def ensure_application_id(source: str) -> str: def ensure_compile_sdk(source: str) -> str: pattern = re.compile(r"\bcompileSdk(?:Version)?\s+(\d+)") match = pattern.search(source) - desired = "compileSdkVersion 35" + desired = "compileSdk 35" if match: start, end = match.span() - current = match.group(0) - if current != desired: + if match.group(0) != desired: source = source[:start] + desired + source[end:] - messages.append("Updated compileSdkVersion to 35") + messages.append("Updated compileSdk to 35") return source android_match = re.search(r"android\s*\{", source) if not android_match: - raise SystemExit("Unable to locate android block when inserting compileSdkVersion") + raise SystemExit("Unable to locate android block when inserting compileSdk") insert = android_match.end() source = source[:insert] + f"\n {desired}" + source[insert:] - messages.append("Inserted compileSdkVersion 35") + messages.append("Inserted compileSdk 35") return source -def ensure_default_config_value(source: str, key: str, value: str) -> str: - pattern = re.compile(rf"{key}\s+[\"']?([^\"'\s]+)[\"']?") +def ensure_default_config_value(source: str, key: str, value: str, *, quoted: bool = False) -> str: + pattern = re.compile(rf"{key}(?:Version)?\s+[\"']?([^\"'\s]+)[\"']?") default_match = re.search(r"defaultConfig\s*\{", source) if not default_match: raise SystemExit(f"Unable to locate defaultConfig when inserting {key}") @@ -539,9 +538,10 @@ def ensure_default_config_value(source: str, key: str, value: str) -> str: block_end = _find_matching_brace(source, default_match.start()) block = source[block_start:block_end] match = pattern.search(block) - replacement = f" {key} {value}" + replacement_value = f'"{value}"' if quoted else value + replacement = f" {key} {replacement_value}" if match: - if match.group(1) != value: + if match.group(1) != value or "Version" in match.group(0): start = block_start + match.start() end = block_start + match.end() source = source[:start] + replacement + source[end:] @@ -593,8 +593,10 @@ text = ensure_namespace(text) text = ensure_default_config(text) text = ensure_application_id(text) text = ensure_compile_sdk(text) -text = ensure_default_config_value(text, "minSdkVersion", "19") -text = ensure_default_config_value(text, "targetSdkVersion", "35") +text = ensure_default_config_value(text, "minSdk", "19") +text = ensure_default_config_value(text, "targetSdk", "35") +text = ensure_default_config_value(text, "versionCode", "100") +text = ensure_default_config_value(text, "versionName", "1.0", quoted=True) text = ensure_test_instrumentation_runner(text) if text != original: @@ -918,37 +920,45 @@ wait_for_api_level() { return 1 } -framework_try_ready() { +adb_framework_ready_once() { local serial="$1" local per_try="$2" - local phase_timeout="${FRAMEWORK_READY_PHASE_TIMEOUT_SECONDS:-180}" + local phase_timeout="$3" local log_interval="${FRAMEWORK_READY_STATUS_LOG_INTERVAL_SECONDS:-10}" if ! [[ "$phase_timeout" =~ ^[0-9]+$ ]] || [ "$phase_timeout" -le 0 ]; then phase_timeout=180 fi + if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then + per_try=5 + fi if ! [[ "$log_interval" =~ ^[0-9]+$ ]] || [ "$log_interval" -le 0 ]; then log_interval=10 fi local deadline=$((SECONDS + phase_timeout)) local last_log=$SECONDS - local boot_ok="" ce_ok="" while [ $SECONDS -lt $deadline ]; do - boot_ok="" - ce_ok="" - if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server >/dev/null 2>&1; then - boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" - ce_ok="$($ADB_BIN -s "$serial" shell getprop sys.user.0.ce_available 2>/dev/null | tr -d '\r')" - if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1 \ - && run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then - return 0 - fi + local boot_ok system_pid pm_ok activity_ok + boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" + pm_ok=0 + activity_ok=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1; then + pm_ok=1 + fi + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then + activity_ok=1 + fi + + if [ "$boot_ok" = "1" ] && [ -n "$system_pid" ] && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ]; then + ba_log "Android framework ready on $serial (system_server=$system_pid)" + return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=$([ -n "$boot_ok" ] && echo up || echo down) boot_ok=${boot_ok:-?} ce_ok=${ce_ok:-?})" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?} pm_ready=$pm_ok activity_ready=$activity_ok)" last_log=$SECONDS fi sleep 2 @@ -957,33 +967,30 @@ framework_try_ready() { return 1 } -ensure_framework_ready() { +adb_wait_framework_ready() { local serial="$1" "$ADB_BIN" -s "$serial" wait-for-device >/dev/null 2>&1 || return 1 local per_try="${FRAMEWORK_READY_PER_TRY_TIMEOUT_SECONDS:-5}" - if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then - per_try=5 - fi - if framework_try_ready "$serial" "$per_try"; then + if adb_framework_ready_once "$serial" "$per_try" "${FRAMEWORK_READY_PRIMARY_TIMEOUT_SECONDS:-180}"; then return 0 fi - ba_log "Framework not ready on $serial. Attempting framework restart (stop/start)…" + ba_log "Framework not ready on $serial; restarting system services" "$ADB_BIN" -s "$serial" shell stop >/dev/null 2>&1 || true sleep 2 "$ADB_BIN" -s "$serial" shell start >/dev/null 2>&1 || true - if framework_try_ready "$serial" "$per_try"; then + if adb_framework_ready_once "$serial" "$per_try" "${FRAMEWORK_READY_RESTART_TIMEOUT_SECONDS:-120}"; then return 0 fi - ba_log "Framework still unavailable on $serial. Rebooting device…" + ba_log "Framework still unavailable on $serial; rebooting device" "$ADB_BIN" -s "$serial" reboot >/dev/null 2>&1 || return 1 "$ADB_BIN" -s "$serial" wait-for-device >/dev/null 2>&1 || return 1 - if framework_try_ready "$serial" "$per_try"; then + if adb_framework_ready_once "$serial" "$per_try" "${FRAMEWORK_READY_REBOOT_TIMEOUT_SECONDS:-180}"; then return 0 fi @@ -1196,7 +1203,7 @@ if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pidof system_server >/dev/null 2>&1; "$ADB_BIN" -s "$EMULATOR_SERIAL" shell start >/dev/null 2>&1 || true fi -if ! ensure_framework_ready "$EMULATOR_SERIAL"; then +if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then dump_emulator_diagnostics stop_emulator exit 1 @@ -1283,7 +1290,7 @@ adb_install_file_path() { local serial="$1" apk="$2" local remote_tmp="/data/local/tmp/$(basename "$apk")" - if ! ensure_framework_ready "$serial"; then + if ! adb_wait_framework_ready "$serial"; then return 1 fi @@ -1323,32 +1330,15 @@ fi APP_ID="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^applicationId:/{print $2; found=1} END{if(!found) print ""}' || true)" NS_VALUE="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^namespace:/{print $2; found=1} END{if(!found) print ""}' || true)" -if [ -z "$APP_ID" ] || [ "$APP_ID" != "$PACKAGE_NAME" ] || [ -z "$NS_VALUE" ] || [ "$NS_VALUE" != "$PACKAGE_NAME" ]; then - ba_log "applicationId/namespace mismatch (applicationId='${APP_ID:-}' namespace='${NS_VALUE:-}'), patching" - if ! GRADLE_PACKAGE_LOG=$(ensure_gradle_package_config); then - ba_log "Failed to align namespace/applicationId with Codename One package" >&2 - stop_emulator - exit 1 - fi - if [ -n "$GRADLE_PACKAGE_LOG" ]; then - while IFS= read -r line; do - [ -n "$line" ] && ba_log "$line" - done <<<"$GRADLE_PACKAGE_LOG" - fi - APP_PROPERTIES_RAW=$(cd "$GRADLE_PROJECT_DIR" && ./gradlew -q :app:properties 2>/dev/null || true) - if [ -n "$APP_PROPERTIES_RAW" ]; then - set +o pipefail - MATCHED_PROPS=$(printf '%s\n' "$APP_PROPERTIES_RAW" | grep -E '^(applicationId|testApplicationId|namespace):' || true) - set -o pipefail - if [ -n "$MATCHED_PROPS" ]; then - printf '%s\n' "$MATCHED_PROPS" | sed 's/^/[build-android-app] props: /' - fi - fi - APP_ID="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^applicationId:/{print $2; found=1} END{if(!found) print ""}' || true)" - NS_VALUE="$(printf '%s\n' "$APP_PROPERTIES_RAW" | awk -F': ' '/^namespace:/{print $2; found=1} END{if(!found) print ""}' || true)" - if [ -z "$APP_ID" ] || [ "$APP_ID" != "$PACKAGE_NAME" ] || [ -z "$NS_VALUE" ] || [ "$NS_VALUE" != "$PACKAGE_NAME" ]; then - ba_log "WARNING: applicationId/namespace remain misaligned (applicationId='${APP_ID:-}' namespace='${NS_VALUE:-}'); continuing with APK inspection" >&2 - fi +if [ -z "$APP_ID" ]; then + ba_log "Gradle did not report applicationId; relying on APK metadata" +elif [ "$APP_ID" != "$PACKAGE_NAME" ]; then + ba_log "WARNING: Gradle applicationId '$APP_ID' differs from Codename One package '$PACKAGE_NAME'" >&2 +fi +if [ -z "$NS_VALUE" ]; then + ba_log "Gradle did not report namespace; relying on APK metadata" +elif [ "$NS_VALUE" != "$PACKAGE_NAME" ]; then + ba_log "WARNING: Gradle namespace '$NS_VALUE' differs from Codename One package '$PACKAGE_NAME'" >&2 fi MERGED_MANIFEST="$APP_MODULE_DIR/build/intermediates/packaged_manifests/debug/AndroidManifest.xml" @@ -1413,7 +1403,7 @@ TEST_RUNTIME_PACKAGE="${RUNTIME_PACKAGE}.test" RUNTIME_STUB_FQCN="${RUNTIME_PACKAGE}.${MAIN_NAME}Stub" ba_log "Preparing device for APK installation" -if ! ensure_framework_ready "$EMULATOR_SERIAL"; then +if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then dump_emulator_diagnostics stop_emulator exit 1 @@ -1448,7 +1438,7 @@ if ! adb_install_file_path "$EMULATOR_SERIAL" "$TEST_APK"; then exit 1 fi -if ! ensure_framework_ready "$EMULATOR_SERIAL"; then +if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then dump_emulator_diagnostics stop_emulator exit 1 From 9ce86e6273b287a537e6a022871e0493edbed529 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:41:34 +0300 Subject: [PATCH 04/35] Align instrumentation install retries and Gradle namespace --- scripts/build-android-app.sh | 26 +++- scripts/update_android_ui_test_gradle.py | 158 ++++++++++++++++++----- 2 files changed, 145 insertions(+), 39 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 355bac2292..2e2e6c018c 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -622,7 +622,7 @@ sed -n '1,80p' "$APP_BUILD_GRADLE" | sed 's/^/[build-android-app] app.gradle: /' chmod +x "$GRADLE_PROJECT_DIR/gradlew" -GRADLE_UPDATE_OUTPUT="$("$SCRIPT_DIR/update_android_ui_test_gradle.py" "$APP_BUILD_GRADLE")" +GRADLE_UPDATE_OUTPUT="$("$SCRIPT_DIR/update_android_ui_test_gradle.py" --package-name "$PACKAGE_NAME" "$APP_BUILD_GRADLE")" if [ -n "$GRADLE_UPDATE_OUTPUT" ]; then while IFS= read -r line; do [ -n "$line" ] && ba_log "$line" @@ -1426,16 +1426,28 @@ fi ba_log "Installing app APK: $APP_APK" if ! adb_install_file_path "$EMULATOR_SERIAL" "$APP_APK"; then - dump_emulator_diagnostics - stop_emulator - exit 1 + ba_log "App APK install failed; restarting framework services and retrying" + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell stop >/dev/null 2>&1 || true + sleep 2 + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell start >/dev/null 2>&1 || true + if ! adb_install_file_path "$EMULATOR_SERIAL" "$APP_APK"; then + dump_emulator_diagnostics + stop_emulator + exit 1 + fi fi ba_log "Installing androidTest APK: $TEST_APK" if ! adb_install_file_path "$EMULATOR_SERIAL" "$TEST_APK"; then - dump_emulator_diagnostics - stop_emulator - exit 1 + ba_log "androidTest APK install failed; restarting framework services and retrying" + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell stop >/dev/null 2>&1 || true + sleep 2 + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell start >/dev/null 2>&1 || true + if ! adb_install_file_path "$EMULATOR_SERIAL" "$TEST_APK"; then + dump_emulator_diagnostics + stop_emulator + exit 1 + fi fi if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then diff --git a/scripts/update_android_ui_test_gradle.py b/scripts/update_android_ui_test_gradle.py index 4e732c23f0..5cb908439f 100755 --- a/scripts/update_android_ui_test_gradle.py +++ b/scripts/update_android_ui_test_gradle.py @@ -6,12 +6,13 @@ import pathlib import re import sys +from typing import Iterable, Optional -COMPILE_SDK_LINE = " compileSdkVersion 35\n" - -TEST_OPTIONS_SNIPPET = """ testOptions {\n animationsDisabled = true\n }\n\n""" - -DEFAULT_CONFIG_SNIPPET = """ defaultConfig {\n testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n }\n\n""" +COMPILE_SDK = 35 +MIN_SDK = 19 +TARGET_SDK = 35 +VERSION_CODE = 100 +VERSION_NAME = "1.0" ANDROID_TEST_DEPENDENCIES = ( "androidx.test:core:1.5.0", @@ -28,8 +29,12 @@ def __init__(self, content: str) -> None: self.content = content self.configuration_used: str | None = None self.added_dependencies: list[str] = [] + self._package_name: str | None = None - def _iter_blocks(self): + def set_package_name(self, package_name: Optional[str]) -> None: + self._package_name = package_name + + def _iter_blocks(self) -> Iterable[tuple[str, int, int, list[str]]]: content = self.content length = len(content) stack: list[tuple[str, int]] = [] @@ -91,8 +96,43 @@ def _find_block(self, name: str, *, parent: str | None = None) -> tuple[int, int return start, end return None + def ensure_namespace(self) -> None: + if not self._package_name: + return + android_block = self._find_block("android") + if not android_block: + raise SystemExit("Unable to locate android block in Gradle file") + block_text = self.content[android_block[0]:android_block[1]] + pattern = re.compile(r"^(\s*)namespace\s+['\"]([^'\"]+)['\"]\s*$", re.MULTILINE) + + def repl(match: re.Match[str]) -> str: + indent = match.group(1) + return f"{indent}namespace \"{self._package_name}\"" + + if pattern.search(block_text): + block_text = pattern.sub(repl, block_text, count=1) + else: + insert = block_text.find('\n', block_text.find('android')) + if insert == -1: + brace = block_text.find('{') + if brace == -1: + raise SystemExit("Malformed android block") + insert = brace + 1 + else: + insert += 1 + namespace_line = f" namespace \"{self._package_name}\"\n" + block_text = block_text[:insert] + namespace_line + block_text[insert:] + self.content = self.content[:android_block[0]] + block_text + self.content[android_block[1]:] + def ensure_compile_sdk(self) -> None: - if re.search(r"compileSdkVersion\s+\d+", self.content): + pattern = re.compile(r"^(\s*)compileSdk(?:Version)?\s+\d+\s*$", re.MULTILINE) + + def repl(match: re.Match[str]) -> str: + indent = match.group(1) + return f"{indent}compileSdk {COMPILE_SDK}" + + if pattern.search(self.content): + self.content = pattern.sub(repl, self.content, count=1) return android_block = self._find_block("android") if not android_block: @@ -100,7 +140,64 @@ def ensure_compile_sdk(self) -> None: insert = self.content.find('\n', android_block[0]) + 1 if insert <= 0: insert = android_block[0] + len("android {") - self.content = self.content[:insert] + COMPILE_SDK_LINE + self.content[insert:] + line = f" compileSdk {COMPILE_SDK}\n" + self.content = self.content[:insert] + line + self.content[insert:] + + @staticmethod + def _ensure_block_entry(block_text: str, key: str, replacement: str) -> str: + pattern = re.compile(rf"^(\s*){key}\b.*$", re.MULTILINE) + if pattern.search(block_text): + def repl(match: re.Match[str]) -> str: + indent = match.group(1) + return f"{indent}{replacement}" + + return pattern.sub(repl, block_text, count=1) + indent_match = re.search(r"\{\s*\n(\s*)", block_text) + indent = indent_match.group(1) if indent_match else " " + brace_index = block_text.find('{') + if brace_index == -1: + return block_text + insert_pos = block_text.find('\n', brace_index) + if insert_pos == -1: + block_text = block_text + '\n' + insert_pos = len(block_text) - 1 + insert_pos += 1 + insertion = f"{indent}{replacement}\n" + return block_text[:insert_pos] + insertion + block_text[insert_pos:] + + def ensure_default_config(self) -> None: + replacements: list[tuple[str, str]] = [ + ("minSdk", f"minSdk {MIN_SDK}"), + ("targetSdk", f"targetSdk {TARGET_SDK}"), + ("versionCode", f"versionCode {VERSION_CODE}"), + ("versionName", f"versionName \"{VERSION_NAME}\""), + ( + "testInstrumentationRunner", + "testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"", + ), + ("multiDexEnabled", "multiDexEnabled true"), + ] + if self._package_name: + replacements.insert(0, ("applicationId", f"applicationId \"{self._package_name}\"")) + block = self._find_block("defaultConfig", parent="android") + if not block: + lines = [" defaultConfig {"] + for _, line in replacements: + lines.append(f" {line}") + lines.append(" }\n") + snippet = "\n".join(lines) + android_block = self._find_block("android") + if not android_block: + raise SystemExit("Unable to locate android block in Gradle file") + insert = self.content.find('\n', android_block[0]) + 1 + if insert <= 0: + insert = android_block[0] + len("android {") + self.content = self.content[:insert] + snippet + self.content[insert:] + return + block_text = self.content[block[0]:block[1]] + for key, replacement in replacements: + block_text = self._ensure_block_entry(block_text, key, replacement) + self.content = self.content[:block[0]] + block_text + self.content[block[1]:] def ensure_test_options(self) -> None: if "animationsDisabled" in self.content: @@ -110,7 +207,10 @@ def ensure_test_options(self) -> None: insert = self.content.find('\n', test_block[0]) + 1 body = " animationsDisabled = true\n" self.content = ( - self.content[:insert] + body + self.content[insert:test_block[1]] + self.content[test_block[1]:] + self.content[:insert] + + body + + self.content[insert:test_block[1]] + + self.content[test_block[1]:] ) return android_block = self._find_block("android") @@ -121,24 +221,8 @@ def ensure_test_options(self) -> None: insert = android_block[0] + len("android {") self.content = self.content[:insert] + TEST_OPTIONS_SNIPPET + self.content[insert:] - def ensure_instrumentation_runner(self) -> None: - if "testInstrumentationRunner" in self.content: - return - default_block = self._find_block("defaultConfig", parent="android") - if default_block: - insert = self.content.find('\n', default_block[0]) + 1 - line = " testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n" - self.content = ( - self.content[:insert] + line + self.content[insert:default_block[1]] + self.content[default_block[1]:] - ) - return - android_block = self._find_block("android") - if not android_block: - raise SystemExit("Unable to locate android block in Gradle file") - insert = self.content.find('\n', android_block[0]) + 1 - if insert <= 0: - insert = android_block[0] + len("android {") - self.content = self.content[:insert] + DEFAULT_CONFIG_SNIPPET + self.content[insert:] + def remove_dex_options(self) -> None: + self.content = re.sub(r"\s*dexOptions\s*\{[^{}]*\}\s*", "\n", self.content) def ensure_dependencies(self) -> None: block = self._find_dependencies_block() @@ -181,10 +265,16 @@ def _find_dependencies_block(self) -> tuple[int, int] | None: if parents and parents[-1] in {"buildscript", "pluginManagement"}: continue block_content = self.content[start:end] - if re.search(r"^\s*(implementation|api|compile|androidTestImplementation|androidTestCompile)\b", block_content, re.MULTILINE): + if re.search( + r"^\s*(implementation|api|compile|androidTestImplementation|androidTestCompile)\b", + block_content, + re.MULTILINE, + ): preferred = (start, end) break - if "classpath" in block_content and not re.search(r"^\s*(implementation|api|compile)\b", block_content, re.MULTILINE): + if "classpath" in block_content and not re.search( + r"^\s*(implementation|api|compile)\b", block_content, re.MULTILINE + ): continue if fallback is None: fallback = (start, end) @@ -216,9 +306,11 @@ def _select_configuration(block: str, content: str) -> str: return "androidTestImplementation" def apply(self) -> None: + self.ensure_namespace() self.ensure_compile_sdk() + self.ensure_default_config() self.ensure_test_options() - self.ensure_instrumentation_runner() + self.remove_dex_options() self.ensure_dependencies() def summary(self) -> str: @@ -233,13 +325,14 @@ def summary(self) -> str: ) -def process(path: pathlib.Path) -> None: +def process(path: pathlib.Path, package_name: Optional[str]) -> None: content = path.read_text(encoding="utf-8") if "android {" not in content: raise SystemExit( "Selected Gradle file doesn't contain an android { } block. Check module path." ) editor = GradleFile(content) + editor.set_package_name(package_name) editor.apply() path.write_text(editor.content, encoding="utf-8") print(editor.summary()) @@ -248,12 +341,13 @@ def process(path: pathlib.Path) -> None: def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("gradle_file", type=pathlib.Path) + parser.add_argument("--package-name", dest="package_name", default=None) args = parser.parse_args(argv) if not args.gradle_file.is_file(): parser.error(f"Gradle file not found: {args.gradle_file}") - process(args.gradle_file) + process(args.gradle_file, args.package_name) return 0 From c19a946b5b1e66e1850d771382c85e724fd7f33a Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:17:31 +0300 Subject: [PATCH 05/35] Define missing test options snippet constant --- scripts/update_android_ui_test_gradle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/update_android_ui_test_gradle.py b/scripts/update_android_ui_test_gradle.py index 5cb908439f..ffba40d29b 100755 --- a/scripts/update_android_ui_test_gradle.py +++ b/scripts/update_android_ui_test_gradle.py @@ -23,6 +23,12 @@ "androidx.test.uiautomator:uiautomator:2.2.0", ) +TEST_OPTIONS_SNIPPET = """ + testOptions { + animationsDisabled = true + } +""" + class GradleFile: def __init__(self, content: str) -> None: From e900a0db3d92553fd82e870bf846a7d189c545d9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:17:34 +0300 Subject: [PATCH 06/35] Add missing test options snippet constant --- scripts/update_android_ui_test_gradle.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/update_android_ui_test_gradle.py b/scripts/update_android_ui_test_gradle.py index ffba40d29b..c9b49a111f 100755 --- a/scripts/update_android_ui_test_gradle.py +++ b/scripts/update_android_ui_test_gradle.py @@ -23,7 +23,8 @@ "androidx.test.uiautomator:uiautomator:2.2.0", ) -TEST_OPTIONS_SNIPPET = """ +# Snippet injected into the android { } block to keep instrumentation runs stable. +TEST_OPTIONS_SNIPPET: str = """ testOptions { animationsDisabled = true } From 70e63b92a6d27788bb9b4aafcf250771971ffe17 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:11:33 +0300 Subject: [PATCH 07/35] Harden framework readiness check before APK installs --- scripts/build-android-app.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 2e2e6c018c..5021233829 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -940,25 +940,30 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok system_pid pm_ok activity_ok + local boot_ok system_pid pm_ok activity_ok service_ok service_status boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" pm_ok=0 activity_ok=0 + service_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1; then pm_ok=1 fi if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then activity_ok=1 fi + service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" + if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then + service_ok=1 + fi - if [ "$boot_ok" = "1" ] && [ -n "$system_pid" ] && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ]; then + if [ "$boot_ok" = "1" ] && [ -n "$system_pid" ] && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?} pm_ready=$pm_ok activity_ready=$activity_ok)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?} pm_ready=$pm_ok activity_ready=$activity_ok package_service_ready=$service_ok)" last_log=$SECONDS fi sleep 2 From 09098c19bb3bc52cf7c881d2628085a0d7bd2e6c Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:46:15 +0300 Subject: [PATCH 08/35] Harden emulator readiness gating before installs --- scripts/build-android-app.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 5021233829..9f1cb1c03c 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -940,30 +940,36 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok system_pid pm_ok activity_ok service_ok service_status + local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" pm_ok=0 activity_ok=0 service_ok=0 + user_ready=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1; then pm_ok=1 fi if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then activity_ok=1 fi + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell am get-current-user >/dev/null 2>&1; then + user_ready=1 + fi service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 fi - if [ "$boot_ok" = "1" ] && [ -n "$system_pid" ] && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ]; then + if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ + && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ] && [ $user_ready -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?} pm_ready=$pm_ok activity_ready=$activity_ok package_service_ready=$service_ok)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok package_service_ready=$service_ok user_ready=$user_ready)" last_log=$SECONDS fi sleep 2 @@ -1229,10 +1235,15 @@ if ! wait_for_package_service "$EMULATOR_SERIAL"; then exit 1 fi +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell locksettings set-disabled true >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am get-current-user >/dev/null 2>&1 || true if ! wait_for_api_level "$EMULATOR_SERIAL"; then dump_emulator_diagnostics @@ -1242,6 +1253,7 @@ fi "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm path android | sed 's/^/[build-android-app] pm path android: /' || true +"$ADB_BIN" start-server >/dev/null 2>&1 || true "$ADB_BIN" kill-server >/dev/null 2>&1 || true "$ADB_BIN" start-server >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" wait-for-device From c927501ca1817211017f1b45cb22cf3dafb099a4 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:24:06 +0300 Subject: [PATCH 09/35] Retry non-streamed installs after confirming framework readiness --- scripts/build-android-app.sh | 56 +++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 9f1cb1c03c..987c84ee48 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1306,29 +1306,53 @@ fi adb_install_file_path() { local serial="$1" apk="$2" local remote_tmp="/data/local/tmp/$(basename "$apk")" + local attempts="${ADB_INSTALL_ATTEMPTS:-3}" + local sleep_between="${ADB_INSTALL_RETRY_DELAY_SECONDS:-5}" + local attempt install_status apk_size - if ! adb_wait_framework_ready "$serial"; then - return 1 + if ! [[ "$attempts" =~ ^[0-9]+$ ]] || [ "$attempts" -le 0 ]; then + attempts=3 fi - - "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true - if ! "$ADB_BIN" -s "$serial" push "$apk" "$remote_tmp" >/dev/null 2>&1; then - ba_log "Failed to push $(basename "$apk") to $remote_tmp" >&2 - return 1 + if ! [[ "$sleep_between" =~ ^[0-9]+$ ]]; then + sleep_between=5 fi - local install_status=1 - if "$ADB_BIN" -s "$serial" shell pm install -r -t -g "$remote_tmp"; then - install_status=0 - else - local apk_size - apk_size=$(stat -c%s "$apk" 2>/dev/null || wc -c <"$apk") - if [ -n "$apk_size" ] && "$ADB_BIN" -s "$serial" shell "cat '$remote_tmp' | pm install -r -t -g -S $apk_size"; then + install_status=1 + + for attempt in $(seq 1 "$attempts"); do + if ! adb_wait_framework_ready "$serial"; then + ba_log "Android framework not ready before install attempt $attempt for $(basename "$apk")" + sleep "$sleep_between" + continue + fi + + "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + if ! "$ADB_BIN" -s "$serial" push "$apk" "$remote_tmp" >/dev/null 2>&1; then + ba_log "Failed to push $(basename "$apk") to $remote_tmp on attempt $attempt" >&2 + sleep "$sleep_between" + continue + fi + + if "$ADB_BIN" -s "$serial" shell pm install -r -t -g "$remote_tmp"; then install_status=0 + else + apk_size=$(stat -c%s "$apk" 2>/dev/null || wc -c <"$apk") + if [ -n "$apk_size" ] && "$ADB_BIN" -s "$serial" shell "cat '$remote_tmp' | pm install -r -t -g -S $apk_size"; then + install_status=0 + fi fi - fi - "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + "$ADB_BIN" -s "$serial" shell rm -f "$remote_tmp" >/dev/null 2>&1 || true + + if [ "$install_status" -eq 0 ]; then + ba_log "Install of $(basename "$apk") succeeded on attempt $attempt" + break + fi + + ba_log "Install attempt $attempt for $(basename "$apk") failed; retrying after ${sleep_between}s" + sleep "$sleep_between" + done + return $install_status } From 649481e601bd8342a9e18614709a731d1e44e731 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:00:11 +0300 Subject: [PATCH 10/35] Harden emulator readiness and verify APK installs --- scripts/build-android-app.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 987c84ee48..5c303da8c8 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1238,6 +1238,7 @@ fi "$ADB_BIN" -s "$EMULATOR_SERIAL" shell locksettings set-disabled true >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon true >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true @@ -1497,6 +1498,20 @@ if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then exit 1 fi +if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list packages | grep -q "^package:${RUNTIME_PACKAGE//./\.}$"; then + ba_log "ERROR: Installed package $RUNTIME_PACKAGE not visible on $EMULATOR_SERIAL" + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + +if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list packages | grep -q "^package:${TEST_RUNTIME_PACKAGE//./\.}$"; then + ba_log "ERROR: Installed test package $TEST_RUNTIME_PACKAGE not visible on $EMULATOR_SERIAL" + dump_emulator_diagnostics + stop_emulator + exit 1 +fi + "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list instrumentation | sed "s/^/[build-android-app] instrumentation: /" || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pm list packages | grep -E "${RUNTIME_PACKAGE//./\.}|${RUNTIME_PACKAGE//./\.}\\.test" | sed "s/^/[build-android-app] package: /" || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell cmd package resolve-activity --brief "$RUNTIME_PACKAGE/$RUNTIME_STUB_FQCN" | sed "s/^/[build-android-app] resolve-stub (pre-test): /" || true From e4f2db5e5de53bfb7c344e61355d24397c6b5527 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:15:17 +0300 Subject: [PATCH 11/35] Relax screenshot content detection --- .../HelloCodenameOneUiTest.java.tmpl | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/scripts/templates/HelloCodenameOneUiTest.java.tmpl b/scripts/templates/HelloCodenameOneUiTest.java.tmpl index 032eb0ed62..ade785a359 100644 --- a/scripts/templates/HelloCodenameOneUiTest.java.tmpl +++ b/scripts/templates/HelloCodenameOneUiTest.java.tmpl @@ -235,21 +235,48 @@ public class @MAIN_NAME@UiTest { if (pixels == null || pixels.length == 0) { return false; } - int reference = pixels[0]; + + final int minVisiblePixels = 32; + final int minDifferingPixels = 8; + final int minLumaSpread = 12; + + int reference = -1; int differing = 0; + int visible = 0; + int minLuma = 255; + int maxLuma = 0; + for (int argb : pixels) { int alpha = (argb >>> 24) & 0xFF; - if (alpha == 0) { + if (alpha < 16) { continue; } - if (argb != reference) { + + visible++; + int rgb = argb & 0x00FFFFFF; + if (reference == -1) { + reference = rgb; + } else if (rgb != reference) { differing++; - if (differing > pixels.length / 100) { - return true; - } + } + + int r = (rgb >>> 16) & 0xFF; + int g = (rgb >>> 8) & 0xFF; + int b = rgb & 0xFF; + int luma = (299 * r + 587 * g + 114 * b) / 1000; + if (luma < minLuma) { + minLuma = luma; + } + if (luma > maxLuma) { + maxLuma = luma; + } + + if (visible >= minVisiblePixels && differing >= minDifferingPixels) { + return true; } } - return false; + + return visible >= minVisiblePixels && (differing >= minDifferingPixels || (maxLuma - minLuma) >= minLumaSpread); } private static File saveBitmap(Bitmap bitmap, String fileName) throws IOException { From c1bbc5698c8caf1ea08d7b181646f48633e543f4 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:50:58 +0300 Subject: [PATCH 12/35] Normalize stub activity manifest and modernize Gradle updates --- scripts/build-android-app.sh | 7 ++--- scripts/update_android_ui_test_gradle.py | 33 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 5c303da8c8..941e26bcc3 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -392,12 +392,13 @@ seen = {"value": False} def replace_activity(match): body = match.group(0) - if "tools:node=" in body: - body = re.sub(r'tools:node="[^"]*"', 'tools:node="replace"', body, count=1) + body = re.sub(r'\s+tools:node="[^"]*"', '', body) + if 'android:exported=' in body: + body = re.sub(r'android:exported="[^"]*"', 'android:exported="true"', body, count=1) else: close = body.find('>') if close != -1: - body = body[:close] + ' tools:node="replace"' + body[close:] + body = body[:close] + ' android:exported="true"' + body[close:] if seen["value"]: return '' seen["value"] = True diff --git a/scripts/update_android_ui_test_gradle.py b/scripts/update_android_ui_test_gradle.py index c9b49a111f..47d1564e51 100755 --- a/scripts/update_android_ui_test_gradle.py +++ b/scripts/update_android_ui_test_gradle.py @@ -231,6 +231,37 @@ def ensure_test_options(self) -> None: def remove_dex_options(self) -> None: self.content = re.sub(r"\s*dexOptions\s*\{[^{}]*\}\s*", "\n", self.content) + def convert_lint_options(self) -> None: + block = self._find_block("lintOptions", parent="android") + if not block: + return + block_text = self.content[block[0]:block[1]] + start = block_text.find('{') + end = block_text.rfind('}') + if start == -1 or end == -1 or end <= start: + self.content = ( + self.content[:block[0]] + + " lint {\n }\n" + + self.content[block[1]:] + ) + return + body = block_text[start + 1 : end] + lines = [] + for line in body.splitlines(): + stripped = line.strip() + if not stripped: + lines.append("") + else: + lines.append(f" {stripped}") + joined = "\n".join(lines) + if joined: + joined = joined + "\n" + replacement = f" lint {{\n{joined} }}\n" + self.content = self.content[:block[0]] + replacement + self.content[block[1]:] + + def remove_jcenter(self) -> None: + self.content = re.sub(r"^\s*jcenter\(\)\s*$\n?", "", self.content, flags=re.MULTILINE) + def ensure_dependencies(self) -> None: block = self._find_dependencies_block() if not block: @@ -318,6 +349,8 @@ def apply(self) -> None: self.ensure_default_config() self.ensure_test_options() self.remove_dex_options() + self.convert_lint_options() + self.remove_jcenter() self.ensure_dependencies() def summary(self) -> str: From a52c32a9660abc8f538ac15763f45d839ca30779 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:30:49 +0300 Subject: [PATCH 13/35] Improve emulator readiness and launch stability --- scripts/build-android-app.sh | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 941e26bcc3..49ee977f86 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -731,7 +731,7 @@ create_avd() { rm -f "$ini_file" rm -rf "$image_dir" fi - if ! ANDROID_AVD_HOME="$avd_dir" "$manager" create avd -n "$name" -k "$image" --device "2.7in QVGA" --force >/dev/null <<<'no' + if ! ANDROID_AVD_HOME="$avd_dir" "$manager" create avd -n "$name" -k "$image" --device "pixel_5" --force >/dev/null <<<'no' then ba_log "Failed to create Android Virtual Device $name using image $image" >&2 find "$avd_dir" -maxdepth 2 -mindepth 1 -print | sed 's/^/[build-android-app] AVD: /' >&2 || true @@ -753,7 +753,7 @@ configure_avd() { return fi declare -A settings=( - ["hw.ramSize"]=4096 + ["hw.ramSize"]=2048 ["disk.dataPartition.size"]=8192M ["fastboot.forceColdBoot"]=yes ["hw.bluetooth"]=no @@ -761,6 +761,7 @@ configure_avd() { ["hw.camera.front"]=none ["hw.audioInput"]=no ["hw.audioOutput"]=no + ["hw.cpu.ncore"]=2 ) local key value for key in "${!settings[@]}"; do @@ -941,7 +942,7 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status + local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status cmd_ok boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" @@ -949,6 +950,7 @@ adb_framework_ready_once() { activity_ok=0 service_ok=0 user_ready=0 + cmd_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1; then pm_ok=1 fi @@ -958,19 +960,23 @@ adb_framework_ready_once() { if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell am get-current-user >/dev/null 2>&1; then user_ready=1 fi + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd -l >/dev/null 2>&1; then + cmd_ok=1 + fi service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 fi if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ - && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ] && [ $user_ready -eq 1 ]; then + && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ] \ + && [ $cmd_ok -eq 1 ] && [ $user_ready -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok package_service_ready=$service_ok user_ready=$user_ready)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok cmd_ready=$cmd_ok package_service_ready=$service_ok user_ready=$user_ready)" last_log=$SECONDS fi sleep 2 @@ -1152,8 +1158,9 @@ ba_log "Starting headless Android emulator $AVD_NAME on port $EMULATOR_PORT" ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_PORT" \ -no-window -no-snapshot -no-snapshot-load -no-snapshot-save -wipe-data \ -gpu swiftshader_indirect -no-audio -no-boot-anim \ - -accel off -no-accel -camera-back none -camera-front none -skip-adb-auth \ - -feature -Vulkan -netfast -memory 4096 >"$EMULATOR_LOG" 2>&1 & + -accel on -camera-back none -camera-front none -skip-adb-auth \ + -feature -Vulkan -netfast -skin 1080x1920 -memory 2048 -cores 2 \ + -writable-system -selinux permissive >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT @@ -1239,9 +1246,10 @@ fi "$ADB_BIN" -s "$EMULATOR_SERIAL" shell locksettings set-disabled true >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon true >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon usb >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true From c9d5101348669251c8fa766c2ad116f0c6aa9fc5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:57:00 +0300 Subject: [PATCH 14/35] Disable emulator acceleration on CI hosts --- scripts/build-android-app.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 49ee977f86..a2417ee974 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1158,13 +1158,18 @@ ba_log "Starting headless Android emulator $AVD_NAME on port $EMULATOR_PORT" ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_PORT" \ -no-window -no-snapshot -no-snapshot-load -no-snapshot-save -wipe-data \ -gpu swiftshader_indirect -no-audio -no-boot-anim \ - -accel on -camera-back none -camera-front none -skip-adb-auth \ + -accel off -no-metrics -camera-back none -camera-front none -skip-adb-auth \ -feature -Vulkan -netfast -skin 1080x1920 -memory 2048 -cores 2 \ -writable-system -selinux permissive >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT -sleep 5 +for _ in {1..60}; do + if "$ADB_BIN" -s "$EMULATOR_SERIAL" get-state >/dev/null 2>&1; then + break + fi + sleep 1 +done detect_emulator_serial() { local deadline current_devices serial existing From 76357f3b8e19671c2a697dd018d898b4eba0f1c8 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:24:44 +0300 Subject: [PATCH 15/35] Stabilize headless ARM emulator startup for UI tests --- scripts/build-android-app.sh | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index a2417ee974..949b82cc8c 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -706,7 +706,7 @@ install_android_packages() { "platform-tools" \ "emulator" \ "platforms;android-35" \ - "system-images;android-35;google_apis;x86_64" >/dev/null 2>&1 || true + "system-images;android-35;google_apis;arm64-v8a" >/dev/null 2>&1 || true } create_avd() { @@ -721,16 +721,9 @@ create_avd() { mkdir -p "$avd_dir" local ini_file="$avd_dir/$name.ini" local image_dir="$avd_dir/$name.avd" - if [ -f "$ini_file" ] && [ -d "$image_dir" ]; then - if grep -F -q "$image" "$ini_file" 2>/dev/null; then - ba_log "Reusing existing Android Virtual Device $name" - configure_avd "$avd_dir" "$name" - return - fi - ba_log "Existing Android Virtual Device $name uses a different system image; recreating" - rm -f "$ini_file" - rm -rf "$image_dir" - fi + ANDROID_AVD_HOME="$avd_dir" "$manager" delete avd -n "$name" >/dev/null 2>&1 || true + rm -f "$ini_file" + rm -rf "$image_dir" if ! ANDROID_AVD_HOME="$avd_dir" "$manager" create avd -n "$name" -k "$image" --device "pixel_5" --force >/dev/null <<<'no' then ba_log "Failed to create Android Virtual Device $name using image $image" >&2 @@ -762,6 +755,8 @@ configure_avd() { ["hw.audioInput"]=no ["hw.audioOutput"]=no ["hw.cpu.ncore"]=2 + ["hw.cpu.arch"]="arm64" + ["abi.type"]="arm64-v8a" ) local key value for key in "${!settings[@]}"; do @@ -942,7 +937,7 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status cmd_ok + local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status cmd_ok resolve_ok boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" @@ -963,6 +958,11 @@ adb_framework_ready_once() { if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd -l >/dev/null 2>&1; then cmd_ok=1 fi + resolve_ok=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell \ + "cmd package resolve-activity --brief android.intent.action.MAIN -c android.intent.category.HOME" >/dev/null 2>&1; then + resolve_ok=1 + fi service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 @@ -970,13 +970,14 @@ adb_framework_ready_once() { if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ] \ + && [ $resolve_ok -eq 1 ] \ && [ $cmd_ok -eq 1 ] && [ $user_ready -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok cmd_ready=$cmd_ok package_service_ready=$service_ok user_ready=$user_ready)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok cmd_ready=$cmd_ok package_service_ready=$service_ok resolve_ready=$resolve_ok user_ready=$user_ready)" last_log=$SECONDS fi sleep 2 @@ -1133,7 +1134,7 @@ if [ ! -x "$EMULATOR_BIN" ]; then fi AVD_NAME="cn1UiTestAvd" -SYSTEM_IMAGE="system-images;android-35;google_apis;x86_64" +SYSTEM_IMAGE="system-images;android-35;google_apis;arm64-v8a" AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-${RUNNER_TEMP:-$HOME}/cn1-android-avd}" mkdir -p "$AVD_CACHE_ROOT" AVD_HOME="$AVD_CACHE_ROOT" @@ -1160,7 +1161,7 @@ ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_P -gpu swiftshader_indirect -no-audio -no-boot-anim \ -accel off -no-metrics -camera-back none -camera-front none -skip-adb-auth \ -feature -Vulkan -netfast -skin 1080x1920 -memory 2048 -cores 2 \ - -writable-system -selinux permissive >"$EMULATOR_LOG" 2>&1 & + -writable-system -selinux permissive -partition-size 2048 >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT @@ -1348,11 +1349,11 @@ adb_install_file_path() { continue fi - if "$ADB_BIN" -s "$serial" shell pm install -r -t -g "$remote_tmp"; then + if "$ADB_BIN" -s "$serial" shell pm install -r -t -d -g "$remote_tmp"; then install_status=0 else apk_size=$(stat -c%s "$apk" 2>/dev/null || wc -c <"$apk") - if [ -n "$apk_size" ] && "$ADB_BIN" -s "$serial" shell "cat '$remote_tmp' | pm install -r -t -g -S $apk_size"; then + if [ -n "$apk_size" ] && "$ADB_BIN" -s "$serial" shell "cat '$remote_tmp' | pm install -r -t -d -g -S $apk_size"; then install_status=0 fi fi From 9a4b7fb3166c78279b54b844bbe9cb6556741e49 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 19:49:42 +0300 Subject: [PATCH 16/35] Use x86_64 emulator image for UI tests --- scripts/build-android-app.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 949b82cc8c..8f6857f191 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -706,7 +706,7 @@ install_android_packages() { "platform-tools" \ "emulator" \ "platforms;android-35" \ - "system-images;android-35;google_apis;arm64-v8a" >/dev/null 2>&1 || true + "system-images;android-35;google_apis;x86_64" >/dev/null 2>&1 || true } create_avd() { @@ -755,8 +755,8 @@ configure_avd() { ["hw.audioInput"]=no ["hw.audioOutput"]=no ["hw.cpu.ncore"]=2 - ["hw.cpu.arch"]="arm64" - ["abi.type"]="arm64-v8a" + ["hw.cpu.arch"]="x86_64" + ["abi.type"]="x86_64" ) local key value for key in "${!settings[@]}"; do @@ -1134,7 +1134,7 @@ if [ ! -x "$EMULATOR_BIN" ]; then fi AVD_NAME="cn1UiTestAvd" -SYSTEM_IMAGE="system-images;android-35;google_apis;arm64-v8a" +SYSTEM_IMAGE="system-images;android-35;google_apis;x86_64" AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-${RUNNER_TEMP:-$HOME}/cn1-android-avd}" mkdir -p "$AVD_CACHE_ROOT" AVD_HOME="$AVD_CACHE_ROOT" From cd24441ec56e08d30bebd3a6eb5314e24fa7cf44 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:25:01 +0300 Subject: [PATCH 17/35] Harden emulator readiness for x86_64 UI tests --- scripts/build-android-app.sh | 40 +++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 8f6857f191..9e2fcf376a 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -937,47 +937,42 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok dev_boot system_pid pm_ok activity_ok service_ok user_ready service_status cmd_ok resolve_ok + local boot_ok dev_boot system_pid pm_ok service_ok cmd_ok resolve_ok service_status boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" + pm_ok=0 - activity_ok=0 - service_ok=0 - user_ready=0 - cmd_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1; then pm_ok=1 fi - if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity get-standby-bucket >/dev/null 2>&1; then - activity_ok=1 - fi - if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell am get-current-user >/dev/null 2>&1; then - user_ready=1 - fi + + cmd_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd -l >/dev/null 2>&1; then cmd_ok=1 fi + resolve_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell \ "cmd package resolve-activity --brief android.intent.action.MAIN -c android.intent.category.HOME" >/dev/null 2>&1; then resolve_ok=1 fi + + service_ok=0 service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 fi if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ - && [ $pm_ok -eq 1 ] && [ $activity_ok -eq 1 ] && [ $service_ok -eq 1 ] \ - && [ $resolve_ok -eq 1 ] \ - && [ $cmd_ok -eq 1 ] && [ $user_ready -eq 1 ]; then + && [ $pm_ok -eq 1 ] && [ $cmd_ok -eq 1 ] && [ $resolve_ok -eq 1 ] \ + && [ $service_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok activity_ready=$activity_ok cmd_ready=$cmd_ok package_service_ready=$service_ok resolve_ready=$resolve_ok user_ready=$user_ready)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok cmd_ready=$cmd_ok package_service_ready=$service_ok resolve_ready=$resolve_ok)" last_log=$SECONDS fi sleep 2 @@ -1161,7 +1156,7 @@ ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_P -gpu swiftshader_indirect -no-audio -no-boot-anim \ -accel off -no-metrics -camera-back none -camera-front none -skip-adb-auth \ -feature -Vulkan -netfast -skin 1080x1920 -memory 2048 -cores 2 \ - -writable-system -selinux permissive -partition-size 2048 >"$EMULATOR_LOG" 2>&1 & + -partition-size 4096 >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT @@ -1221,6 +1216,13 @@ if ! wait_for_emulator "$EMULATOR_SERIAL"; then exit 1 fi +# Provision and wake the device before framework readiness checks +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon true >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 || true + if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pidof system_server >/dev/null 2>&1; then ba_log "system_server not running after boot; restarting framework" "$ADB_BIN" -s "$EMULATOR_SERIAL" shell stop >/dev/null 2>&1 || true @@ -1250,12 +1252,8 @@ if ! wait_for_package_service "$EMULATOR_SERIAL"; then fi "$ADB_BIN" -s "$EMULATOR_SERIAL" shell locksettings set-disabled true >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global device_provisioned 1 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put secure user_setup_complete 1 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon usb >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true +"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon true >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true From 4f044ab9f401c5f48941a5d2c82ec88559157c5e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:58:44 +0300 Subject: [PATCH 18/35] Tighten Android framework readiness checks --- scripts/build-android-app.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 9e2fcf376a..115e343f8e 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -937,7 +937,7 @@ adb_framework_ready_once() { local last_log=$SECONDS while [ $SECONDS -lt $deadline ]; do - local boot_ok dev_boot system_pid pm_ok service_ok cmd_ok resolve_ok service_status + local boot_ok dev_boot system_pid pm_ok pm_list_ok service_ok cmd_ok activity_ok resolve_ok service_status boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" system_pid="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" @@ -947,6 +947,11 @@ adb_framework_ready_once() { pm_ok=1 fi + pm_list_ok=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm list packages -f >/dev/null 2>&1; then + pm_list_ok=1 + fi + cmd_ok=0 if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd -l >/dev/null 2>&1; then cmd_ok=1 @@ -958,6 +963,11 @@ adb_framework_ready_once() { resolve_ok=1 fi + activity_ok=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd activity top >/dev/null 2>&1; then + activity_ok=1 + fi + service_ok=0 service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' || true)" if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then @@ -965,14 +975,14 @@ adb_framework_ready_once() { fi if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ - && [ $pm_ok -eq 1 ] && [ $cmd_ok -eq 1 ] && [ $resolve_ok -eq 1 ] \ - && [ $service_ok -eq 1 ]; then + && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $cmd_ok -eq 1 ] \ + && [ $activity_ok -eq 1 ] && [ $resolve_ok -eq 1 ] && [ $service_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then - ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok cmd_ready=$cmd_ok package_service_ready=$service_ok resolve_ready=$resolve_ok)" + ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok pm_list_ready=$pm_list_ok cmd_ready=$cmd_ok activity_ready=$activity_ok package_service_ready=$service_ok resolve_ready=$resolve_ok)" last_log=$SECONDS fi sleep 2 From 128ffeb9828cd58f0531d4c48d2cb8963d028fb2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:47:25 +0300 Subject: [PATCH 19/35] Relax Android framework readiness heuristics --- scripts/build-android-app.sh | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 115e343f8e..79bc707eff 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -933,8 +933,10 @@ adb_framework_ready_once() { log_interval=10 fi + local start_time=$SECONDS local deadline=$((SECONDS + phase_timeout)) local last_log=$SECONDS + local last_home_nudge=$SECONDS while [ $SECONDS -lt $deadline ]; do local boot_ok dev_boot system_pid pm_ok pm_list_ok service_ok cmd_ok activity_ok resolve_ok service_status @@ -975,16 +977,37 @@ adb_framework_ready_once() { fi if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ - && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $cmd_ok -eq 1 ] \ - && [ $activity_ok -eq 1 ] && [ $resolve_ok -eq 1 ] && [ $service_ok -eq 1 ]; then - ba_log "Android framework ready on $serial (system_server=$system_pid)" - return 0 + && [ $pm_ok -eq 1 ] && [ $cmd_ok -eq 1 ] && [ $service_ok -eq 1 ]; then + if [ $pm_list_ok -eq 1 ] || [ $activity_ok -eq 1 ] || [ $resolve_ok -eq 1 ]; then + ba_log "Android framework ready on $serial (system_server=$system_pid)" + return 0 + fi + + local secondary_wait="${FRAMEWORK_READY_SECONDARY_GRACE_SECONDS:-90}" + if ! [[ "$secondary_wait" =~ ^[0-9]+$ ]]; then + secondary_wait=90 + fi + if [ $((SECONDS - start_time)) -ge "$secondary_wait" ]; then + ba_log "Android framework heuristically ready on $serial (system_server=$system_pid; pm_list=$pm_list_ok activity=$activity_ok resolve=$resolve_ok)" + return 0 + fi fi if [ $((SECONDS - last_log)) -ge $log_interval ]; then ba_log "Waiting for Android framework on $serial (system_server=${system_pid:-down} boot_ok=${boot_ok:-?}/${dev_boot:-?} pm_ready=$pm_ok pm_list_ready=$pm_list_ok cmd_ready=$cmd_ok activity_ready=$activity_ok package_service_ready=$service_ok resolve_ready=$resolve_ok)" last_log=$SECONDS fi + + local home_retry="${FRAMEWORK_READY_HOME_RETRY_SECONDS:-30}" + if ! [[ "$home_retry" =~ ^[0-9]+$ ]]; then + home_retry=30 + fi + if [ $((SECONDS - last_home_nudge)) -ge "$home_retry" ]; then + run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell input keyevent 82 >/dev/null 2>&1 || true + run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell \ + am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 || true + last_home_nudge=$SECONDS + fi sleep 2 done From 82fa15fb579b9f2ac18847b933f07426cdc51b91 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 07:30:40 +0300 Subject: [PATCH 20/35] Optimize API 35 emulator boot for CI --- .github/workflows/scripts-android.yml | 24 +++ scripts/build-android-app.sh | 210 ++++++++++++++++++-------- 2 files changed, 168 insertions(+), 66 deletions(-) diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index 84a9bd1e10..d41b6b19fb 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -15,8 +15,32 @@ name: Test Android build scripts jobs: build-android: runs-on: ubuntu-latest + timeout-minutes: 90 steps: - uses: actions/checkout@v4 + - name: Free disk space for emulator + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + df -h + - name: Increase swap space for emulator + run: | + sudo swapoff -a + sudo fallocate -l 8G /swapfile + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + free -h + - name: Configure emulator timeouts + run: | + echo "EMULATOR_BOOT_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" + echo "PACKAGE_SERVICE_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" + echo "FRAMEWORK_READY_PRIMARY_TIMEOUT_SECONDS=300" >> "$GITHUB_ENV" + echo "FRAMEWORK_READY_RESTART_TIMEOUT_SECONDS=240" >> "$GITHUB_ENV" + echo "EMULATOR_POST_BOOT_GRACE_SECONDS=30" >> "$GITHUB_ENV" + echo "UI_TEST_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" - name: Setup workspace run: ./scripts/setup-workspace.sh -q -DskipTests - name: Build Android port diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 79bc707eff..d86e61c410 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -746,17 +746,34 @@ configure_avd() { return fi declare -A settings=( - ["hw.ramSize"]=2048 - ["disk.dataPartition.size"]=8192M + ["hw.ramSize"]=6144 + ["disk.dataPartition.size"]=12288M ["fastboot.forceColdBoot"]=yes ["hw.bluetooth"]=no ["hw.camera.back"]=none ["hw.camera.front"]=none ["hw.audioInput"]=no ["hw.audioOutput"]=no - ["hw.cpu.ncore"]=2 + ["hw.cpu.ncore"]=4 ["hw.cpu.arch"]="x86_64" ["abi.type"]="x86_64" + ["hw.gpu.enabled"]=yes + ["hw.gpu.mode"]=auto + ["hw.keyboard"]=yes + ["hw.sensors.proximity"]=no + ["hw.sensors.magnetic_field"]=no + ["hw.sensors.orientation"]=no + ["hw.sensors.temperature"]=no + ["hw.sensors.light"]=no + ["hw.sensors.pressure"]=no + ["hw.sensors.humidity"]=no + ["hw.gps"]=no + ["showDeviceFrame"]=no + ["skin.dynamic"]=yes + ["hw.lcd.density"]=320 + ["hw.lcd.width"]=720 + ["hw.lcd.height"]=1280 + ["vm.heapSize"]=512 ) local key value for key in "${!settings[@]}"; do @@ -771,119 +788,181 @@ configure_avd() { wait_for_emulator() { local serial="$1" + "$ADB_BIN" start-server >/dev/null "$ADB_BIN" -s "$serial" wait-for-device - local boot_timeout="${EMULATOR_BOOT_TIMEOUT_SECONDS:-900}" + local boot_timeout="${EMULATOR_BOOT_TIMEOUT_SECONDS:-1200}" if ! [[ "$boot_timeout" =~ ^[0-9]+$ ]] || [ "$boot_timeout" -le 0 ]; then - ba_log "Invalid EMULATOR_BOOT_TIMEOUT_SECONDS=$boot_timeout provided; falling back to 900" - boot_timeout=900 - fi - local poll_interval="${EMULATOR_BOOT_POLL_INTERVAL_SECONDS:-5}" - if ! [[ "$poll_interval" =~ ^[0-9]+$ ]] || [ "$poll_interval" -le 0 ]; then - poll_interval=5 + boot_timeout=1200 fi - local status_log_interval="${EMULATOR_BOOT_STATUS_LOG_INTERVAL_SECONDS:-30}" - if ! [[ "$status_log_interval" =~ ^[0-9]+$ ]] || [ "$status_log_interval" -le 0 ]; then - status_log_interval=30 + + local log_interval="${EMULATOR_BOOT_STATUS_LOG_INTERVAL_SECONDS:-15}" + if ! [[ "$log_interval" =~ ^[0-9]+$ ]] || [ "$log_interval" -le 0 ]; then + log_interval=15 fi local deadline=$((SECONDS + boot_timeout)) local last_log=$SECONDS - local boot_completed="0" - local dev_boot_completed="0" - local bootanim="" - local bootanim_exit="" - local device_state="" - local boot_ready=0 + # Stage 1: wait for device to report "device" + ba_log "Stage 1: waiting for emulator $serial to report device state" + local device_online=0 while [ $SECONDS -lt $deadline ]; do - device_state="$($ADB_BIN -s "$serial" get-state 2>/dev/null | tr -d '\r')" - if [ "$device_state" != "device" ]; then - if [ $((SECONDS - last_log)) -ge $status_log_interval ]; then - ba_log "Waiting for emulator $serial to become ready (state=$device_state)" - last_log=$SECONDS - fi - sleep "$poll_interval" - continue + local state + state="$($ADB_BIN -s "$serial" get-state 2>/dev/null | tr -d '\r')" + if [ "$state" = "device" ]; then + device_online=1 + break + fi + if [ $((SECONDS - last_log)) -ge $log_interval ]; then + ba_log "Waiting for emulator $serial to come online (state=${state:-})" + last_log=$SECONDS fi + sleep 3 + done - boot_completed="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" - dev_boot_completed="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r')" - bootanim="$($ADB_BIN -s "$serial" shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r')" - bootanim_exit="$($ADB_BIN -s "$serial" shell getprop service.bootanim.exit 2>/dev/null | tr -d '\r')" + if [ $device_online -ne 1 ]; then + ba_log "Emulator $serial did not report device state within ${boot_timeout}s" >&2 + return 1 + fi - if { [ "$boot_completed" = "1" ] || [ "$boot_completed" = "true" ]; } \ - && { [ -z "$dev_boot_completed" ] || [ "$dev_boot_completed" = "1" ] || [ "$dev_boot_completed" = "true" ]; }; then - boot_ready=1 + # Stage 2: wait for boot properties to indicate readiness + ba_log "Stage 2: waiting for core system properties on $serial" + local system_ready=0 + last_log=$SECONDS + while [ $SECONDS -lt $deadline ]; do + local sys_boot dev_boot bootanim bootanim_exit + sys_boot="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n')" + dev_boot="$($ADB_BIN -s "$serial" shell getprop dev.bootcomplete 2>/dev/null | tr -d '\r\n')" + bootanim="$($ADB_BIN -s "$serial" shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r\n')" + bootanim_exit="$($ADB_BIN -s "$serial" shell getprop service.bootanim.exit 2>/dev/null | tr -d '\r\n')" + + if { [ "$sys_boot" = "1" ] || [ "$sys_boot" = "true" ]; } && { + [ "$dev_boot" = "1" ] || [ "$dev_boot" = "true" ]; + }; then + system_ready=1 break fi if [ "$bootanim" = "stopped" ] || [ "$bootanim_exit" = "1" ]; then - boot_ready=2 + system_ready=1 + ba_log "Emulator $serial reported boot animation stopped; continuing despite unset bootcomplete" break fi - if [ $((SECONDS - last_log)) -ge $status_log_interval ]; then - ba_log "Waiting for emulator $serial to boot (sys.boot_completed=${boot_completed:-} dev.bootcomplete=${dev_boot_completed:-} bootanim=${bootanim:-} bootanim_exit=${bootanim_exit:-})" + if [ $((SECONDS - last_log)) -ge $log_interval ]; then + ba_log "Waiting for system services on $serial (sys.boot_completed=${sys_boot:-} dev.bootcomplete=${dev_boot:-} bootanim=${bootanim:-} bootanim_exit=${bootanim_exit:-})" last_log=$SECONDS fi - sleep "$poll_interval" + sleep 3 done - if [ $boot_ready -eq 0 ]; then - ba_log "Emulator $serial failed to boot within ${boot_timeout}s (sys.boot_completed=${boot_completed:-} dev.bootcomplete=${dev_boot_completed:-} bootanim=${bootanim:-} bootanim_exit=${bootanim_exit:-} state=${device_state:-})" >&2 + if [ $system_ready -ne 1 ]; then + ba_log "Emulator $serial failed to finish boot sequence within ${boot_timeout}s" >&2 return 1 - elif [ $boot_ready -eq 2 ]; then - ba_log "Emulator $serial reported boot animation stopped; proceeding without bootcomplete properties" fi + # Stage 3: baseline device configuration + ba_log "Stage 3: configuring emulator $serial for UI testing" "$ADB_BIN" -s "$serial" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$serial" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$serial" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true "$ADB_BIN" -s "$serial" shell input keyevent 82 >/dev/null 2>&1 || true "$ADB_BIN" -s "$serial" shell wm dismiss-keyguard >/dev/null 2>&1 || true + return 0 } wait_for_package_service() { local serial="$1" - local timeout="${PACKAGE_SERVICE_TIMEOUT_SECONDS:-${PACKAGE_SERVICE_TIMEOUT:-600}}" - local per_try="${PACKAGE_SERVICE_PER_TRY_TIMEOUT_SECONDS:-${PACKAGE_SERVICE_PER_TRY_TIMEOUT:-5}}" + local timeout="${PACKAGE_SERVICE_TIMEOUT_SECONDS:-1200}" + local per_try="${PACKAGE_SERVICE_PER_TRY_TIMEOUT_SECONDS:-15}" if ! [[ "$timeout" =~ ^[0-9]+$ ]] || [ "$timeout" -le 0 ]; then - timeout=600 + timeout=1200 fi if ! [[ "$per_try" =~ ^[0-9]+$ ]] || [ "$per_try" -le 0 ]; then - per_try=5 + per_try=15 fi local deadline=$((SECONDS + timeout)) local last_log=$SECONDS + local restart_count=0 + local max_restarts=5 + + ba_log "Waiting for package manager service on $serial" while [ $SECONDS -lt $deadline ]; do - local boot_ok ce_ok - boot_ok="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" - ce_ok="$($ADB_BIN -s "$serial" shell getprop sys.user.0.ce_available 2>/dev/null | tr -d '\r')" + local sys_boot system_server pm_ready pm_list - if timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd package path android >/dev/null 2>&1 \ - || timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1 \ - || timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd package list packages >/dev/null 2>&1 \ - || timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm list packages >/dev/null 2>&1 \ - || timeout "$per_try" "$ADB_BIN" -s "$serial" shell dumpsys package >/dev/null 2>&1; then + sys_boot="$($ADB_BIN -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" + system_server="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pidof system_server 2>/dev/null | tr -d '\r' || true)" + + pm_ready=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm path android >/dev/null 2>&1 \ + || run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell cmd package path android >/dev/null 2>&1; then + pm_ready=1 + fi + + pm_list=0 + if run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell pm list packages >/dev/null 2>&1; then + pm_list=1 + fi + + if [ $pm_ready -eq 1 ] && [ $pm_list -eq 1 ] && [ -n "$system_server" ]; then + ba_log "Package manager ready on $serial (system_server=$system_server)" return 0 fi - if [ $((SECONDS - last_log)) -ge 10 ]; then - ba_log "Waiting for package manager service on $serial (boot_ok=${boot_ok:-?} ce_ok=${ce_ok:-?})" + local service_status + service_status="$(run_with_timeout "$per_try" "$ADB_BIN" -s "$serial" shell service check package 2>/dev/null | tr -d '\r' | tr -d '\n' || true)" + if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then + if [ $pm_ready -eq 1 ] && [ -n "$system_server" ]; then + ba_log "Package service responding despite limited signals; continuing" + return 0 + fi + fi + + if [ $((SECONDS - last_log)) -ge 20 ]; then + ba_log "PM status on $serial: boot=${sys_boot:-?} system_server=${system_server:-} pm_ready=$pm_ready pm_list=$pm_list" last_log=$SECONDS fi - sleep 2 + + local elapsed=$((SECONDS - (deadline - timeout))) + if [ "$sys_boot" = "1" ] && [ $pm_ready -eq 0 ] && [ $elapsed -gt 0 ] && [ $((elapsed % 180)) -eq 0 ] \ + && [ $restart_count -lt $max_restarts ]; then + restart_count=$((restart_count + 1)) + ba_log "Package manager not ready after ${elapsed}s; restarting framework (attempt ${restart_count}/${max_restarts})" + "$ADB_BIN" -s "$serial" shell stop >/dev/null 2>&1 || true + sleep 5 + "$ADB_BIN" -s "$serial" shell start >/dev/null 2>&1 || true + sleep 15 + continue + fi + + sleep 3 done ba_log "Package manager service not ready on $serial after ${timeout}s" >&2 return 1 } +optimize_api35_after_boot() { + local serial="$1" + ba_log "Optimizing emulator $serial for headless testing" + + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell pm disable-user com.google.android.googlequicksearchbox >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell pm disable-user com.android.vending >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell pm disable-user com.google.android.gms >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell cmd package bg-dexopt-job >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell logcat -c >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell locksettings set-disabled true >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell svc power stayon true >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell wm dismiss-keyguard >/dev/null 2>&1 || true + run_with_timeout 30 "$ADB_BIN" -s "$serial" shell am kill-all >/dev/null 2>&1 || true + sleep 10 +} + wait_for_api_level() { local serial="$1" local timeout="${API_LEVEL_TIMEOUT_SECONDS:-600}" @@ -1184,12 +1263,16 @@ EMULATOR_SERIAL="emulator-$EMULATOR_PORT" EMULATOR_LOG="$GRADLE_PROJECT_DIR/emulator.log" ba_log "Starting headless Android emulator $AVD_NAME on port $EMULATOR_PORT" +export ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL="${ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL:-120}" +export ANDROID_EMULATOR_KVM_DISABLE_CHECK=1 +export ANDROID_SDK_ROOT_NO_SETTINGS=1 ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_PORT" \ -no-window -no-snapshot -no-snapshot-load -no-snapshot-save -wipe-data \ - -gpu swiftshader_indirect -no-audio -no-boot-anim \ + -gpu auto -no-audio -no-boot-anim \ -accel off -no-metrics -camera-back none -camera-front none -skip-adb-auth \ - -feature -Vulkan -netfast -skin 1080x1920 -memory 2048 -cores 2 \ - -partition-size 4096 >"$EMULATOR_LOG" 2>&1 & + -feature -Vulkan -feature -GLDMA -feature -GLDirectMem -no-passive-gps \ + -netdelay none -netspeed full -skin 720x1280 -memory 6144 -cores 4 \ + -partition-size 12288 -delay-adb >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT @@ -1284,12 +1367,7 @@ if ! wait_for_package_service "$EMULATOR_SERIAL"; then exit 1 fi -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell locksettings set-disabled true >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell svc power stayon true >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell wm dismiss-keyguard >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global window_animation_scale 0 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global transition_animation_scale 0 >/dev/null 2>&1 || true -"$ADB_BIN" -s "$EMULATOR_SERIAL" shell settings put global animator_duration_scale 0 >/dev/null 2>&1 || true +optimize_api35_after_boot "$EMULATOR_SERIAL" "$ADB_BIN" -s "$EMULATOR_SERIAL" shell am get-current-user >/dev/null 2>&1 || true if ! wait_for_api_level "$EMULATOR_SERIAL"; then From 6115d332b9544a848f293248e7973ed3ff68f298 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 07:59:52 +0300 Subject: [PATCH 21/35] Reduce API 35 emulator disk footprint --- .github/workflows/scripts-android.yml | 12 ++++++ .gitignore | 1 + scripts/build-android-app.sh | 58 ++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index d41b6b19fb..1e1c6b2ed4 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -18,6 +18,16 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 + - name: Free disk space (action) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: true + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true - name: Free disk space for emulator run: | sudo rm -rf /usr/share/dotnet @@ -47,6 +57,8 @@ jobs: run: ./scripts/build-android-port.sh -q -DskipTests - name: Build Hello Codename One Android app run: ./scripts/build-android-app.sh -q -DskipTests + env: + AVD_CACHE_ROOT: ${{ github.workspace }}/.android-avd - name: Upload UI test artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index f88f65279d..107297647f 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ pom.xml.tag !.brokk/style.md !.brokk/review.md !.brokk/project.properties +.android-avd/ diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index d86e61c410..6ed867faca 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -709,6 +709,42 @@ install_android_packages() { "system-images;android-35;google_apis;x86_64" >/dev/null 2>&1 || true } +check_disk_space() { + local target_dir="$1" + local required_mb="$2" + + ba_log "Checking disk space for AVD creation in $target_dir" + df -h "$target_dir" | sed 's/^/[build-android-app] disk: /' + + local available_mb + available_mb=$(df -m "$target_dir" | awk 'NR==2 {print $4}') + ba_log "Available space: ${available_mb} MB (required ${required_mb} MB)" + + if [ "$available_mb" -lt "$required_mb" ]; then + ba_log "Insufficient disk space detected; attempting cleanup" >&2 + cleanup_disk_space + available_mb=$(df -m "$target_dir" | awk 'NR==2 {print $4}') + ba_log "Post-cleanup available space: ${available_mb} MB" + if [ "$available_mb" -lt "$required_mb" ]; then + return 1 + fi + fi + return 0 +} + +cleanup_disk_space() { + ba_log "Cleaning up disk space before AVD creation" + if [ -n "${HOME:-}" ]; then + rm -rf "$HOME/.gradle/caches/build-cache-*" 2>/dev/null || true + rm -rf "$HOME/.m2/repository" 2>/dev/null || true + fi + if [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "$ANDROID_SDK_ROOT/system-images" ]; then + find "$ANDROID_SDK_ROOT/system-images" -mindepth 1 -maxdepth 1 ! -name "android-35" -exec rm -rf {} + 2>/dev/null || true + fi + rm -rf /tmp/cn1-* 2>/dev/null || true + df -h | sed 's/^/[build-android-app] disk-after-cleanup: /' +} + create_avd() { local manager="$1" local name="$2" @@ -719,6 +755,13 @@ create_avd() { exit 1 fi mkdir -p "$avd_dir" + + local required_mb=10240 + if ! check_disk_space "$avd_dir" "$required_mb"; then + ba_log "ERROR: insufficient disk space for AVD creation (need ${required_mb} MB)" >&2 + exit 1 + fi + local ini_file="$avd_dir/$name.ini" local image_dir="$avd_dir/$name.avd" ANDROID_AVD_HOME="$avd_dir" "$manager" delete avd -n "$name" >/dev/null 2>&1 || true @@ -736,6 +779,7 @@ create_avd() { exit 1 fi configure_avd "$avd_dir" "$name" + du -sh "$image_dir" 2>/dev/null | sed 's/^/[build-android-app] AVD-size: /' || true } configure_avd() { @@ -746,8 +790,8 @@ configure_avd() { return fi declare -A settings=( - ["hw.ramSize"]=6144 - ["disk.dataPartition.size"]=12288M + ["hw.ramSize"]=4096 + ["disk.dataPartition.size"]=6144M ["fastboot.forceColdBoot"]=yes ["hw.bluetooth"]=no ["hw.camera.back"]=none @@ -1242,7 +1286,11 @@ fi AVD_NAME="cn1UiTestAvd" SYSTEM_IMAGE="system-images;android-35;google_apis;x86_64" -AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-${RUNNER_TEMP:-$HOME}/cn1-android-avd}" +if [ -n "${GITHUB_WORKSPACE:-}" ]; then + AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-$GITHUB_WORKSPACE/.android-avd}" +else + AVD_CACHE_ROOT="${AVD_CACHE_ROOT:-${RUNNER_TEMP:-$HOME}/cn1-android-avd}" +fi mkdir -p "$AVD_CACHE_ROOT" AVD_HOME="$AVD_CACHE_ROOT" ba_log "Using AVD home at $AVD_HOME" @@ -1271,8 +1319,8 @@ ANDROID_AVD_HOME="$AVD_HOME" "$EMULATOR_BIN" -avd "$AVD_NAME" -port "$EMULATOR_P -gpu auto -no-audio -no-boot-anim \ -accel off -no-metrics -camera-back none -camera-front none -skip-adb-auth \ -feature -Vulkan -feature -GLDMA -feature -GLDirectMem -no-passive-gps \ - -netdelay none -netspeed full -skin 720x1280 -memory 6144 -cores 4 \ - -partition-size 12288 -delay-adb >"$EMULATOR_LOG" 2>&1 & + -netdelay none -netspeed full -skin 720x1280 -memory 4096 -cores 4 \ + -partition-size 6144 -delay-adb >"$EMULATOR_LOG" 2>&1 & EMULATOR_PID=$! trap stop_emulator EXIT From be031e25dd1f3d1bb74f7b21308ffa4647b4e5ad Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 08:17:32 +0300 Subject: [PATCH 22/35] Install CI GUI dependencies before disk cleanup --- .github/workflows/scripts-android.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index 1e1c6b2ed4..f2506c2861 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -18,6 +18,10 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 + - name: Install system dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 xmlstarlet - name: Free disk space (action) uses: jlumbroso/free-disk-space@main with: From 3316255a8fd5631c1ad5dcb4060f5013a3359dc3 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 09:00:20 +0300 Subject: [PATCH 23/35] Ensure CI installs GUI deps before cleanup --- .github/workflows/scripts-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index f2506c2861..06ad84c7e9 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -18,7 +18,7 @@ jobs: timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - name: Install system dependencies + - name: Install System Dependencies run: | sudo apt-get update -y sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 xmlstarlet From aa02255d41602427c7b209b012889144b0b109e6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 09:27:33 +0300 Subject: [PATCH 24/35] Preserve system PATH in setup workspace env --- scripts/setup-workspace.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index fd59ef20d7..abfe166fd7 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -161,7 +161,7 @@ cat > "$ENV_FILE" < Date: Sat, 11 Oct 2025 09:56:30 +0300 Subject: [PATCH 25/35] Preserve system binaries in build-android-port PATH --- scripts/build-android-port.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/build-android-port.sh b/scripts/build-android-port.sh index d0395724f9..be22c37e65 100755 --- a/scripts/build-android-port.sh +++ b/scripts/build-android-port.sh @@ -108,7 +108,22 @@ fi log "Loaded environment: JAVA_HOME=${JAVA_HOME:-} JAVA17_HOME=${JAVA17_HOME:-} MAVEN_HOME=${MAVEN_HOME:-}" -export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH" +# setup-workspace.sh already prepends the Java and Maven bin directories to PATH +# when generating env.sh. Ensure common system locations remain reachable even +# if the inherited PATH was truncated before sourcing the environment file. +case ":$PATH:" in + *:/usr/bin:*) ;; + *) PATH="$PATH:/usr/bin" ;; +esac +case ":$PATH:" in + *:/bin:*) ;; + *) PATH="$PATH:/bin" ;; +esac +case ":$PATH:" in + *:/usr/local/bin:*) ;; + *) PATH="$PATH:/usr/local/bin" ;; +esac +export PATH "$JAVA_HOME/bin/java" -version "$JAVA17_HOME/bin/java" -version "$MAVEN_HOME/bin/mvn" -version From cd4ca8f6c48d1563cf9f69ad02f2e12ea3d269db Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:11:34 +0300 Subject: [PATCH 26/35] Preserve system PATH entries in workspace scripts --- scripts/build-android-port.sh | 6 ++++++ scripts/setup-workspace.sh | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/build-android-port.sh b/scripts/build-android-port.sh index be22c37e65..df52ef7bda 100755 --- a/scripts/build-android-port.sh +++ b/scripts/build-android-port.sh @@ -29,6 +29,12 @@ load_environment() { sed 's/^/[build-android-port] ENV: /' "$ENV_FILE" # shellcheck disable=SC1090 source "$ENV_FILE" + + # Ensure common system binaries remain reachable even if the sourced + # environment overrides PATH. + if ! command -v xvfb-run >/dev/null 2>&1; then + export PATH="$PATH:/usr/bin:/bin:/usr/local/bin" + fi } check_java_home() { diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index abfe166fd7..1136d09970 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -161,7 +161,7 @@ cat > "$ENV_FILE" < Date: Sat, 11 Oct 2025 12:07:37 +0300 Subject: [PATCH 27/35] Capture current PATH when writing workspace env --- scripts/setup-workspace.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index 1136d09970..059d0e7434 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -157,11 +157,23 @@ else fi log "Writing environment to $ENV_FILE" +current_path="${PATH:-}" +for required in /usr/bin /bin /usr/local/bin; do + case ":$current_path:" in + *:"$required":*) ;; + *) current_path="${current_path:+$current_path:}$required" ;; + esac +done +if [ -n "$current_path" ]; then + path_suffix=":$current_path" +else + path_suffix="" +fi cat > "$ENV_FILE" < Date: Sat, 11 Oct 2025 12:32:32 +0300 Subject: [PATCH 28/35] Fix env PATH expansion in setup workspace --- scripts/setup-workspace.sh | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index 059d0e7434..8b485a5068 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -157,23 +157,11 @@ else fi log "Writing environment to $ENV_FILE" -current_path="${PATH:-}" -for required in /usr/bin /bin /usr/local/bin; do - case ":$current_path:" in - *:"$required":*) ;; - *) current_path="${current_path:+$current_path:}$required" ;; - esac -done -if [ -n "$current_path" ]; then - path_suffix=":$current_path" -else - path_suffix="" -fi -cat > "$ENV_FILE" < "$ENV_FILE" <<'ENV' export JAVA_HOME="$JAVA_HOME" export JAVA17_HOME="$JAVA17_HOME" export MAVEN_HOME="$MAVEN_HOME" -export PATH="\$JAVA_HOME/bin:\$MAVEN_HOME/bin$path_suffix" +export PATH="\$JAVA_HOME/bin:\$MAVEN_HOME/bin:\$PATH" ENV log "Workspace environment file metadata" From ad63258a4ec41578eaa1959e3fc81ee1e681c48e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:04:35 +0300 Subject: [PATCH 29/35] Fix env.sh heredoc expansion --- scripts/setup-workspace.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup-workspace.sh b/scripts/setup-workspace.sh index 8b485a5068..17bc9336e6 100755 --- a/scripts/setup-workspace.sh +++ b/scripts/setup-workspace.sh @@ -157,12 +157,12 @@ else fi log "Writing environment to $ENV_FILE" -cat > "$ENV_FILE" <<'ENV' +cat > "$ENV_FILE" < Date: Sat, 11 Oct 2025 13:38:27 +0300 Subject: [PATCH 30/35] Trying manually --- scripts/build-android-port.sh | 43 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/scripts/build-android-port.sh b/scripts/build-android-port.sh index df52ef7bda..e994f46d96 100755 --- a/scripts/build-android-port.sh +++ b/scripts/build-android-port.sh @@ -27,14 +27,29 @@ load_environment() { ls -l "$ENV_FILE" | while IFS= read -r line; do log "$line"; done log "Workspace environment file contents" sed 's/^/[build-android-port] ENV: /' "$ENV_FILE" + + # Preserve system PATH before sourcing + SYSTEM_PATH_BACKUP="$PATH" + # shellcheck disable=SC1090 source "$ENV_FILE" - # Ensure common system binaries remain reachable even if the sourced - # environment overrides PATH. - if ! command -v xvfb-run >/dev/null 2>&1; then - export PATH="$PATH:/usr/bin:/bin:/usr/local/bin" - fi + # Restore system paths that may have been lost during sourcing + # Ensure common system binaries remain reachable + case ":$PATH:" in + *:/usr/bin:*) ;; + *) PATH="$PATH:/usr/bin" ;; + esac + case ":$PATH:" in + *:/bin:*) ;; + *) PATH="$PATH:/bin" ;; + esac + case ":$PATH:" in + *:/usr/local/bin:*) ;; + *) PATH="$PATH:/usr/local/bin" ;; + esac + + export PATH } check_java_home() { @@ -114,22 +129,6 @@ fi log "Loaded environment: JAVA_HOME=${JAVA_HOME:-} JAVA17_HOME=${JAVA17_HOME:-} MAVEN_HOME=${MAVEN_HOME:-}" -# setup-workspace.sh already prepends the Java and Maven bin directories to PATH -# when generating env.sh. Ensure common system locations remain reachable even -# if the inherited PATH was truncated before sourcing the environment file. -case ":$PATH:" in - *:/usr/bin:*) ;; - *) PATH="$PATH:/usr/bin" ;; -esac -case ":$PATH:" in - *:/bin:*) ;; - *) PATH="$PATH:/bin" ;; -esac -case ":$PATH:" in - *:/usr/local/bin:*) ;; - *) PATH="$PATH:/usr/local/bin" ;; -esac -export PATH "$JAVA_HOME/bin/java" -version "$JAVA17_HOME/bin/java" -version "$MAVEN_HOME/bin/mvn" -version @@ -145,4 +144,4 @@ if [ ! -f "$BUILD_CLIENT" ]; then fi fi -run_maven -q -f maven/pom.xml -pl android -am -Dmaven.javadoc.skip=true -Djava.awt.headless=true clean install "$@" +run_maven -q -f maven/pom.xml -pl android -am -Dmaven.javadoc.skip=true -Djava.awt.headless=true clean install "$@" \ No newline at end of file From 734c6301ba91c16ce182e5cbcb43ae2e62006336 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:50:30 +0300 Subject: [PATCH 31/35] Set full path for xvfb --- scripts/build-android-app.sh | 2 +- scripts/build-android-port.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 6ed867faca..eb8b877947 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -114,7 +114,7 @@ MAVEN_CMD=( ba_log "Generating Codename One application skeleton via codenameone-maven-plugin" ( cd "$WORK_DIR" - xvfb-run -a "${MAVEN_CMD[@]}" -q \ + /usr/bin/xvfb-run -a "${MAVEN_CMD[@]}" -q \ com.codenameone:codenameone-maven-plugin:7.0.204:generate-app-project \ -DgroupId="$GROUP_ID" \ -DartifactId="$ARTIFACT_ID" \ diff --git a/scripts/build-android-port.sh b/scripts/build-android-port.sh index e994f46d96..c55bf7dd5f 100755 --- a/scripts/build-android-port.sh +++ b/scripts/build-android-port.sh @@ -134,7 +134,7 @@ log "Loaded environment: JAVA_HOME=${JAVA_HOME:-} JAVA17_HOME=${JAVA17_HO "$MAVEN_HOME/bin/mvn" -version run_maven() { - xvfb-run -a "$MAVEN_HOME/bin/mvn" "$@" + /usr/bin/xvfb-run -a "$MAVEN_HOME/bin/mvn" "$@" } BUILD_CLIENT="$HOME/.codenameone/CodeNameOneBuildClient.jar" From ffc657bc3e3785c6fc820b2e5f4335c510356304 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:56:29 +0300 Subject: [PATCH 32/35] Fixed YAML to verify xvfb is properly installed --- .github/workflows/scripts-android.yml | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index 06ad84c7e9..c1aea50547 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -1,4 +1,3 @@ ---- name: Test Android build scripts 'on': @@ -16,13 +15,11 @@ jobs: build-android: runs-on: ubuntu-latest timeout-minutes: 90 + steps: - uses: actions/checkout@v4 - - name: Install System Dependencies - run: | - sudo apt-get update -y - sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 xmlstarlet - - name: Free disk space (action) + + - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main with: tool-cache: true @@ -31,42 +28,45 @@ jobs: haskell: true large-packages: true docker-images: true - swap-storage: true - - name: Free disk space for emulator - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - df -h - - name: Increase swap space for emulator + swap-storage: false # Changed to false - might be removing packages + + - name: Install System Dependencies run: | - sudo swapoff -a - sudo fallocate -l 8G /swapfile - sudo chmod 600 /swapfile - sudo mkswap /swapfile - sudo swapon /swapfile - free -h - - name: Configure emulator timeouts + sudo apt-get update -y + sudo apt-get install -y xvfb libxrender1 libxtst6 libxi6 xmlstarlet + echo "Verifying xvfb installation:" + ls -la /usr/bin/xvfb-run + which xvfb-run + /usr/bin/xvfb-run --help || true + + - name: Additional Cleanup run: | - echo "EMULATOR_BOOT_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" - echo "PACKAGE_SERVICE_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" - echo "FRAMEWORK_READY_PRIMARY_TIMEOUT_SECONDS=300" >> "$GITHUB_ENV" - echo "FRAMEWORK_READY_RESTART_TIMEOUT_SECONDS=240" >> "$GITHUB_ENV" - echo "EMULATOR_POST_BOOT_GRACE_SECONDS=30" >> "$GITHUB_ENV" - echo "UI_TEST_TIMEOUT_SECONDS=1200" >> "$GITHUB_ENV" + sudo rm -rf /usr/local/lib/android/sdk/build-tools/* 2>/dev/null || true + sudo rm -rf /usr/local/lib/android/sdk/platforms/* 2>/dev/null || true + sudo rm -rf /usr/local/lib/android/sdk/platform-tools 2>/dev/null || true + df -h + - name: Setup workspace run: ./scripts/setup-workspace.sh -q -DskipTests + - name: Build Android port run: ./scripts/build-android-port.sh -q -DskipTests + - name: Build Hello Codename One Android app run: ./scripts/build-android-app.sh -q -DskipTests env: AVD_CACHE_ROOT: ${{ github.workspace }}/.android-avd + EMULATOR_BOOT_TIMEOUT_SECONDS: 1200 + PACKAGE_SERVICE_TIMEOUT_SECONDS: 1200 + FRAMEWORK_READY_PRIMARY_TIMEOUT_SECONDS: 300 + FRAMEWORK_READY_RESTART_TIMEOUT_SECONDS: 240 + EMULATOR_POST_BOOT_GRACE_SECONDS: 30 + UI_TEST_TIMEOUT_SECONDS: 1200 + - name: Upload UI test artifacts if: always() uses: actions/upload-artifact@v4 with: name: hello-codenameone-ui-test-artifacts path: ${{ env.CN1_UI_TEST_ARTIFACT_DIR }} - if-no-files-found: warn + if-no-files-found: warn \ No newline at end of file From 810b39edf8e11f3cfbe9f0f9be66b0bdda5e0973 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:37:44 +0300 Subject: [PATCH 33/35] Improved adb wait --- scripts/build-android-app.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index eb8b877947..bff5cd72f2 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1098,11 +1098,26 @@ adb_framework_ready_once() { if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 fi + + # Success conditions - prioritize package manager over activity manager for API 35 + if [ -n "$system_server" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $service_ok -eq 1 ]; then + ba_log "Android framework ready on $serial (system_server=$system_server; package manager operational)" + return 0 + fi + + # Fallback: if boot is complete and PM works, proceed even without full activity manager + if [ "$sys_boot" = "1" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ]; then + local time_waiting=$((SECONDS - start_time)) + if [ $time_waiting -ge 120 ]; then + ba_log "Android framework heuristically ready on $serial after ${time_waiting}s (system_server=$system_server; pm functional but activity_manager slow)" + return 0 + fi + fi - if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ + # Original stricter check for early success + if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_server" ] \ && [ $pm_ok -eq 1 ] && [ $cmd_ok -eq 1 ] && [ $service_ok -eq 1 ]; then - if [ $pm_list_ok -eq 1 ] || [ $activity_ok -eq 1 ] || [ $resolve_ok -eq 1 ]; then - ba_log "Android framework ready on $serial (system_server=$system_pid)" + if [ $pm_list_ok -eq 1 ] || [ $activity_ok -eq 1 ] || [ $resolve_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0 fi From 0519e1f6faa47ed0a607e2cd57e41f7b8057e9ca Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:27:36 +0300 Subject: [PATCH 34/35] Initialized the system_server variable --- scripts/build-android-app.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index bff5cd72f2..3fedeeff90 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1098,7 +1098,7 @@ adb_framework_ready_once() { if [ -n "$service_status" ] && printf '%s' "$service_status" | grep -q "found"; then service_ok=1 fi - + # Success conditions - prioritize package manager over activity manager for API 35 if [ -n "$system_server" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $service_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_server; package manager operational)" @@ -1402,11 +1402,14 @@ fi "$ADB_BIN" -s "$EMULATOR_SERIAL" shell input keyevent 82 >/dev/null 2>&1 || true "$ADB_BIN" -s "$EMULATOR_SERIAL" shell am start -a android.intent.action.MAIN -c android.intent.category.HOME >/dev/null 2>&1 || true -if ! "$ADB_BIN" -s "$EMULATOR_SERIAL" shell pidof system_server >/dev/null 2>&1; then +system_server_pid="$("$ADB_BIN" -s "$EMULATOR_SERIAL" shell pidof system_server 2>/dev/null | tr -d '\r\n' || echo "")" +if [ -z "$system_server_pid" ]; then ba_log "system_server not running after boot; restarting framework" "$ADB_BIN" -s "$EMULATOR_SERIAL" shell stop >/dev/null 2>&1 || true sleep 2 "$ADB_BIN" -s "$EMULATOR_SERIAL" shell start >/dev/null 2>&1 || true +else + ba_log "system_server running with PID $system_server_pid" fi if ! adb_wait_framework_ready "$EMULATOR_SERIAL"; then From 5a3ce17d5da909ef1a12f10a36b374ee5782b848 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:08:22 +0300 Subject: [PATCH 35/35] Changed variable name --- scripts/build-android-app.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build-android-app.sh b/scripts/build-android-app.sh index 3fedeeff90..485eb20126 100755 --- a/scripts/build-android-app.sh +++ b/scripts/build-android-app.sh @@ -1100,8 +1100,8 @@ adb_framework_ready_once() { fi # Success conditions - prioritize package manager over activity manager for API 35 - if [ -n "$system_server" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $service_ok -eq 1 ]; then - ba_log "Android framework ready on $serial (system_server=$system_server; package manager operational)" + if [ -n "system_pid" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ] && [ $service_ok -eq 1 ]; then + ba_log "Android framework ready on $serial (system_server=$system_pid; package manager operational)" return 0 fi @@ -1109,13 +1109,13 @@ adb_framework_ready_once() { if [ "$sys_boot" = "1" ] && [ $pm_ok -eq 1 ] && [ $pm_list_ok -eq 1 ]; then local time_waiting=$((SECONDS - start_time)) if [ $time_waiting -ge 120 ]; then - ba_log "Android framework heuristically ready on $serial after ${time_waiting}s (system_server=$system_server; pm functional but activity_manager slow)" + ba_log "Android framework heuristically ready on $serial after ${time_waiting}s (system_server=$system_pid; pm functional but activity_manager slow)" return 0 fi fi # Original stricter check for early success - if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_server" ] \ + if [ "$boot_ok" = "1" ] && [ "$dev_boot" = "1" ] && [ -n "$system_pid" ] \ && [ $pm_ok -eq 1 ] && [ $cmd_ok -eq 1 ] && [ $service_ok -eq 1 ]; then if [ $pm_list_ok -eq 1 ] || [ $activity_ok -eq 1 ] || [ $resolve_ok -eq 1 ]; then ba_log "Android framework ready on $serial (system_server=$system_pid)" return 0