#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" GRADLEW="$ROOT_DIR/gradlew" ANDROID_APP_ID="com.nuvio.app" ANDROID_ACTIVITY=".MainActivity" IOS_PROJECT="$ROOT_DIR/iosApp/iosApp.xcodeproj" IOS_SCHEME="iosApp" IOS_DERIVED_DATA_BASE="$ROOT_DIR/build/ios-derived" IOS_APP_NAME="Nuvio.app" IOS_BUNDLE_ID="com.nuvio.app.Nuvio" IOS_PREFERRED_DEVICE_MODEL="iPhone 14 Pro" usage() { cat <<'EOF' Usage: ./scripts/run-mobile.sh android [e|p] [full|playstore] ./scripts/run-mobile.sh android [full|playstore] ./scripts/run-mobile.sh ios [s|p] [full|appstore] Builds the debug app for the selected platform, installs it on all available Android emulators or connected physical devices, a booted iOS simulator, or the configured iOS physical device, and launches the app. EOF } require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing required command: $1" >&2 exit 1 fi } android_emulator_avds() { emulator -list-avds } booted_android_emulator_serials() { adb devices | awk '$2 == "device" && $1 ~ /^emulator-/ { print $1 }' } connected_android_physical_serials() { adb devices | awk '$2 == "device" && $1 !~ /^emulator-/ { print $1 }' } wait_for_android_device() { local serial="$1" adb -s "$serial" wait-for-device >/dev/null } wait_for_android_emulator() { local serial="$1" local boot_completed="" adb -s "$serial" wait-for-device >/dev/null until [[ "$boot_completed" == "1" ]]; do boot_completed="$(adb -s "$serial" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" sleep 1 done } boot_android_emulator() { local avd_name="$1" echo "Opening Android emulator $avd_name..." >&2 emulator "@$avd_name" >/dev/null 2>&1 & } first_booted_ios_simulator() { xcrun simctl list devices booted | awk -F '[()]' '/Booted/ { print $2; exit }' } preferred_ios_device() { xcrun xcdevice list --timeout 5 2>/dev/null | python3 -c ' import json import sys import os try: devices = json.load(sys.stdin) except Exception: sys.exit(0) physical = [ device for device in devices if device.get("platform") == "com.apple.platform.iphoneos" and not device.get("simulator", False) and device.get("available") is True and device.get("modelName") == os.environ["IOS_PREFERRED_DEVICE_MODEL"] ] if physical: print(physical[0].get("identifier", "")) ' } validate_ios_distribution() { local distribution="$1" case "$distribution" in full|appstore) ;; *) echo "Unknown iOS distribution: $distribution" >&2 echo "Expected one of: full, appstore" >&2 exit 1 ;; esac } validate_android_flavor() { local flavor="$1" case "$flavor" in full|playstore) ;; *) echo "Unknown Android flavor: $flavor" >&2 echo "Expected one of: full, playstore" >&2 exit 1 ;; esac } android_flavor_task_part() { local flavor="$1" case "$flavor" in full) echo "Full" ;; playstore) echo "Playstore" ;; esac } android_apk_path() { local flavor="$1" case "$flavor" in full) echo "$ROOT_DIR/composeApp/build/outputs/apk/full/debug/composeApp-full-debug.apk" ;; playstore) echo "$ROOT_DIR/composeApp/build/outputs/apk/playstore/debug/composeApp-playstore-debug.apk" ;; esac } build_android_apk() { local flavor="$1" local flavor_task_part local apk_path flavor_task_part="$(android_flavor_task_part "$flavor")" apk_path="$(android_apk_path "$flavor")" echo "Building Android $flavor debug APK..." >&2 "$GRADLEW" ":composeApp:assemble${flavor_task_part}Debug" >&2 if [[ ! -f "$apk_path" ]]; then echo "Expected APK not found at: $apk_path" >&2 exit 1 fi printf '%s\n' "$apk_path" } install_and_launch_android() { local device_label="$1" local apk_path="$2" shift 2 local serial for serial in "$@"; do echo "Installing on $device_label $serial..." adb -s "$serial" install -r "$apk_path" echo "Launching app on $serial..." adb -s "$serial" shell am start -n "$ANDROID_APP_ID/$ANDROID_ACTIVITY" done } ios_derived_data_path() { local target="$1" local distribution="$2" echo "$IOS_DERIVED_DATA_BASE-$distribution-$target" } run_android_emulator() { local flavor="${1:-full}" validate_android_flavor "$flavor" require_command adb require_command emulator local booted_serials=() local serial="" while IFS= read -r serial; do [[ -n "$serial" ]] || continue booted_serials+=("$serial") done < <(booted_android_emulator_serials) if [[ ${#booted_serials[@]} -gt 0 ]]; then echo "Using running Android emulators: ${booted_serials[*]}" else local avds=() local avd_name while IFS= read -r avd_name; do [[ -n "$avd_name" ]] || continue avds+=("$avd_name") done < <(android_emulator_avds) if [[ ${#avds[@]} -eq 0 ]]; then echo "No Android emulators available." >&2 echo "Create an AVD first, then rerun: ./scripts/run-mobile.sh android e" >&2 exit 1 fi for avd_name in "${avds[@]}"; do boot_android_emulator "$avd_name" done echo "Waiting for Android emulators to boot..." while IFS= read -r serial; do [[ -n "$serial" ]] || continue booted_serials+=("$serial") done < <(booted_android_emulator_serials) while [[ ${#booted_serials[@]} -lt ${#avds[@]} ]]; do sleep 2 booted_serials=() while IFS= read -r serial; do [[ -n "$serial" ]] || continue booted_serials+=("$serial") done < <(booted_android_emulator_serials) done fi for serial in "${booted_serials[@]}"; do wait_for_android_emulator "$serial" done local apk_path apk_path="$(build_android_apk "$flavor")" install_and_launch_android "emulator" "$apk_path" "${booted_serials[@]}" } run_android_physical() { local flavor="${1:-full}" validate_android_flavor "$flavor" require_command adb local serials=() local serial while IFS= read -r serial; do [[ -n "$serial" ]] || continue serials+=("$serial") done < <(connected_android_physical_serials) if [[ ${#serials[@]} -eq 0 ]]; then echo "No Android physical devices available." >&2 echo "Connect and authorize a device, then rerun: ./scripts/run-mobile.sh android p" >&2 exit 1 fi echo "Using connected Android physical devices: ${serials[*]}" for serial in "${serials[@]}"; do wait_for_android_device "$serial" done local apk_path apk_path="$(build_android_apk "$flavor")" install_and_launch_android "physical device" "$apk_path" "${serials[@]}" } run_ios_simulator() { local distribution="${1:-appstore}" validate_ios_distribution "$distribution" require_command xcodebuild require_command xcrun local simulator_id simulator_id="$(first_booted_ios_simulator)" if [[ -z "$simulator_id" ]]; then echo "No booted iOS simulator found." >&2 echo "Boot a simulator first, then rerun: ./scripts/run-mobile.sh ios s" >&2 exit 1 fi local derived_data_path derived_data_path="$(ios_derived_data_path simulator "$distribution")" local simulator_app_path simulator_app_path="$derived_data_path/Build/Products/Debug-iphonesimulator/$IOS_APP_NAME" echo "Building iOS $distribution debug app for simulator $simulator_id..." env NUVIO_IOS_DISTRIBUTION="$distribution" xcodebuild \ -project "$IOS_PROJECT" \ -scheme "$IOS_SCHEME" \ -configuration Debug \ -destination "id=$simulator_id" \ -derivedDataPath "$derived_data_path" \ build if [[ ! -d "$simulator_app_path" ]]; then echo "Expected iOS simulator app not found at: $simulator_app_path" >&2 exit 1 fi echo "Installing on simulator $simulator_id..." xcrun simctl install "$simulator_id" "$simulator_app_path" echo "Launching app..." xcrun simctl launch "$simulator_id" "$IOS_BUNDLE_ID" } run_ios_physical() { local distribution="${1:-appstore}" validate_ios_distribution "$distribution" require_command xcodebuild require_command xcrun local physical_device_id physical_device_id="$(IOS_PREFERRED_DEVICE_MODEL="$IOS_PREFERRED_DEVICE_MODEL" preferred_ios_device)" if [[ -n "$physical_device_id" ]]; then local derived_data_path derived_data_path="$(ios_derived_data_path device "$distribution")" local device_app_path device_app_path="$derived_data_path/Build/Products/Debug-iphoneos/$IOS_APP_NAME" echo "Building iOS $distribution debug app for physical device $physical_device_id..." env NUVIO_IOS_DISTRIBUTION="$distribution" xcodebuild \ -project "$IOS_PROJECT" \ -scheme "$IOS_SCHEME" \ -configuration Debug \ -destination "id=$physical_device_id" \ -derivedDataPath "$derived_data_path" \ build if [[ ! -d "$device_app_path" ]]; then echo "Expected iOS app not found at: $device_app_path" >&2 exit 1 fi echo "Installing on physical device $physical_device_id..." xcrun devicectl device install app --device "$physical_device_id" "$device_app_path" echo "Launching app..." xcrun devicectl device process launch --device "$physical_device_id" "$IOS_BUNDLE_ID" return fi echo "Preferred iOS device not available: $IOS_PREFERRED_DEVICE_MODEL" >&2 echo "Connect and unlock that device, then rerun: ./scripts/run-mobile.sh ios p" >&2 exit 1 } main() { if [[ $# -lt 1 ]]; then usage exit 1 fi case "$1" in android) if [[ $# -gt 3 ]]; then usage exit 1 fi local android_target="e" local android_flavor="full" if [[ $# -ge 2 ]]; then case "$2" in e|p) android_target="$2" ;; full|playstore) android_flavor="$2" ;; *) echo "Unknown Android target or flavor: $2" >&2 usage exit 1 ;; esac fi if [[ $# -eq 3 ]]; then android_flavor="$3" fi case "$android_target" in e) run_android_emulator "$android_flavor" ;; p) run_android_physical "$android_flavor" ;; esac ;; ios) if [[ $# -lt 2 || $# -gt 3 ]]; then usage exit 1 fi local ios_distribution="${3:-appstore}" case "$2" in s) run_ios_simulator "$ios_distribution" ;; p) run_ios_physical "$ios_distribution" ;; *) echo "Unknown iOS target: $2" >&2 usage exit 1 ;; esac ;; -h|--help|help) usage ;; *) echo "Unknown platform: $1" >&2 usage exit 1 ;; esac } main "$@"