An Android sign-in driver (ios#7) #203
Executable
+180
@@ -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 <host> <handle>}"
|
||||
HANDLE="${2:?usage: android-signin.sh <host> <handle>}"
|
||||
# 🔴 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'<node[^>]*>', 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
|
||||
Reference in New Issue
Block a user