diff --git a/Scripts/android-signin.sh b/Scripts/android-signin.sh new file mode 100755 index 0000000..d6bf7d2 --- /dev/null +++ b/Scripts/android-signin.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# Sign IN on a booted Android emulator, through the app's own UI. +# +# FN_PASS=… Scripts/android-signin.sh relay.flamenet.io fnreviewbeta +# +# The sibling of `android-signup.sh`, and it exists for the same reason: the +# Android half of a cross-platform check has to be driven by something, and +# driving it by hand is where the mistakes live. Sign-IN specifically, because +# signing up on the live relay leaves a real account behind and spends the +# account service's three-per-hour-per-IP budget. +# +# 🔴 **Three traps, all of which cost me an hour on 2026-09-05, all encoded +# below. None is obvious and every one fails SILENTLY.** +# +# 1. `input text` drops characters on longer strings. A 21-character +# passphrase arrived as 11. Nothing reports it; the sign-in simply fails +# as though the password were wrong. So: chunks, and read back the count. +# +# 2. The soft keyboard covers the button. A tap at the button's coordinates +# lands on a KEY instead — which types a character into the field you just +# filled, so the passphrase grows by one every time you "press" sign in. +# So: dismiss the keyboard first, and verify the field afterwards. +# +# 3. Dismissing the keyboard MOVES the button. Coordinates read before the +# dismissal are stale by the time you tap them, and the tap lands on +# nothing at all — no error, no network, no clue. So: re-dump immediately +# before every tap, never reuse a position across an interaction. +# +# ⚠️ Prints FAIL with a reason, never a bare non-zero. A driver that fails +# quietly is worse than no driver: the run continues against a screen nobody +# looked at. +set -uo pipefail +ADB=~/Library/Android/sdk/platform-tools/adb +HOST="${1:?usage: android-signin.sh }" +HANDLE="${2:?usage: android-signin.sh }" +# 🔴 Not in the repo, and not defaulted. This repository is public, and a +# default here would be a live passphrase anybody can read (ios#62). +PASS="${FN_PASS:?set FN_PASS}" +PKG=io.flamenet.messenger + +dump() { $ADB shell uiautomator dump /sdcard/s.xml >/dev/null 2>&1; $ADB shell cat /sdcard/s.xml 2>/dev/null; } + +# Centre of the first node whose text matches, from a FRESH dump. Trap 3. +tap_text() { + local want="$1" d b x y + d=$(dump) + b=$(printf '%s' "$d" | tr '<' '\n<' | grep "text=\"$want\"" \ + | grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' | head -1) + [ -z "$b" ] && return 1 + x=$(printf '%s' "$b" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\1 \3/' | awk '{print int(($1+$2)/2)}') + y=$(printf '%s' "$b" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\2 \4/' | awk '{print int(($1+$2)/2)}') + $ADB shell input tap "$x" "$y" >/dev/null 2>&1 + sleep 2 +} + +# Tap the control carrying a given label, and refuse to tap a moving target. +# +# 🔴 **Trap 3, in its worst form.** A fresh dump is not enough on a slow +# emulator: the layout can still be settling when the tap is issued, and the +# tap then lands on whatever slid into that position. That is not theoretical — +# it put this driver on the SIGN-UP form, one field away from creating an +# account on the production relay, while every coordinate it had read was +# correct at the moment it read them. +# +# So: read the bounds twice, a second apart, and only tap when they agree. +# A control that is still moving is a control this driver refuses to touch. +tap_label() { + local want="$1" a b x y + bounds_of() { + dump | tr '<' '\n<' | grep "text=\"$1\"" \ + | grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' | head -1 + } + a=$(bounds_of "$want"); sleep 1; b=$(bounds_of "$want") + [ -z "$a" ] && { echo "FAIL '$want' is not on screen"; return 1; } + [ "$a" != "$b" ] && { echo "FAIL '$want' is still moving — refusing to tap"; return 1; } + x=$(printf '%s' "$a" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\1 \3/' | awk '{print int(($1+$2)/2)}') + y=$(printf '%s' "$a" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\2 \4/' | awk '{print int(($1+$2)/2)}') + $ADB shell input tap "$x" "$y" >/dev/null 2>&1 + sleep 2 +} + +# Kept for screens whose control has no label of its own. +tap_button() { + local index="$1" d + d=$(dump) + printf '%s' "$d" > /tmp/fn-andui.xml + local xy + xy=$(python3 - "$index" <<'PY' +import re, sys +idx = int(sys.argv[1]) +s = open('/tmp/fn-andui.xml', encoding='utf-8', errors='replace').read() +hits = [] +for n in re.findall(r']*>', s): + if 'clickable="true"' not in n or 'EditText' in n: + continue + b = re.search(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', n) + if b: + hits.append(((int(b.group(1)) + int(b.group(3))) // 2, + (int(b.group(2)) + int(b.group(4))) // 2)) +print(f"{hits[idx][0]} {hits[idx][1]}" if idx < len(hits) else "") +PY +) + [ -z "$xy" ] && return 1 + $ADB shell input tap $xy >/dev/null 2>&1 + sleep 2 +} + +# How many characters are actually in the passphrase field. Trap 1. +dots() { + dump > /tmp/fn-andui.xml + python3 -c " +import re +s = open('/tmp/fn-andui.xml', encoding='utf-8', errors='replace').read() +m = re.findall(r'text=\"(•+)\"', s) +print(len(m[0]) if m else 0)" +} + +# Type into the field at a text label, in chunks, verified. Traps 1 and 3. +type_into() { # label, text, verify-substring + local label="$1" text="$2" want="$3" + tap_text "$label" || { echo "FAIL no '$label' field on screen"; exit 1; } + $ADB shell input keyevent KEYCODE_MOVE_END >/dev/null 2>&1 + for _ in $(seq 1 40); do $ADB shell input keyevent 67 >/dev/null 2>&1; done + local i=0 len=${#text} + while [ "$i" -lt "$len" ]; do + $ADB shell input text "${text:$i:7}" >/dev/null 2>&1 + i=$((i + 7)); sleep 1 + done + sleep 1 + dump | grep -q "$want" || { echo "FAIL '$label' did not read back as expected"; exit 1; } +} + +$ADB shell am force-stop "$PKG" >/dev/null 2>&1 +$ADB shell pm clear "$PKG" >/dev/null 2>&1 +$ADB shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1 +sleep 12 + +# Runtime permission prompts, if the image asks. +for _ in 1 2 3; do tap_text "Allow" || break; done + +D=$(dump) +printf '%s' "$D" | grep -q 'text="Handle"' || { echo "FAIL no sign-in screen"; exit 1; } + +type_into "Server" "$HOST" "$HOST" +type_into "Handle" "$HANDLE" "$HANDLE" + +# The passphrase, verified by LENGTH — its text is bullets, so there is +# nothing else to compare against. +tap_text "Passphrase" || { echo "FAIL no passphrase field"; exit 1; } +$ADB shell input keyevent KEYCODE_MOVE_END >/dev/null 2>&1 +for _ in $(seq 1 40); do $ADB shell input keyevent 67 >/dev/null 2>&1; done +i=0 +while [ "$i" -lt "${#PASS}" ]; do + $ADB shell input text "${PASS:$i:7}" >/dev/null 2>&1 + i=$((i + 7)); sleep 1 +done +sleep 1 +got=$(dots) +[ "$got" = "${#PASS}" ] || { echo "FAIL passphrase arrived as $got of ${#PASS} characters"; exit 1; } + +# Trap 2, then trap 3: put the keyboard away, let the layout settle, and only +# then find the button. +$ADB shell input keyevent 4 >/dev/null 2>&1 +sleep 3 +after=$(dots) +[ "$after" = "${#PASS}" ] || { echo "FAIL dismissing the keyboard changed the field ($after chars)"; exit 1; } + +# ⚠️ By LABEL, never by index. An index picked "Create an account" here and +# walked onto the sign-up form on the live relay. +tap_label "Sign in" || exit 1 + +for _ in $(seq 1 30); do + D=$(dump) + printf '%s' "$D" | grep -qE 'text="(Contacts|Messages|Chats)"' && { echo "$HOST $HANDLE OK"; exit 0; } + printf '%s' "$D" | grep -qi "too many" && { echo "$HOST $HANDLE SKIP throttled"; exit 0; } + printf '%s' "$D" | grep -qi "check your" && { echo "$HOST $HANDLE FAIL credentials refused"; exit 1; } + sleep 3 +done +echo "$HOST $HANDLE FAIL did-not-complete" +exit 1