#!/usr/bin/env bash # Drive the dsh-sync app inside MuMu Player 12 from the command line. # # bash android/emulator.sh start # boot the VM, wait for adb # bash android/emulator.sh install # build + install + launch # bash android/emulator.sh shot [out.png] # capture just the app's window # bash android/emulator.sh tap X Y # tap in APP coordinates (0..1080 x 0..1920) # bash android/emulator.sh text "..." # type into the focused field # bash android/emulator.sh key # e.g. 66 = Enter, 4 = Back # bash android/emulator.sh deeplink # http://ip:8020/app#invite= # bash android/emulator.sh ui # dump visible text from the WebView # bash android/emulator.sh log [n] # # Two MuMu behaviours drive the design here, both learned the hard way: # # 1. Every app gets its OWN virtual display, and the logical display id is # re-assigned on every boot (0, then 3, then 6, then 8, then 11 across one # session). `adb shell input tap` without `-d` goes to display 0, i.e. the # launcher — taps appear to do nothing while the app is visibly on screen. # Every input call below resolves the app's current display and passes -d. # # 2. `screencap` cannot capture the app's display: the compositor refuses with # "Status: -2" for MuMu's app displays. Screenshots therefore come from the # host window (see the CUA path) or from `screencap -a`; this script shells # out to the window capture for `shot`. set -uo pipefail MUMU="${MUMU_HOME:-/c/Program Files/Netease/MuMu}" ADB="$MUMU/nx_main/adb.exe" MANAGER="$MUMU/nx_main/MuMuManager.exe" SERIAL="${MUMU_SERIAL:-127.0.0.1:16384}" PKG="com.dsh.sync" APK="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/data/dist/dsh-sync.apk" # Git Bash rewrites /sdcard/... into a Windows path; this disables that. export MSYS_NO_PATHCONV=1 adb_() { "$ADB" -s "$SERIAL" "$@"; } ensure_serial() { if ! adb_ get-state >/dev/null 2>&1; then "$ADB" connect "$SERIAL" >/dev/null 2>&1 fi } # Logical display currently hosting our activity, or empty. # # The imeInputTarget line names whichever window holds the input focus, so it # must be filtered down to our own package first — otherwise the launcher's # line matches and every tap lands on display 0. app_display() { adb_ shell dumpsys window windows 2>/dev/null | tr -d '\r' | grep -E "imeInputTarget in display#" | grep -F "$PKG" | grep -oE "display# ?[0-9]+" | grep -oE "[0-9]+$" | head -1 } require_app() { local d; d="$(app_display)" if [ -z "$d" ]; then echo "app is not on screen — run: $0 start" >&2 exit 1 fi printf '%s' "$d" } case "${1:-}" in start) "$MANAGER" control -v 0 launch >/dev/null 2>&1 for i in $(seq 1 40); do sleep 5 ensure_serial if [ "$(adb_ shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; then echo "booted after $((i * 5))s" # A stale proxy silently blackholes every WebView request (Chromium # honours the system proxy; netcat does not, so the network looks fine). adb_ shell settings delete global http_proxy >/dev/null 2>&1 adb_ shell settings delete global global_http_proxy_host >/dev/null 2>&1 adb_ shell settings delete global global_http_proxy_port >/dev/null 2>&1 echo "proxy cleared" exit 0 fi done echo "boot timed out" >&2; exit 1 ;; install) [ -f "$APK" ] || { echo "missing $APK — run android/build.sh first" >&2; exit 1; } adb_ install -r "$APK" | tail -1 adb_ shell am start -n "$PKG/.MainActivity" >/dev/null echo launched ;; deeplink) # Explicit component: Android only offers a bare http URL to browsers, so an # implicit VIEW intent never reaches this app. adb_ shell am start -n "$PKG/.MainActivity" \ -a android.intent.action.VIEW -d "$2" >/dev/null echo "sent $2" ;; tap) d="$(require_app)" adb_ shell input -d "$d" tap "$2" "$3" ;; text) d="$(require_app)" adb_ shell input -d "$d" text "$2" ;; key) d="$(require_app)" adb_ shell input -d "$d" keyevent "$2" ;; swipe) d="$(require_app)" adb_ shell input -d "$d" swipe "$2" "$3" "$4" "$5" "${6:-300}" ;; ui) # dumpsys window gives the WebView's text without needing a screenshot. d="$(require_app)" adb_ shell uiautomator dump //sdcard/_ui.xml >/dev/null 2>&1 adb_ shell cat //sdcard/_ui.xml 2>/dev/null | tr '>' '\n' | grep -oE 'text="[^"]+"' | sed 's/text=//' | tr -d '"' ;; shot) # screencap refuses MuMu's app displays, so capture through the host window. out="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/_mumu_shot.png}" "$MUMU/nx_main/MuMuManager.exe" >/dev/null 2>&1 powershell -NoProfile -Command " Add-Type -AssemblyName System.Drawing,System.Windows.Forms \$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds \$bmp = New-Object System.Drawing.Bitmap \$b.Width, \$b.Height \$g = [System.Drawing.Graphics]::FromImage(\$bmp) \$g.CopyFromScreen(\$b.Location, [System.Drawing.Point]::Empty, \$b.Size) \$bmp.Save('$out') " >/dev/null 2>&1 [ -s "$out" ] && echo "$out" || { echo "capture failed" >&2; exit 1; } ;; status) ensure_serial echo "serial : $SERIAL" echo "boot : $(adb_ shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" echo "app display : $(app_display)" echo "app pid : $(adb_ shell pidof $PKG 2>/dev/null | tr -d '\r')" echo "proxy : $(adb_ shell settings get global http_proxy 2>/dev/null | tr -d '\r')" echo "foreground : $(adb_ shell dumpsys activity activities 2>/dev/null | tr -d '\r' | grep -m1 topResumedActivity | sed 's/.*u0 //')" ;; log) adb_ logcat -d -t "${2:-80}" 2>/dev/null | grep -iE "dsh|chromium|AndroidRuntime|FATAL" | tail -25 ;; *) sed -n '2,22p' "$0" ;; esac