#!/bin/bash
# Find an Android phone's current wireless-ADB port via mDNS, connect, and mirror it with scrcpy.
# The port changes every time Wireless debugging is toggled, so it's looked up fresh each run.
# The phone to use is remembered in ~/.config/frame-tweaks/phone.conf (PHONE_NAME="My phone"): taken by itself
# when only one is advertising, chosen from a list when there are several (phone --pick: choose
# again). When it isn't in sight you can wait for it or use another one.
# Extra args are passed to scrcpy (e.g. phone --turn-screen-off).
export PATH="$HOME/.local/bin:/home/linuxbrew/.linuxbrew/bin:$PATH"
# Translations (lib/i18n.sh; the copy install_i18n puts here), else English
# shellcheck source=/dev/null
source "$HOME/.local/share/frame-tweaks/i18n/i18n.sh" 2>/dev/null ||
    t() { local f=$1; shift; if [ $# -eq 0 ]; then printf '%s' "$f"; else printf -- "$f" "$@"; fi; }
CONF="$HOME/.config/frame-tweaks/phone.conf"
# (before 0.6 it was ~/.config/phone.conf: moved here once)
if [ ! -f "$CONF" ] && [ -f "$HOME/.config/phone.conf" ]; then
    mkdir -p "${CONF%/*}" && mv "$HOME/.config/phone.conf" "$CONF"
fi
# shellcheck source=/dev/null
[ -f "$CONF" ] && . "$CONF"

# advertised: the phones on the Wi-Fi with Wireless debugging on, one mDNS line each. (A look
# takes a second or two; avahi-browse once went on waiting for an answer for a day, and this with it.)
advertised() { timeout 10 avahi-browse -rtp _adb-tls-connect._tcp 2>/dev/null | grep '^=' | grep ';IPv4;'; }
names_in() { sed -n 's/.*"name=\([^"]*\)".*/\1/p' | sort -u; }
# find_phone: sets dev=address:port of the chosen phone (PHONE_NAME, the whole name: "Pixel 9"
# isn't "Pixel 9 Pro"), or returns 1.
find_phone() {
    all=$(advertised)
    c=$(echo "$all" | grep -F "\"name=${PHONE_NAME:-}\"" | head -1)
    [ -n "$c" ] || return 1
    dev="$(echo "$c" | cut -d';' -f8):$(echo "$c" | cut -d';' -f9)"
}
# remember NAME: the phone to use from now on (PHONE_NAME in phone.conf; other lines kept).
remember() {
    PHONE_NAME=$1
    mkdir -p "${CONF%/*}" && touch "$CONF"
    sed -i '/^[[:space:]]*PHONE_NAME=/d' "$CONF"
    printf 'PHONE_NAME=%q\n' "$PHONE_NAME" >> "$CONF"
}
# ask TITLE TEXT TAG LABEL...: a list to choose from (kdialog, on the display you're using);
# prints the chosen tag; returns 1 if closed, 2 without kdialog.
ask() {
    local title=$1 text=$2; shift 2
    command -v kdialog >/dev/null || return 2
    kdialog --title "$title" --menu "$text" "$@" 2>/dev/null
}
# Its dialogs' title. Never the mirror window's own ("Android Mirror", which the VR dashboard's
# patches go by): a dialog with that title was once taken for the mirror after a stream restart,
# moved to where the mirror floated, and left over it, so its handle couldn't be grabbed.
DLG=$(t 'Android Mirror setup')
# The splash: a small window with what's happening, from starting until the mirror shows
# (notifications alone go unseen in VR); a failure replaces it with a message saying why.
# kdialog's progress dialog, steered over D-Bus. PHONE_NO_SPLASH=1: none.
SPLASH=()
splash() {                          # splash TEXT: show it, or change its text
    if [ ${#SPLASH[@]} -eq 0 ]; then
        [ -z "${PHONE_NO_SPLASH:-}" ] && command -v kdialog >/dev/null && command -v qdbus6 >/dev/null || return 0
        splash_leftovers
        read -ra SPLASH <<< "$(kdialog --title "$DLG" --progressbar "$1" 0 2>/dev/null)"
        [ ${#SPLASH[@]} -eq 2 ] && qdbus6 "${SPLASH[@]}" showCancelButton true >/dev/null 2>&1
    else
        qdbus6 "${SPLASH[@]}" setLabelText "$1" >/dev/null 2>&1
    fi
}
# splash_end: close it; if it doesn't answer, stop its process (its D-Bus name ends in the pid).
splash_end() {
    [ ${#SPLASH[@]} -eq 2 ] || { SPLASH=(); return 0; }
    local pid=${SPLASH[0]##*-}
    qdbus6 "${SPLASH[@]}" close >/dev/null 2>&1
    SPLASH=()
    for _ in 1 2 3 4; do kill -0 "$pid" 2>/dev/null || return 0; sleep 0.25; done
    [[ $pid =~ ^[0-9]+$ ]] && grep -qs kdialog "/proc/$pid/cmdline" && kill "$pid" 2>/dev/null
    return 0
}
# splash_leftovers: a splash this script lost track of (a stream restart arriving while one was
# opening): ours by its title, before a new one opens.
splash_leftovers() {
    local pid
    for pid in $(pgrep -u "$(id -u)" -f '^/usr/bin/kdialog_progress_helper ' 2>/dev/null); do
        tr '\0' '\n' < "/proc/$pid/cmdline" 2>/dev/null | grep -qxF -- "$DLG" && kill "$pid" 2>/dev/null
    done
    return 0
}
cancelled() { [ ${#SPLASH[@]} -eq 2 ] && [ "$(qdbus6 "${SPLASH[@]}" wasCancelled 2>/dev/null)" = true ]; }
# fail TITLE TEXT: say why it stopped (a dialog, and a notification), then stop.
fail() {
    splash_end
    notify-send -a Phone "$1" "$2"
    command -v kdialog >/dev/null && kdialog --title "$(t 'Android Mirror: %s' "$1")" --error "$2" 2>/dev/null
    exit 1
}
trap 'splash_end' EXIT
# pair_phone ADDRESS: pair the Frame with the phone again. The phone's "Pair device with pairing
# code" dialog advertises its pairing port on the Wi-Fi (mDNS), so only the code is asked for.
pair_phone() {
    local ip=${1%:*} where="" code out
    splash "$(t 'On the phone: Developer options → Wireless debugging → Pair device with pairing code. Waiting for it to show a code…')"
    for _ in $(seq 60); do
        where=$(timeout 10 avahi-browse -rtp _adb-tls-pairing._tcp 2>/dev/null | grep '^=' | grep ';IPv4;' |
                awk -F';' -v ip="$ip" '$8 == ip {print $8 ":" $9; exit}')
        [ -n "$where" ] && break
        cancelled && exit 0
        sleep 2
    done
    splash_end
    [ -n "$where" ] || fail "$(t "The phone didn't offer pairing")" "$(t 'Nothing showed a pairing code within two minutes. On the phone: Developer options → Wireless debugging → Pair device with pairing code, then open Android Mirror again.')"
    code=$(kdialog --title "$(t 'Android Mirror: pair with %s' "${PHONE_NAME:-$(t 'the phone')}")" --inputbox "$(t 'The six-digit pairing code the phone shows:')" 2>/dev/null) || exit 0
    # (the code on adb's input, which asks for it: not on its command line, which any program can read)
    out=$(printf '%s\n' "$code" | adb pair "$where" 2>&1); out=${out#Enter pairing code: }
    grep -qi "successfully paired" <<< "$out" || fail "$(t "Pairing didn't work")" "$(t "adb said: %s

Check the code and try again (a new code appears each time the phone's pairing dialog opens)." "$out")"
}
# wait_for NAME: up to 3 minutes for it to show up (Wireless debugging turned on, back on the Wi-Fi).
wait_for() {
    splash "$(t 'Waiting for %s: turn on Wireless debugging on it (Developer options), on the same Wi-Fi. Up to 3 minutes.' "$1")"
    for _ in $(seq 36); do
        find_phone && return 0
        cancelled && exit 0
        sleep 5
    done
    fail "$(t "%s didn't appear" "$1")" "$(t "It didn't turn up on the Wi-Fi with Wireless debugging on. Turn that on in the phone's Developer options (main profile, same Wi-Fi as the Frame), then open Android Mirror again.")"
}
if [ "${1:-}" = --forget ]; then     # phone --forget: no remembered phone any more
    [ -f "$CONF" ] && sed -i '/^[[:space:]]*PHONE_NAME=/d' "$CONF"
    t 'Forgot %s. Next time, Android Mirror takes the phone in sight, or asks which if there are several.' "${PHONE_NAME:-$(t 'the remembered phone')}"; echo
    exit 0
fi
pick=; [ "${1:-}" = --pick ] && { pick=1; shift; }       # phone --pick: choose the phone again
splash "$(t 'Looking for your phone on the Wi-Fi…')"
names=$(advertised | names_in)
count=$(printf '%s\n' "$names" | grep -c .)
if [ -n "$pick" ] || [ -z "${PHONE_NAME:-}" ]; then
    # No phone chosen yet (or choosing again): one in sight is taken (and remembered); several, you choose.
    if [ "$count" -eq 0 ]; then
        fail "$(t 'No phone found')" "$(t "No phone with Wireless debugging on is on the Wi-Fi. Turn it on in the phone's Developer options (main profile, same Wi-Fi as the Frame). First time? Pair it once: see Android Mirror's README.")"
    elif [ "$count" -eq 1 ] && [ -z "$pick" ]; then
        remember "$names"
        notify-send -a Phone "$(t 'Android Mirror')" "$(t "Connecting to %s from now on. To choose another: Android Mirror's \"Choose phone…\" (or phone --pick)." "$PHONE_NAME")"
    else
        mapfile -t list <<< "$names"
        args=(); for n in "${list[@]}"; do args+=("$n" "$n"); done
        splash_end
        chosen=$(ask "$DLG" "$(t 'Which phone?')" "${args[@]}")
        [ -n "$chosen" ] || { [ -z "${PHONE_NAME:-}" ] && notify-send -a Phone "$(t 'Which phone?')" "$(t 'Several are advertising: %s. Run: phone --pick' "$(printf '%s' "$names" | paste -sd, - | sed 's/,/, /g')")"; exit 1; }
        remember "$chosen"
    fi
fi
if ! find_phone; then
    # The chosen phone isn't in sight: wait for it, or use one that is.
    mapfile -t list <<< "$names"
    # (no other phone in sight, or no kdialog: just wait)
    args=(@wait "$(t 'Wait for %s' "$PHONE_NAME")"); for n in "${list[@]}"; do [ -n "$n" ] && args+=("$n" "$(t 'Use %s instead' "$n")"); done
    if [ "$count" -eq 0 ]; then chosen=@wait; else splash_end; chosen=$(ask "$DLG" "$(t "%s isn't on the Wi-Fi with Wireless debugging on." "$PHONE_NAME")" "${args[@]}"); r=$?; [ $r -eq 2 ] && chosen=@wait; fi
    case "$chosen" in
        "") exit 1 ;;                                       # closed: do nothing
        @wait) wait_for "$PHONE_NAME" || exit 1 ;;
        *) remember "$chosen"; find_phone || fail "$(t '%s not found' "$chosen")" "$(t 'It stopped showing up on the Wi-Fi. Check its Wireless debugging is still on, then try again.')" ;;
    esac
fi
# With "Memory caps" installed, scrcpy runs inside its memory limit.
cap=(); [ -x "$HOME/.local/bin/capped" ] && cap=("$HOME/.local/bin/capped" scrcpy --)
MAX=1920                                   # the window: fits the Steam session's 1920x1080 screen
# The stream: no larger than VR shows it (the upright window is 1080 tall). scrcpy already shows
# only the newest picture, so a delay is pictures still on their way: the wireless connection
# (TCP, which can't drop a late one) and the phone's encoder. Less data per second, less backlog.
# Set any of these in ~/.config/frame-tweaks/phone.conf.
STREAM_MAX=${PHONE_MAX_SIZE:-1024} BIT_RATE=${PHONE_BIT_RATE:-2M} MAX_FPS=${PHONE_MAX_FPS:-60}
# The clipboard with the phone (PHONE_CLIPBOARD): scrcpy shares it unless told not to. Shared, the
# Frame's goes to the phone when you paste in the mirror, and the phone's comes here whenever it
# copies (then on to the desktop and VR with the Shared clipboard tweak).
clipboard() { [ "${PHONE_CLIPBOARD:-both}" = off ] && echo --no-clipboard-autosync; return 0; }

# A wireless connection can die without scrcpy noticing (the phone changed network or address,
# e.g. after switching profile): the picture just freezes. Check the phone answers every 5 s;
# after 3 misses, close the frozen mirror and reconnect at the phone's current address.
alive() { timeout 4 adb -s "$dev" shell true >/dev/null 2>&1; }
# The mirror's gear panel (in VR) changes the settings above and asks for a restart (SIGUSR1):
# scrcpy starts again, reading ~/.config/frame-tweaks/phone.conf afresh. (In the session's
# runtime folder, like adb's socket. The tests give it their own: their runs once removed the
# pid file of a mirror that was running.)
RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
PIDFILE="$RUN/sft-phone.pid"
echo $$ > "$PIDFILE"
trap 'splash_end; [ "$(cat "$PIDFILE" 2>/dev/null)" = $$ ] && rm -f "$PIDFILE"' EXIT
restart=
trap 'restart=1; kill "$pid" 2>/dev/null' USR1
SCRCPY_LOG="$RUN/phone-scrcpy.log"                         # what scrcpy said, for a failure message
first=1
while :; do
    if [ -n "$restart" ]; then
        restart=
        # (a setting no longer in the file, after the gear's Reset to defaults say, is back to its
        # default: reading the file again can't unset what it doesn't say)
        unset PHONE_MAX_SIZE PHONE_BIT_RATE PHONE_MAX_FPS PHONE_STICK_SPEED PHONE_CLIPBOARD
        # shellcheck source=/dev/null
        [ -f "$CONF" ] && . "$CONF"
        STREAM_MAX=${PHONE_MAX_SIZE:-1024} BIT_RATE=${PHONE_BIT_RATE:-2M} MAX_FPS=${PHONE_MAX_FPS:-60}
    fi
    splash "$(t 'Connecting to %s…' "${PHONE_NAME:-$(t 'the phone')}")"
    out=$(adb connect "$dev" 2>&1)
    if [ -n "$first" ] && ! grep -q "connected to" <<< "$out"; then
        # Often: the phone forgot the Frame (or never knew it). Offer to pair again, then retry.
        splash_end
        why=$(t "Couldn't connect to %s. adb said: %s

If the phone forgot the Frame (or it's a new phone), pair them again." "${PHONE_NAME:-$(t 'the phone')}" "$out")
        choice=$(ask "$DLG" "$why" pair "$(t 'Pair it again')" forget "$(t 'Forget %s' "${PHONE_NAME:-$(t 'this phone')}")" close "$(t 'Close')"); r=$?
        [ $r -eq 2 ] && fail "$(t "Couldn't connect to %s" "${PHONE_NAME:-$(t 'the phone')}")" "$why $(t "See Android Mirror's README for adb pair.")"
        if [ "$choice" = forget ]; then          # stop connecting to it by default (phone --forget)
            [ -f "$CONF" ] && sed -i '/^[[:space:]]*PHONE_NAME=/d' "$CONF"
            kdialog --title "$DLG" --msgbox "$(t 'Forgot %s. Next time, Android Mirror takes the phone in sight, or asks which if there are several.' "${PHONE_NAME:-$(t 'the phone')}")" 2>/dev/null
            exit 0
        fi
        [ "$choice" = pair ] || exit 1
        pair_phone "$dev"
        splash "$(t 'Connecting to %s…' "${PHONE_NAME:-$(t 'the phone')}")"
        out=$(adb connect "$dev" 2>&1)
        grep -q "connected to" <<< "$out" || fail "$(t "Couldn't connect to %s" "${PHONE_NAME:-$(t 'the phone')}")" "$(t 'Paired, but connecting still failed. adb said: %s' "$out")"
    fi
    # In VR, shape the window like the phone (upright or sideways) and follow its rotation;
    # without it the Steam session makes the window 1920x1080, with black bars beside a portrait
    # phone. (It ends with scrcpy, so it's started again with each reconnect.)
    # The Steam session puts laser clicks on so narrow a window at its left edge: phone-touch
    # takes them from a layer over the window instead and sends them to the phone as touches.
    # (Desktop mode or no VR environment helper: it just exits. A reconnect replaces it.) With a
    # scrcpy it wasn't tested with, or with its layer's patch switched off by vr-dashboard after a
    # SteamVR update (a line in its status file), neither runs: a plain 16:9 window, where clicks
    # land right.
    extras=1
    if [ -x "$HOME/.local/bin/phone-touch" ] && [ "${DISPLAY:-}" = ":0" ]; then
        "$HOME/.local/bin/phone-touch" --check 2>/dev/null || extras=
        grep -qs '^phone-touch:' "$RUN/sft-dashboard.status" && extras=
    fi
    if [ -n "$extras" ] && [ -x "$HOME/.local/bin/phone-fit" ] && ! pgrep -u "$(id -u)" -f "bin/phone-fit" >/dev/null; then
        setsid -f "$HOME/.local/bin/phone-fit" "$MAX" >/dev/null 2>&1 </dev/null
    fi
    if [ -n "$extras" ] && [ -x "$HOME/.local/bin/phone-touch" ] && [ "${DISPLAY:-}" = ":0" ]; then
        setsid -f "$HOME/.local/bin/phone-touch" "$dev" >/dev/null 2>&1 </dev/null
    fi
    # The title is how the dashboard patches recognise the mirror window.
    splash "$(t 'Starting the mirror…')"
    # shellcheck disable=SC2046   # (one word or none)
    "${cap[@]}" scrcpy -s "$dev" --video-bit-rate="$BIT_RATE" --max-size="$STREAM_MAX" --max-fps="$MAX_FPS" \
        $(clipboard) --window-title="Android Mirror" "$@" >"$SCRCPY_LOG" 2>&1 &
    pid=$! misses=0
    # The splash stays until the mirror has been up a moment (after a reconnect or a restart too).
    for _ in 1 2 3 4 5 6; do sleep 0.5; kill -0 "$pid" 2>/dev/null || break; done
    if [ -n "$first" ]; then
        if ! kill -0 "$pid" 2>/dev/null && wait "$pid"; then exit 0; fi       # closed straight away
        if ! kill -0 "$pid" 2>/dev/null; then
            fail "$(t "The mirror didn't start")" "$(t 'scrcpy stopped straight away. It said:

%s

If the phone asked to allow USB debugging or screen sharing, allow it and try again.' "$(tail -n 6 "$SCRCPY_LOG")")"
        fi
        first=
    fi
    splash_end
    while kill -0 "$pid" 2>/dev/null; do
        sleep 5 & wait $!                      # (a restart request interrupts the wait)
        [ -n "$restart" ] && break
        if alive; then misses=0; else misses=$((misses + 1)); fi
        [ "$misses" -ge 3 ] && break
    done
    if [ -n "$restart" ]; then
        kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
        continue                               # same phone, new settings
    fi
    if ! kill -0 "$pid" 2>/dev/null; then
        wait "$pid"; code=$?
        # Closed by you (or the phone was unplugged/turned off on purpose): done. scrcpy also
        # stops by itself when it notices the connection dropped: reconnect then.
        [ "$code" -eq 0 ] || alive && exit "$code"
    else
        kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null       # frozen
    fi
    adb disconnect "$dev" >/dev/null 2>&1
    notify-send -a Phone "$(t 'Phone connection lost')" "$(t 'Reconnecting…')"
    splash "$(t 'The connection to %s was lost. Reconnecting…' "${PHONE_NAME:-$(t 'the phone')}")"
    found=
    for _ in $(seq 12); do                             # up to a minute
        if find_phone && adb connect "$dev" >/dev/null && alive; then found=1; break; fi
        cancelled && exit 0
        sleep 5
    done
    splash_end
    if [ -z "$found" ]; then
        fail "$(t 'Phone not found')" "$(t "The connection to %s was lost and it didn't come back within a minute. Is Wireless debugging still on, on the same Wi-Fi? Open Android Mirror again when it is." "${PHONE_NAME:-$(t 'the phone')}")"
    fi
done
