#!/usr/bin/env bash # Device helpers for driving the app over adb. # # TCL's AppBootManagerService sometimes refuses to let the app bind the # WebView's renderer process: # # W/AppBootManager: Result[false]; Target[...SandboxedProcessService0]; # Caller[dev.ncode.sp404layers]; Reason[screen_on & caller care] # E/cr_ChildProcessConn: Failed to establish the service connection. # # With no renderer the page never parses: no crash, no JS error, an empty # document.title, and DevTools Runtime.evaluate hangs. It is sticky for the life # of the process, so reopening from Recents keeps the blank window. # # Observed trigger is the window just after `adb install -r` replaces the # package - every failure followed an install, and an immediate relaunch with no # install has always succeeded. The Reason string reports the current screen # state ("screen_on" and "screen_off" have both been seen denying), so screen # state is not the cause. Hence: launch retries the whole cycle. set -euo pipefail ADB="${ADB:-$HOME/Android/Sdk/platform-tools/adb}" PKG=dev.ncode.sp404layers ACT="$PKG/.MainActivity" PORT=9333 ATTEMPTS=3 # Not the cause of the denial, but a locked phone cannot be checked visually # and input keyevents will not reach the app. ensure_awake() { "$ADB" shell input keyevent KEYCODE_WAKEUP >/dev/null 2>&1 || true "$ADB" shell input keyevent 82 >/dev/null 2>&1 || true local wake lock wake=$("$ADB" shell dumpsys power | grep -m1 -o 'mWakefulness=[A-Za-z]*' | cut -d= -f2 | tr -d '\r') lock=$("$ADB" shell dumpsys window | grep -m1 -o 'mDreamingLockscreen=[a-z]*' | cut -d= -f2 | tr -d '\r') if [ "$wake" != "Awake" ] || [ "$lock" = "true" ]; then echo "" >&2 echo " #############################################################" >&2 echo " # UNLOCK THE PHONE #" >&2 echo " # wakefulness=$wake lockscreen=$lock" >&2 echo " # Nothing on screen can be verified while it is locked. #" >&2 echo " #############################################################" >&2 echo "" >&2 return 1 fi echo "device awake and unlocked" } keep_awake() { # `usb` writes stay_on_while_plugged_in=2 (USB only) and this phone charges # over AC, so it silently never engages. `true` sets every charging-source bit: # measured as 15 here (AC|USB|wireless|dock), not the 7 that older notes claim. "$ADB" shell svc power stayon true >/dev/null echo "stay-awake while charging: ON (revert with: scripts/device.sh sleep-normal)" } sleep_normal() { "$ADB" shell svc power stayon false >/dev/null echo "stay-awake: reverted to system default" } # Release builds are not DEBUGGABLE, so Capacitor never calls # setWebContentsDebuggingEnabled and the devtools socket is never opened. Asking # for it anyway reports "renderer denied" for a perfectly healthy app. is_debuggable() { "$ADB" shell dumpsys package "$PKG" | grep -q DEBUGGABLE } # Device-side clock, for bounding logcat to this launch. The format has to be # quoted as one argument: toybox date rejects a bare `+FMT` after `adb shell`. device_now() { "$ADB" shell "date +'%m-%d %H:%M:%S.000'" | tr -d '\r' } # Did the renderer get denied since $1? These lines are emitted whether or not # the build is debuggable, and the package filter matters - other apps are # denied constantly and their lines mean nothing here. denied_since() { "$ADB" logcat -d -T "$1" 2>/dev/null \ | grep -E "AppBootManager.*Caller\[$PKG\]|cr_ChildProcessConn: Failed" \ | grep -v 'Result\[true\]' } # Wait until the page is actually parsed, not merely until the activity is up: # the activity starts fine even when the renderer was denied. page_loaded() { local pid sock i # Read the pid AFTER starting. `adb install -r` kills the old process, so a pid # captured earlier points at a dead one and the socket lookup fails while the # app itself is healthy - that produced one false "page never loaded". pid=$("$ADB" shell pidof "$PKG" | tr -d '\r') [ -n "$pid" ] || return 1 # Release build: the page cannot be inspected from here at all, so prove what # can be proved - the process survived and nothing denied its renderer. A # blank grey window is indistinguishable from a working one over adb, which is # why this says "assumed" rather than claiming the page loaded. if ! is_debuggable; then sleep 2 [ -n "$("$ADB" shell pidof "$PKG" | tr -d '\r')" ] || return 1 [ -z "$(denied_since "$1")" ] || return 1 echo "app running (pid $pid, release build: page load assumed, not verified)" return 0 fi for i in $(seq 1 40); do sock=$("$ADB" shell cat /proc/net/unix | grep -o "webview_devtools_remote_$pid" | head -1) if [ -n "$sock" ]; then "$ADB" forward "tcp:$PORT" "localabstract:$sock" >/dev/null 2>&1 || true if curl -s --max-time 1 "http://127.0.0.1:$PORT/json" | grep -q 'SP-404'; then echo "app loaded (pid $pid, $i polls)" return 0 fi fi done return 1 } launch() { ensure_awake || return 1 local attempt started for attempt in $(seq 1 $ATTEMPTS); do started=$(device_now) "$ADB" shell am force-stop "$PKG" "$ADB" shell am start -W -n "$ACT" >/dev/null if page_loaded "$started"; then [ "$attempt" -gt 1 ] && echo "(took $attempt attempts - post-install renderer denial)" return 0 fi echo "attempt $attempt: no page, retrying" >&2 done echo "ERROR: page never loaded after $ATTEMPTS attempts." >&2 # Bounded to this launch and to our package. Unfiltered, this printed other # apps' denials from minutes earlier and sent one debugging session chasing a # denial that had never happened to us. denied_since "$started" | tail -4 >&2 return 1 } case "${1:-}" in awake) ensure_awake ;; keep-awake) keep_awake ;; sleep-normal) sleep_normal ;; launch) launch ;; *) echo "usage: $0 {awake|keep-awake|sleep-normal|launch}" >&2; exit 2 ;; esac