#!/system/bin/sh
# Install every boot hook this device supports, so tb-supervisor.sh starts on
# every boot. Idempotent and safe to re-run — the supervisor calls it on each
# start to re-establish hooks an OTA may have wiped from /system.
#
# ⛔ SAFETY RULE (see CLAUDE.md "SAFETY RULES"). This script runs AUTOMATICALLY —
# the supervisor calls it on EVERY start. Therefore on SDK>=29 (TB40/TB50/TB60)
# it MUST NOT write /system, remount, disable verity, or patch init.rc: doing so
# bricked boxes (SYS off after reboot, USB-reflash only). The SDK>=29 branch
# below only READS and exits; every /system write is under the SDK<29 gate. Do
# NOT move a /system write above that exit, and do NOT add a modern-box overlay
# path here — it is retired (Magisk is the modern boot path, and it is manual).
#
# Layers. Which ones exist depends on the box, so we install every one that
# applies and let the supervisor's pid lock dedupe the overlap:
#   0.  /data/adb/service.d/90tb-agent.sh  — Magisk/KernelSU, root at boot.
#       Best on Android 11 when the box HAS Magisk: no /system write, no app
#       sandbox, no BOOT_COMPLETED. TB60 has no Magisk, hence 0c.
#   0b. accessibility autostart + doze whitelist + magisk su policy (SDK 24+).
#       Makes the APK layer viable on modern Android; skipped on TB2.
#   0c. a tb-agent service patched into the bootloader-first init.rc — Android's
#       OWN mechanism, root at boot. This is what covers TB40/TB60/sdk33: no
#       Magisk, and an app uid cannot exec their 04750 vendor su. It needs a
#       writable system partition, so on SDK>=29 this script ENABLES THE OVERLAY
#       ITSELF (/system/bin/remount) and asks for one reboot. That is deliberate:
#       the supervisor re-runs this script on every start, so a fleet-wide
#       update is enough — nobody has to type anything per box.
#   1.  /data/local/userinit.sh   — in /data, SURVIVES OTA, no root needed.
#       Android 11 init does NOT read it; kept for the older RK ROMs that do.
#   2.  install-recovery.sh hook  — /system, root, wiped by OTA (self-healed).
#   3.  init.d/90tb-agent         — /system, root, only if a loader exists.
#       Layers 2 and 3 are SDK<29 ONLY: Android 10+ init triggers neither, and
#       writing them costs bytes on a partition that has none to spare.
# Config (server/id/token) lives in /data/local/tmp/tb-agent.env (survives OTA),
# so no secrets are written into /system.
LAUNCH='nohup sh /data/local/tmp/tb-supervisor.sh >/dev/null 2>&1 &'
say() { echo "[persist] $*"; }

# --- Layer 1: /data/local/userinit.sh (OTA-durable, some RK ROMs run it) ---
# no printf on this shell (toolbox) — write files with heredocs only
cat > /data/local/userinit.sh 2>/dev/null <<EOF
#!/system/bin/sh
sleep 20
$LAUNCH
EOF
if [ -s /data/local/userinit.sh ]; then
  chmod 755 /data/local/userinit.sh 2>/dev/null
  say "userinit.sh installed (/data, OTA-durable)"
else
  say "userinit.sh not writable — skipped"
fi

# The /system layers need root and a writable /system. Note: Android 5.1
# toolbox `id` has no -u flag, so probe busybox first, then parse plain `id`.
uid="$(busybox id -u 2>/dev/null)"
[ -n "$uid" ] || uid="$(id -u 2>/dev/null)"
case "$uid" in ''|*[!0-9]*) case "$(id 2>/dev/null)" in "uid=0("*) uid=0 ;; *) uid="" ;; esac ;; esac
if [ "$uid" != "0" ]; then
  say "not root — /system hooks skipped (APK + userinit still cover boot)"
  exit 0
fi

SDK="$(getprop ro.build.version.sdk 2>/dev/null | busybox tr -d ' \r\n')"
case "$SDK" in ''|*[!0-9]*) SDK=0 ;; esac

# --- Layer 0: Magisk / KernelSU service.d — the ONLY layer that still works on
# Android 11. It runs as root, late in boot, with no app and no /system write,
# so it sidesteps every Android 11 obstacle at once: dm-verity read-only system,
# init no longer running install-recovery.sh, and the app sandbox refusing su.
# Everything below this point is best-effort by comparison. ---
if [ -d /data/adb/service.d ]; then
  # Loops rather than launching once: the boot hooks fire exactly once, so if
  # the supervisor is ever killed (low-memory killer, a stray pkill) nothing
  # would bring it back until the next reboot. Magisk runs service.d scripts in
  # the background, so staying resident here is free.
  cat > /data/adb/service.d/90tb-agent.sh <<'EOS'
#!/system/bin/sh
# tb-agent boot hook (Magisk/KernelSU service.d — root, late_start)
sleep 20
while true; do
  sh /data/local/tmp/tb-supervisor.sh
  sleep 30
done
EOS
  chmod 755 /data/adb/service.d/90tb-agent.sh 2>/dev/null
  say "service.d/90tb-agent.sh installed (root at boot — Android 11 path)"
elif [ "$SDK" -ge 29 ]; then
  say "no /data/adb/service.d (no Magisk) — the init service below must carry boot"
fi

# --- The init-service wrapper. Lives in /data and is written HERE, before any
# /system work, because it does not depend on /system at all: init only needs
# /system/bin/sh to exist, and the script it runs can be anywhere it can read.
# Writing it inside the /system section was a real bug — on a box whose system
# partition is full that section exits early, so the wrapper was never created
# while an init.rc block from an earlier run still pointed at it. init then
# retried a missing script forever (exit 127, init.svc.tb-agent=restarting). ---
WRAPPER=/data/local/tmp/tb-boot-init.sh
cat > "$WRAPPER" <<'EOS'
#!/system/bin/sh
# Started by init (see the tb-agent service in init.rc). Runs as root at boot.
# The wait is for /data and the network: the supervisor reads its config from
# /data/local/tmp and init fires this well before DHCP settles.
sleep 25
# Never returns. The supervisor is meant to run forever, but if it is ever
# killed (low-memory killer, a stray pkill) nothing else would restart it until
# the next reboot — the boot hooks all fire exactly once.
while true; do
  sh /data/local/tmp/tb-supervisor.sh
  sleep 30
done
EOS
chmod 755 "$WRAPPER" 2>/dev/null
restorecon "$WRAPPER" 2>/dev/null || true
[ -s "$WRAPPER" ] && say "init wrapper ready ($WRAPPER)" \
  || say "⚠️ could not write $WRAPPER — /data full?"

# --- Layer 0b: modern-Android device config. Only from SDK 24 up: TB2 (SDK 22)
# already boots reliably and must not be disturbed by any of this. ---
if [ "$SDK" -ge 24 ]; then
  PKG=uz.nozim.tbboot
  A11Y="$PKG/$PKG.TbAccessibilityService"

  # An accessibility service listed here is bound by the system during boot,
  # without BOOT_COMPLETED and without the app being launched — which is the
  # whole point, since those are exactly what Android 11 withholds. Append,
  # never overwrite: clobbering the list would disable someone else's service.
  cur="$(settings get secure enabled_accessibility_services 2>/dev/null | busybox tr -d ' \r\n')"
  case "$cur" in
    *"$A11Y"*) say "accessibility autostart already enabled" ;;
    ''|null)   settings put secure enabled_accessibility_services "$A11Y" 2>/dev/null \
                 && say "accessibility autostart enabled" ;;
    *)         settings put secure enabled_accessibility_services "$cur:$A11Y" 2>/dev/null \
                 && say "accessibility autostart appended" ;;
  esac
  settings put secure accessibility_enabled 1 2>/dev/null

  # Doze/App Standby may otherwise freeze the launcher app between boots.
  cmd deviceidle whitelist "+$PKG" >/dev/null 2>&1 \
    || dumpsys deviceidle whitelist "+$PKG" >/dev/null 2>&1 || true

  # Magisk denies su to an unknown app by showing a prompt that nobody on a
  # signage wall will ever tap; it then times out and denies. Pre-authorize
  # ourselves so the APK path can actually get root. policy 2 = allow, until 0
  # = forever. Harmless where magisk is absent.
  if magisk -v >/dev/null 2>&1; then
    tbuid="$(pm list packages -U 2>/dev/null | busybox grep "$PKG " | busybox sed 's/.*uid://' | busybox tr -d ' \r\n')"
    case "$tbuid" in
      ''|*[!0-9]*) say "magisk present but $PKG uid unknown — su stays on prompt" ;;
      *) magisk --sqlite "REPLACE INTO policies (uid,policy,until,logging,notification) VALUES($tbuid,2,0,0,0)" \
           >/dev/null 2>&1 && say "magisk su pre-authorized for $PKG (uid $tbuid)" ;;
    esac
  fi
fi

# --- SDK>=29 (TB40/TB50/TB60): the /system boot hook is a SEPARATE script. ---
# None of the overlay / init.rc logic lives here any more. It writes to /system,
# it can stop a box from booting (two were lost on 2026-08-20), and keeping it
# in the script every stable TB2 also runs was how a modern-box change kept
# breaking the stable ones. It is now provisioning/enable-boot-hook-modern.sh,
# run deliberately and per box. This script — the one the supervisor auto-runs
# on every start — never touches /system on a modern box.
if [ "$SDK" -ge 29 ]; then
  if [ -f /data/adb/service.d/90tb-agent.sh ]      || busybox grep -q "service tb-agent" /system/etc/init/hw/init.rc 2>/dev/null; then
    say "modern box: a root-side boot layer is already present"
  else
    say "modern box: NO auto boot hook (safe default). To install one, with"
    say "   physical access to THIS box (flag as a prefix INSIDE su):"
    say "     su 0 sh -c 'TB_I_HAVE_PHYSICAL_ACCESS=1 sh /data/local/tmp/tb-enable-boot-hook.sh'"
  fi
  say "done"
  exit 0
fi

# ===================================================================
# Everything below is SDK < 29 ONLY — the stable TB1..TB5 boxes.
# These /system layers have worked for months and are left untouched.
# ===================================================================
remount_rw() {
  mount -o rw,remount /system 2>/dev/null && return 0
  mount -o rw,remount /system /system 2>/dev/null && return 0
  busybox mount -o rw,remount /system 2>/dev/null && return 0
  return 1
}
remount_ro() {
  mount -o ro,remount /system 2>/dev/null || busybox mount -o ro,remount /system 2>/dev/null || true
}
if ! remount_rw; then
  say "cannot remount /system rw (dm-verity/locked?) — APK + userinit still cover boot"
  exit 0
fi

# generic launcher in /system (no secrets; reads config from /data at boot)
cat > /system/etc/tb-boot.sh <<EOF
#!/system/bin/sh
sleep 20
$LAUNCH
EOF
chmod 755 /system/etc/tb-boot.sh
say "/system/etc/tb-boot.sh installed"

# --- Layers 2 and 3: pre-Android-10 only. ---
# (SDK<29 already — the whole tail below only runs on stable boxes.)

# --- Layer 2: install-recovery.sh (runs as root at boot on most RK images) ---
IR=/system/bin/install-recovery.sh
if [ ! -f "$IR" ] || ! [ -s "$IR" ]; then
  # missing or empty (an earlier printf-based write may have truncated it)
  cat > "$IR" <<EOF
#!/system/bin/sh
# tb-agent-hook
sh /system/etc/tb-boot.sh &
[ -f $IR.orig ] && sh $IR.orig
EOF
  chmod 755 "$IR"; say "install-recovery.sh created"
elif ! grep -q tb-agent-hook "$IR" 2>/dev/null; then
  [ -f "$IR.orig" ] || cp "$IR" "$IR.orig"   # preserve the original once
  cat > "$IR" <<EOF
#!/system/bin/sh
# tb-agent-hook
sh /system/etc/tb-boot.sh &
[ -f $IR.orig ] && sh $IR.orig
EOF
  chmod 755 "$IR"; say "install-recovery.sh hooked (original kept as .orig)"
else
  say "install-recovery.sh already hooked"
fi

# --- Layer 3: init.d (only if a run-parts loader is present) ---
if [ -d /system/etc/init.d ]; then
  cp /system/etc/tb-boot.sh /system/etc/init.d/90tb-agent
  chmod 755 /system/etc/init.d/90tb-agent
  say "init.d/90tb-agent installed"
fi

# --- DNS-less ROMs: pin the server in /system/etc/hosts, but ONLY while DNS is
# actually broken (a working resolver must stay authoritative — if the VPS IP
# ever moves, healthy devices must not be stuck on a stale pin).
TB_HOST=tbremote.nozim-dev.uz
TB_IP=45.94.58.205
if ! grep -q "$TB_HOST" /system/etc/hosts 2>/dev/null; then
  if ! busybox nslookup "$TB_HOST" >/dev/null 2>&1; then
    # Append, then read back. A partial append on a full partition would leave
    # /etc/hosts with a truncated line — every name on the box resolves through
    # that file, so a broken one is worse than a missing pin.
    cp /system/etc/hosts /system/etc/hosts.tbbak 2>/dev/null
    echo "$TB_IP $TB_HOST" >> /system/etc/hosts 2>/dev/null
    if grep -q "^$TB_IP $TB_HOST\$" /system/etc/hosts 2>/dev/null; then
      say "hosts pinned ($TB_HOST -> $TB_IP; ROM has no DNS)"
    else
      say "⚠️ hosts pin failed — restoring the original"
      [ -s /system/etc/hosts.tbbak ] && cp /system/etc/hosts.tbbak /system/etc/hosts 2>/dev/null
    fi
  fi
fi

remount_ro
say "done"
