#!/sbin/bash
# Aiways Drivers Community - head-unit update package.
#
# Recovery calls us as:  update-binary <api_version> <pipe_fd> <zip_path>
# This recovery ships /sbin/bash and a full busybox, so the package is a plain shell script
# rather than an edify `updater` binary.
#
# WE OWN THE SCREEN. Recovery's own UI is useless to us:
#   * its icon_installing_en.png wrongly reads "IVI system is being reset to factory setting"
#     during ANY install - that is what owners report as "it did a factory reset";
#   * ui_print goes only to the log, never to the panel;
#   * its bitmaps live inside its HAB-signed ramdisk, so they cannot be replaced;
#   * its progress bar is started by recovery BEFORE we run, and reserves part of the range for
#     signature verification, so our set_progress values land ~15-20% low.
# Since we run as ROOT and recovery is our PARENT, we simply SIGSTOP it and draw the framebuffer
# ourselves. SIGCONT at the end lets it reboot normally.
# fb0 MEASURED: 1920x720, 32bpp, stride 7680, byte order RGBA; virtual 1920x2304 = triple
# buffered (pan seen at 0,1536) -> write all three slots.
OUT=$2
ZIP=$3
BB=/sbin/busybox
SCR=/tmp/oa/screens

ui()  { echo "ui_print $*" >&$OUT; echo "ui_print" >&$OUT; }   # log only; never after freeze
# ★ 2026-10-08: the owner sees every frame ~20-30 px too LOW (two runs, same unit). The fixed slots
#   0/768/1536 (virtual 1920x2304 = 3 x 768) are not necessarily where the page recovery is SHOWING
#   starts - e.g. 720 rounded up to 32 = 736 would drop the picture by exactly 32 px. So the frame is
#   ALSO written at the current pan offset (/sys/class/graphics/fb0/pan = "x,y"), LAST, so the visible
#   page is exactly the frame. The pan is logged once (log_fb) so the real value becomes known.
fb()  { [ -f "$1" ] || return 0
        local off y
        for off in 0 768 1536; do
            $BB dd if="$1" of=/dev/graphics/fb0 bs=7680 seek=$off conv=notrunc 2>/dev/null
        done
        y=$($BB cut -d, -f2 /sys/class/graphics/fb0/pan 2>/dev/null)
        case "$y" in ''|*[!0-9]*|0|768|1536) return 0 ;; esac
        $BB dd if="$1" of=/dev/graphics/fb0 bs=7680 seek=$y conv=notrunc 2>/dev/null; }
log_fb() { log "fb0: pan=$($BB cat /sys/class/graphics/fb0/pan 2>/dev/null) virtual=$($BB cat /sys/class/graphics/fb0/virtual_size 2>/dev/null) stride=$($BB cat /sys/class/graphics/fb0/stride 2>/dev/null)"; }
stick_present() { [ -e /sys/block/sda ] || [ -e /dev/block/sda ]; }

REC=$PPID
[ -n "$REC" ] || REC=$($BB pidof recovery 2>/dev/null | $BB cut -d" " -f1)
freeze() { [ -n "$REC" ] && $BB kill -STOP "$REC" 2>/dev/null; }
thaw()   { [ -n "$REC" ] && $BB kill -CONT "$REC" 2>/dev/null; }

# ★★ OUR OWN LOG. recovery's last_log/last_install on this ROM are ALWAYS 0 bytes (measured
#    2026-09-21 and again on car-daily 2026-09-25 after three failed runs), there is no
#    /cache/recovery/log and no /proc/last_kmsg - so a failed install has never left any evidence
#    at all, and the only diagnosis available was guesswork. /cache survives the reboot and is
#    readable from Android afterwards, so this file is the black box.
LOG=/cache/adc-install.log
log() { echo "$*" >> "$LOG" 2>/dev/null; }

die() { log "DIED: $*"; fb "$SCR/failed.raw"; $BB sleep 15; thaw; ui "FAILED: $*"; exit 1; }

# --- connect-backup: full HU partition backup straight to the stick ---------------------------
# Part of the deliberate update (not an autofire): reads the HU partitions and writes ONLY to the
# stick, so it CANNOT brick — there is no path that writes a head-unit partition. Runs AFTER the apps
# are installed and NEVER dies: any problem (stick not writable, too small, a bad partition) is logged
# and skipped, and the install still completes. Same partition SET as tools/hu_backup_device.sh.
hu_backup_to_stick() {
    $BB mount -o remount,rw /udisk1 2>/dev/null || $BB mount -o rw /udisk1 2>/dev/null
    if ! ( : > /udisk1/.adc_wtest ) 2>/dev/null; then
        log "HU backup: stick not writable — skipping"; return 0
    fi
    $BB rm -f /udisk1/.adc_wtest 2>/dev/null
    local FREE; FREE=$($BB df /udisk1 2>/dev/null | $BB tail -1 | $BB awk '{print $4}')
    case "$FREE" in *[!0-9]*|"") FREE=0 ;; esac
    if [ "$FREE" -gt 0 ] && [ "$FREE" -lt 2200000 ]; then
        log "HU backup: only ${FREE} KB free (<2.2 GB) — skipping"; return 0
    fi
    local FOTA; FOTA=$($BB grep -m1 "^ro.fota.version=" /system/build.prop 2>/dev/null | $BB cut -d= -f2)
    [ -n "$FOTA" ] || FOTA=unknown
    local STAMP; STAMP=$($BB date +%Y%m%d-%H%M%S 2>/dev/null)
    local OUT="/udisk1/adc-backup/${FOTA}_${STAMP}"
    $BB mkdir -p "$OUT" || { log "HU backup: cannot mkdir $OUT — skipping"; return 0; }
    local MAN="$OUT/manifest.txt"
    { echo "# ADC head-unit backup (recovery)"; echo "created=$STAMP"; echo "ro.fota.version=$FOTA";
      echo "# name device bytes md5 status"; } > "$MAN"
    $BB cat /proc/partitions > "$OUT/partitions.txt" 2>/dev/null
    local e n name dev md sz t0 t1 T0 b cur ddp
    local PARTS="1:boot 2:recovery 3:misc 5:system 7:device 8:p8 9:vendor 10:p10 11:mcu 12:logo 13:p13"
    # ★ 2026-10-08: the bar follows the BYTES copied. It used to sit on ONE frame for the whole ~3 min
    #   copy, and the owner (rightly) read that as a hung install. Total from /proc/partitions (1K blocks).
    BK_TOTAL=0; BK_DONE=0; BAR_LAST=
    for e in $PARTS; do
        b=$($BB awk -v p="mmcblk0p${e%%:*}" '$4 == p { print $3 }' /proc/partitions 2>/dev/null)
        BK_TOTAL=$((BK_TOTAL + ${b:-0} * 1024))
    done
    log "HU backup: $((BK_TOTAL / 1048576)) MB to copy"
    backup_bar 0
    T0=$($BB date +%s 2>/dev/null)
    for e in $PARTS; do
        n=${e%%:*}; name=${e#*:}; dev="/dev/block/mmcblk0p$n"
        t0=$($BB date +%s 2>/dev/null)
        [ -b "$dev" ] || { echo "$name $dev - - MISSING" >> "$MAN"; continue; }
        $BB dd if="$dev" of="$OUT/$name.img" bs=1048576 2>/dev/null &
        ddp=$!
        while $BB kill -0 $ddp 2>/dev/null; do
            cur=$($BB stat -c %s "$OUT/$name.img" 2>/dev/null); case "$cur" in ''|*[!0-9]*) cur=0 ;; esac
            [ "$BK_TOTAL" -gt 0 ] && backup_bar $(( (BK_DONE + cur) * 100 / BK_TOTAL ))
            $BB sleep 2
        done
        if wait $ddp; then
            md=$($BB md5sum "$OUT/$name.img" 2>/dev/null | $BB cut -d' ' -f1)
            sz=$($BB stat -c %s "$OUT/$name.img" 2>/dev/null)
            echo "$name $dev $sz $md OK" >> "$MAN"
            BK_DONE=$((BK_DONE + ${sz:-0}))
        else
            echo "$name $dev - - FAILED" >> "$MAN"; $BB rm -f "$OUT/$name.img"
        fi
        t1=$($BB date +%s 2>/dev/null); log "HU backup: $name $(( ${t1:-0} - ${t0:-0} ))s"
    done
    $BB sync
    log "HU backup: done -> $OUT ($(( $($BB date +%s 2>/dev/null) - ${T0:-0} ))s in total)"
}

# Progress frame for the backup copy: only the 20 % frames exist (no finer render yet), so the bar
# moves in five steps; a frame is drawn only when it changes (each draw writes all three fb slots).
backup_bar() {
    local f=p0
    [ "$1" -ge 20 ] && f=p20; [ "$1" -ge 40 ] && f=p40; [ "$1" -ge 60 ] && f=p60; [ "$1" -ge 80 ] && f=p80
    [ "$f" = "$BAR_LAST" ] || { fb "$SCR/$f.raw"; BAR_LAST=$f; }
}

# --- connect-backup: park the boot trigger ON THE STICK, so the unit can reboot WITH the stick in ---
# ★ 2026-10-08 (owner, at the bench): the old flow told the owner to pull the stick, then needed it
#   back in Android for the T-Box half - and the T-Box one-shot fired on the hub's card reader in the
#   meantime. Now the trigger files go to adc-parked/ (Connect's own StickPark convention, which it
#   already reads), so u-boot finds nothing to re-arm on the next boot, the stick stays in, and
#   Connect starts the T-Box backup onto it. 0 = parked (no trigger left at the stick's top level).
#   ⛔ Anything else returns 1 and the old wait-for-removal path keeps the re-install loop away.
park_trigger() {
    local P=/udisk1/adc-parked f
    $BB mkdir -p "$P" 2>/dev/null || return 1
    for f in ota_update.ini update_os.zip update.zip; do
        [ -e "/udisk1/$f" ] || continue
        $BB mv -f "/udisk1/$f" "$P/$f" 2>/dev/null || return 1
    done
    $BB sync
    [ ! -e /udisk1/ota_update.ini ] && [ ! -e /udisk1/update_os.zip ]
}

# --- get our artwork out BEFORE we freeze anything --------------------------
ui "Aiways Drivers Community - head unit update"
log "=== ADC install $($BB date 2>/dev/null) ==="
log "zip=$ZIP"
$BB mkdir -p /tmp/oa
$BB unzip -o "$ZIP" 'screens/*' 'vendor/etc/adc-release' -d /tmp/oa >/dev/null 2>&1
. /tmp/oa/vendor/etc/adc-release 2>/dev/null
log "version=$ADC_VERSION flavour=$ADC_FLAVOUR base_fota=$ADC_BASE_FOTA"
log_fb
# apps-like flavours ship ONLY our own apps (nothing OEM-derived): no firmware gate, vendor-only unzip.
APPSLIKE=0; case "$ADC_FLAVOUR" in apps|connect-backup) APPSLIKE=1 ;; esac

freeze                      # from here recovery cannot touch the panel...
$BB sleep 1
fb "$SCR/p0.raw"            # ...and neither its bar nor its false text is on screen

# --- mount the targets ------------------------------------------------------
$BB mount -o rw,remount /system 2>/dev/null || $BB mount -t ext4 -o rw /dev/block/mmcblk0p5 /system || die "mount /system"
$BB mkdir -p /vendor
$BB mount -t ext4 -o rw /dev/block/mmcblk0p9 /vendor || die "mount /vendor"
# --- refuse to install on a firmware we were not built against ----------------
# Our patched Settings.apk / SystemUI.apk / HKLauncher.apk are builds of ONE OEM release.
# Installing them over a different firmware would replace that unit's own system apps with
# foreign ones. THAT is the entire reason this gate exists.
# ★ An APPS-ONLY package (ADC_FLAVOUR=apps) ships none of them - only our own apps, which do not
#   depend on the OEM build at all - so there is nothing to refuse and the gate does not apply.
#   Measured 2026-09-25: car-daily runs 0051 while every full stick so far targets 0046, because
#   the BENCH unit was downgraded to 0046 during a recovery. The gate was right; it is the full
#   package that cannot go on a stock car, not our apps.
HAVE=$($BB grep -m1 "^ro.fota.version=" /system/build.prop 2>/dev/null | $BB cut -d= -f2)
if [ "$APPSLIKE" != 1 ] && [ -n "$ADC_BASE_FOTA" ] && [ -n "$HAVE" ] && [ "$HAVE" != "$ADC_BASE_FOTA" ]; then
    $BB umount /vendor 2>/dev/null
    $BB mount -o ro,remount /system 2>/dev/null
    fb "$SCR/wrongfw.raw"
    $BB sleep 25
    thaw
    ui "REFUSED: this stick is for $ADC_BASE_FOTA, unit has $HAVE"
    exit 1
fi
fb "$SCR/p20.raw"

# A leftover /data copy of the boot animation would shadow ours.
#
# ⛔ We deliberately do NOT delete /data/app/com.openaiways.connect-*.apk any more.
#    Removing an installed package's APK behind PackageManager's back is unsafe: packages.xml
#    still records that codePath, the file is gone on the next boot, and the package can be
#    dropped entirely. That happened on 2026-09-25 — Connect disappeared after an update, and
#    because the Settings rows hide themselves when Connect is absent, the owner simply saw the
#    Drivers Community switches vanish from Settings -> Connect.
#    It is also unnecessary: PackageManager keeps a /data update only while its versionCode is
#    >= the /vendor one, so a newer hand-installed build legitimately wins and an older one is
#    discarded automatically.
$BB mkdir -p /data
$BB mount -t ext4 -o rw /dev/block/mmcblk0p4 /data 2>/dev/null
# ⛔⛔ ONLY remove the OEM animation if THIS package installs one in its place.
#    On this ROM the boot animation lives ONLY at /data/local/bootanimation.zip - there is none in
#    /system/media or /vendor/media, and /init.rc chmods that exact path. Deleting it is correct
#    for a FULL package, which then installs its own to /system/media where it wins.
#    ★ An apps-only package ships no animation, so the unconditional delete left car-daily with NO
#    boot animation at all on 2026-09-25. The owner's words: "I almost paniced."
if $BB unzip -l "$ZIP" 2>/dev/null | $BB grep -q 'system/media/bootanimation.zip'; then
    $BB rm -f /data/local/bootanimation.zip
fi

# --- the actual install (the long step) -------------------------------------
# Payload is laid out as vendor/... and system/... so one unzip -d / lands everything in place.
# Run it in the background so we can advance our own bar while it works.
# ⛔⛔ THE PATTERNS MUST BE QUOTED LITERALS - DO NOT PUT THEM IN A VARIABLE.
#    A variable expanded unquoted here undergoes PATHNAME EXPANSION against the working directory,
#    which in recovery is `/` - and `/` really does have a vendor/ directory, because we mounted it
#    three lines above. `vendor/*` therefore becomes `vendor/app`, which matches NO entry in the zip
#    (the entries are vendor/app/<name>.apk), nothing is extracted, and the install dies.
#    ★ That is exactly what failed on car-daily on 2026-09-25: three recovery runs, nothing
#    installed. Reproduced afterwards in a shell with a vendor/ dir present. An apps-only package
#    also has no system/ tree, so its pattern list genuinely differs - hence two explicit calls
#    rather than one clever one.
if [ "$APPSLIKE" = 1 ]; then
    $BB unzip -o "$ZIP" 'vendor/*' -d / >/dev/null 2>&1 &
else
    $BB unzip -o "$ZIP" 'vendor/*' 'system/*' -d / >/dev/null 2>&1 &
fi
UZ=$!
i=40
while $BB kill -0 $UZ 2>/dev/null; do
    fb "$SCR/p$i.raw"
    [ $i -lt 80 ] && i=$(($i + 20))
    $BB sleep 3
done
wait $UZ || die "unzip"

# ★ Derived from the zip rather than hardcoded: the list differs per flavour, and a hardcoded
#   list would either miss a file or demand one this package never carried.
SHIPPED=$($BB unzip -l "$ZIP" 2>/dev/null | $BB awk '{print $NF}' \
          | $BB grep -E '^(vendor/app/.*\.apk|vendor/etc/adc-release|system/media/.*\.zip)$')
[ -n "$SHIPPED" ] || die "package carries no payload"
for f in $SHIPPED; do
    [ -f "/$f" ] || die "missing after install: /$f"
    $BB chown 0:0 "/$f"
    $BB chmod 644 "/$f"
done

# --- wireless CarPlay payload ---------------------------------------------------------------
# Verify extraction first. The generic SHIPPED loop above only covers vendor/app/*.apk, the marker
# and the boot animation, so without this a failed system/* unzip would pass silently and leave a
# half-installed CarPlay (the exact class of bug that cost 2026-09-25).
if $BB unzip -l "$ZIP" 2>/dev/null | $BB grep -q 'system/bin/aiwaysbt'; then
    for f in system/bin/aiwaysbt system/etc/adc-softap.conf \
             system/etc/firmware/wlan/qcom_cfg.ini system/bin/netd.5g \
             system/bin/oa_init.sh system/bin/dropbearmulti; do
        [ -f "/$f" ] || die "missing after install: /$f"
    done
fi

# ⛔ The generic loop above only chmods APKs/marker/bootanim (644). Executables need 755, and the
#    netd swap needs a GUARD: we must never drop a patched binary onto a build it was not made for.
# --- sshd: the SHIPPED dropbear (added 2026-09-29) --------------------------------------------
# ★ It used to be staged into /data by the Connect app, so a factory reset or a package reinstall
#   wiped it and sshd stayed down until Connect happened to run again. Shipping the binary on
#   /system makes sshd survive; only the host key and authorized_keys remain on /data.
#   oa_init.sh starts it from here, init-descended, so it gets the full capability set.
if [ -f /system/bin/dropbearmulti ]; then
    $BB chown 0:0 /system/bin/dropbearmulti
    $BB chmod 755 /system/bin/dropbearmulti
fi

if [ -f /system/bin/aiwaysbt ]; then
    # ★★ SETUID root, GROUP system — this is what makes automatic CarPlay possible.
    #    aiwaysbt needs root (stop brlinkd, open /dev/ttymxc2.tmp, drive the WiFi AP), but the thing
    #    that must START it is Connect, which runs as uid system. Without setuid the only route was
    #    Connect's RootExec -> local adbd, and KitKat adbd serves ONE transport, so it silently
    #    no-ops whenever a laptop is attached. 4750 = rwsr-x---: ONLY uid/gid system can execute it,
    #    and it then runs as root. Group system rather than shell deliberately: shell is reachable
    #    by anyone with adb, system is not.
    $BB chown 0:1000 /system/bin/aiwaysbt
    $BB chmod 4750 /system/bin/aiwaysbt

    # ★★ AND IT MUST ALSO LIVE ON /vendor — NECESSARY, THOUGH NOT SUFFICIENT (see the end of
    #    this block). An earlier version of this line called it "the copy that actually gets root";
    #    that is RETRACTED — it gets euid 0, which is not the same thing.
    #    MEASURED 2026-09-28, the app's mount namespace vs init's:
    #      /proc/<connect-pid>/mountinfo:  /system  ro,nosuid,nodev,relatime
    #                                      /vendor  ro,relatime
    #      /proc/1/mountinfo:              /system  ro,relatime
    #    Android remounts /system, /data and /cache **nosuid** inside the zygote's namespace, so the
    #    setuid bit set just above is SILENTLY IGNORED when Connect (an app) execs it. Everything the
    #    comment above says about why setuid is needed is right; it just cannot work from /system.
    #    Symptom it produced: aiwaysbt ran as uid 1000, could not traverse /data/misc/wifi
    #    (drwxrwx--- wifi:wifi), and advertised its fallback "Aiways-CarPlay"/ch11/default-pass to the
    #    phone over 0x5703 instead of the live DMSLVXMAXBAXLR800010/ch36 - so the phone was told to
    #    join a network that does not exist. adbd is in init's namespace, which is why running it by
    #    hand over adb always worked and hid this for days.
    $BB mkdir -p /vendor/bin
    $BB cat /system/bin/aiwaysbt > /vendor/bin/aiwaysbt || die "could not install /vendor/bin/aiwaysbt"
    $BB chown 0:1000 /vendor/bin/aiwaysbt
    $BB chmod 4750 /vendor/bin/aiwaysbt
    [ -f /vendor/bin/aiwaysbt ] || die "missing after install: /vendor/bin/aiwaysbt"
    # ⛔⛔ AND IT IS STILL NOT ENOUGH ON ITS OWN. MEASURED 2026-09-28: even at uid=0 euid=0 from
    #    /vendor/bin, an app-launched aiwaysbt could NOT read /proc/<brlinkd>/fd or
    #    /data/misc/wifi/softap.conf. The ceiling is the CAPABILITY BOUNDING SET, which a fork of the
    #    zygote can never widen:
    #      adb shell child (init-descended):  CapBnd ffffffffffffffff
    #      app child (zygote-descended)    :  CapBnd fffffff000000000   <- no CAP_DAC_OVERRIDE
    #    uid 0 without CAP_DAC_OVERRIDE cannot bypass file permissions at all.
    #    ⇒ THE PRIVILEGED LAUNCH MUST BE INIT-DESCENDED (Connect's RootExec via local adbd, or an
    #      init service + ctl.start). Installing here is correct and required; it does not by itself
    #      make the automatic path work. STILL OPEN as of 2026-09-28.
    ui "CarPlay: aiwaysbt installed (/vendor/bin, setuid)"
fi

# --- init-descended startup hook (this is what makes sshd independent of adb) -----------------
# ★ WHY. Anything an Android app spawns is a child of the ZYGOTE and inherits its clamped capability
#   bounding set (fffffff000000000 — no CAP_DAC_OVERRIDE), so a setuid helper launched by an app
#   reaches uid 0 and still cannot read root-owned files. init's children get the full set.
#   init already runs shell scripts from /system:
#       service hk_oem_init /system/bin/sh /system/bin/hk_oem_init.sh / class main / user root
#   so ONE guarded line there buys the same privilege as a new init.rc service WITHOUT repacking
#   boot.img — no flash, no brick risk, and reversible by restoring the .oaorig copy.
# ⚠️ A stock firmware restore rewrites /system, so this must be re-applied by every install.
if [ -f /system/bin/oa_init.sh ]; then
    $BB chown 0:2000 /system/bin/oa_init.sh
    $BB chmod 755 /system/bin/oa_init.sh
    if $BB grep -q 'oa_init.sh' /system/bin/hk_oem_init.sh 2>/dev/null; then
        ui "sshd: init hook already present"
    elif [ -f /system/bin/hk_oem_init.sh ]; then
        # Keep THIS car's original, not our copy, so a revert restores what it actually had.
        [ -f /system/bin/hk_oem_init.sh.oaorig ] || \
            $BB cp /system/bin/hk_oem_init.sh /system/bin/hk_oem_init.sh.oaorig
        # ⛔ Insert BEFORE `main $@` — main ends with a 120 s boot-animation sleep, so anything after
        #   it would run two minutes late. Rebuilt with grep rather than `sed -i` + \n, because
        #   busybox sed's newline handling in the replacement is not something to bet a /system file
        #   on. `main $@` is the last line of the script.
        $BB grep -v '^main \$@' /system/bin/hk_oem_init.sh > /tmp/hk_oem_init.new
        echo '[ -x /system/bin/oa_init.sh ] && /system/bin/sh /system/bin/oa_init.sh' >> /tmp/hk_oem_init.new
        echo 'main $@' >> /tmp/hk_oem_init.new
        if $BB grep -q 'oa_init.sh' /tmp/hk_oem_init.new && $BB grep -q '^main \$@' /tmp/hk_oem_init.new; then
            $BB cp /tmp/hk_oem_init.new /system/bin/hk_oem_init.sh
            $BB chown 0:2000 /system/bin/hk_oem_init.sh
            $BB chmod 755 /system/bin/hk_oem_init.sh
            ui "sshd: started from init (revert: cp hk_oem_init.sh.oaorig hk_oem_init.sh)"
        else
            ui "sshd: init hook NOT applied (patch check failed) - left untouched"
        fi
        $BB rm -f /tmp/hk_oem_init.new
    fi
fi

# ⛔ THE NATIVE LIBRARY MUST BE LIFTED OUT OF THE APK BY HAND.
#    Android 4.4 does NOT extract lib/ for a system app scanned from /vendor/app - there is no
#    nativeLibraryDir for it - so System.loadLibrary() finds nothing and the receiver dies
#    SILENTLY right after onCreate(). On the bench this was masked because the .so had been pushed
#    to /vendor/lib by hand; a stock unit would have had a CarPlay app that simply never started.
#    /vendor/lib is one of the linker's two default search paths on KitKat (with /system/lib).
if [ -f /vendor/app/AiwaysCarPlay.apk ]; then
    CPLIB=lib/armeabi-v7a/libaiwayscp.so
    $BB mkdir -p /tmp/oacp /vendor/lib
    $BB unzip -o /vendor/app/AiwaysCarPlay.apk "$CPLIB" -d /tmp/oacp >/dev/null 2>&1
    [ -s "/tmp/oacp/$CPLIB" ] || die "CarPlay: $CPLIB missing from the APK"
    $BB cp "/tmp/oacp/$CPLIB" /vendor/lib/libaiwayscp.so
    $BB chown 0:0 /vendor/lib/libaiwayscp.so
    $BB chmod 644 /vendor/lib/libaiwayscp.so
    [ -s /vendor/lib/libaiwayscp.so ] || die "CarPlay: could not install libaiwayscp.so"
    $BB rm -rf /tmp/oacp
    ui "CarPlay: native library installed"
fi

# ⛔ THE MFi BRIDGE dlopen()s A *COPY* OF ADAYO'S STOCK AIRPLAY LIBRARY, UNDER A DIFFERENT NAME.
#    `native/` loads /vendor/lib/libAirPlaySupportStock.so to reach APSMFiSAP (the MFi auth chip
#    routines). That copy was made BY HAND on the bench months ago and was never part of any
#    package - so a stock restore removed it and CarPlay failed with:
#       [mfi] copy_certificate failed: dlopen(.../libAirPlaySupportStock.so) ... not found
#       [bt-driver] handshake budget expired before Identify
#    i.e. the Bluetooth handoff dies ~8 s in, before Identify, on every fresh unit.
#    MEASURED on hu-desk 2026-09-28 after the restore-to-stock. Same class of bug as libaiwayscp.so.
# ⚠️ Copy ONLY if absent, and only FROM the stock library: the point of the second name is to keep an
#    untouched original around even if libAirPlaySupport.so is ever replaced. Never overwrite it.
if [ -f /vendor/lib/libAirPlaySupport.so ] && [ ! -f /vendor/lib/libAirPlaySupportStock.so ]; then
    $BB cp /vendor/lib/libAirPlaySupport.so /vendor/lib/libAirPlaySupportStock.so
    $BB chown 0:0 /vendor/lib/libAirPlaySupportStock.so
    $BB chmod 644 /vendor/lib/libAirPlaySupportStock.so
    [ -s /vendor/lib/libAirPlaySupportStock.so ] || die "CarPlay: could not stage libAirPlaySupportStock.so"
    ui "CarPlay: MFi support library staged"
fi

# 5 GHz SoftAP: two same-length byte patches in netd (hw_mode=g -> a, channel=%d -> 36).
# VERIFIED SAFE FOR THIS BUILD: stock netd md5 is IDENTICAL on 0046 and 0051 (44db1ac7...), so the
# patched binary matches both. We still check, because a build we have not seen must be left alone.
NETD_STOCK_MD5=44db1ac7dca467647d2f6da6cd0cc375
if [ -f /system/bin/netd.5g ]; then
    HAVE=$($BB md5sum /system/bin/netd 2>/dev/null | $BB cut -d' ' -f1)
    if [ "$HAVE" = "$NETD_STOCK_MD5" ]; then
        # Keep the ORIGINAL (not our shipped copy) so a revert restores this car's own binary.
        [ -f /system/bin/netd.stock ] || $BB cp /system/bin/netd /system/bin/netd.stock
        $BB cp /system/bin/netd.5g /system/bin/netd
        $BB chown 0:2000 /system/bin/netd
        $BB chmod 755 /system/bin/netd
        ui "CarPlay: 5 GHz hotspot enabled (revert: mv /system/bin/netd.stock /system/bin/netd)"
    elif [ "$HAVE" = "7b5dd13983f8ede985148a62ddc7ce3e" ]; then
        # ★ OLD PATCH (5 GHz only) — UPGRADE IT. That build still lets netd write
        #   `ignore_broadcast_ssid=6`, an illegal value (hostapd knows 0/1/2 and hides the SSID for
        #   anything nonzero; the 6 is the last digit of channel=36). MEASURED 2026-09-28: the phone
        #   lists the network under "Meine Netzwerke" but it is ABSENT from the available list and
        #   every join fails. Leaving an old-patched unit alone would ship that bug.
        $BB cp /system/bin/netd.5g /system/bin/netd
        $BB chown 0:2000 /system/bin/netd
        $BB chmod 755 /system/bin/netd
        ui "CarPlay: hotspot patch updated (SSID now broadcast)"
    elif [ "$HAVE" = "54f50164fb6854d7f886a0ceed4b3c12" ]; then
        ui "CarPlay: hotspot already up to date"
    else
        # Unknown build: leave netd alone. CarPlay still works, on 2.4 GHz.
        ui "CarPlay: netd not patched (unrecognised build) - hotspot stays 2.4 GHz"
    fi
    $BB rm -f /system/bin/netd.5g
fi

# SoftAP credentials. WifiApConfigStore format: int(1) utf(ssid) int(4=WPA2_PSK) utf(passphrase).
# ⚠️ It is cached at BOOT, so this only takes effect after the reboot at the end of this install -
#    which is exactly when it is read. Never written if the owner already has one we did not make.
# Shipped as a real 39-byte file rather than built with printf: busybox printf escape support is
# not guaranteed, and one wrong length byte silently yields an AP nobody can join.
# ⛔ ONLY WRITTEN IF ABSENT - we must NOT clobber the owner's hotspot. A stock car already has the
#    OEM AP here (observed: ssid=DMSLVXMAXBAXLR<VIN>, pass=12345678) and that is a user-visible
#    feature. aiwaysbt reads the LIVE ssid AND passphrase out of netd's hostapd.conf for its 0x5703,
#    so CarPlay works with whatever is configured - replacing this file is never required.
if [ -f /system/etc/adc-softap.conf ] && [ ! -f /data/misc/wifi/softap.conf ]; then
    $BB mkdir -p /data/misc/wifi
    $BB cp /system/etc/adc-softap.conf /data/misc/wifi/softap.conf
    $BB chown 1000:1000 /data/misc/wifi/softap.conf
    $BB chmod 600 /data/misc/wifi/softap.conf
    ui "CarPlay: hotspot Aiways-CarPlay configured"
fi

# Stale dalvik entries for the replaced system apps must go, or the old code runs.
for f in $SHIPPED; do
    case "$f" in vendor/app/*.apk)
        $BB rm -f "/data/dalvik-cache/vendor@app@$($BB basename "$f")@classes.dex" ;;
    esac
done

# --- optional payload: the 3D viewer, shipped LOOSE on the stick ---------------
# Not inside the signed zip: it is ~569 MB and would make the package unreviewable and slow to
# inflate. The stick is mounted at /udisk1 during install, so we copy straight from it.
#   APK  -> /vendor/app        (a factory reset wipes /data/app, but not /vendor)
#   data -> mmcblk0p14         (/sdcard at runtime via FUSE; SEPARATE partition, also survives)
# In recovery the FUSE layer is not running, so the path carries the "0/" user dir.
# ✅ THE OPEN QUESTION IS ANSWERED (2026-09-28, measured): **RECOVERY DOES SEE A CABIN-PORT STICK.**
#   A real cabin-port install logged `udisk1 dir: yes / udisk1 contents: <empty>` while /sys/block
#   listed **sda**. So the stick IS enumerated on bus 1 - recovery simply had not MOUNTED it,
#   because since Connect 1.6.0 the package is staged to /sdcard and nothing asks for /udisk1 any
#   more. The old "u-boot scans fsl-ehci.0 only" limit is about the BOOT-TIME TRIGGER, not about
#   what recovery can reach once it is running. Those are two different things and conflating them
#   is what made the 3D payload look impossible from the cabin.
# ⇒ mount it ourselves. Retried: USB enumeration can lag behind recovery starting.
U53D=/udisk1/u53d
$BB mkdir -p /udisk1
if [ -z "$($BB ls /udisk1 2>/dev/null)" ]; then
    n=0
    while [ $n -lt 5 ]; do
        for dev in /dev/block/sda1 /dev/block/sda; do
            [ -b "$dev" ] || continue
            $BB mount -t vfat -o ro "$dev" /udisk1 2>/dev/null && break 2
        done
        n=$((n + 1))
        $BB sleep 1
    done
    log "udisk1 self-mount: $([ -n "$($BB ls /udisk1 2>/dev/null)" ] && echo ok || echo failed)"
fi
log "udisk1 dir: $([ -d /udisk1 ] && echo yes || echo no)"
log "udisk1 contents: $($BB ls /udisk1 2>/dev/null | $BB tr '\n' ' ')"
log "u53d payload: $([ -d "$U53D" ] && echo found || echo ABSENT)"
log "block devices: $($BB ls /sys/block 2>/dev/null | $BB tr '\n' ' ')"
if [ -d "$U53D" ]; then
    fb "$SCR/p60.raw"
    U_DST=/sdcard/0/Android/data/u53d.demo/files
    $BB mkdir -p /sdcard
    $BB mount -t vfat -o rw /dev/block/mmcblk0p14 /sdcard 2>/dev/null || true

    # Refuse rather than half-fill the partition: it is ~1.4 GB free and the payload is ~569 MB.
    NEED=$($BB du -sk "$U53D" 2>/dev/null | $BB cut -f1)
    FREE=$($BB df /sdcard 2>/dev/null | $BB tail -1 | $BB awk '{print $4}')
    case "$FREE" in *[!0-9]*|"") FREE=0 ;; esac
    case "$NEED" in *[!0-9]*|"") NEED=0 ;; esac
    if [ "$FREE" -gt 0 ] && [ "$NEED" -gt 0 ] && [ "$NEED" -gt "$FREE" ]; then
        ui "SKIPPED 3D viewer: needs ${NEED} KB, only ${FREE} KB free on /sdcard"
    else
        [ -f "$U53D/u53d.apk" ] && {
            $BB cp "$U53D/u53d.apk" /vendor/app/u53d-viewer.apk
            $BB chown 0:0 /vendor/app/u53d-viewer.apk
            $BB chmod 644 /vendor/app/u53d-viewer.apk
        }
        if [ -d "$U53D/files" ]; then
            # wipe-then-copy: files absent from their manifest are stale, and without
            # catalog/catalog.json the app falls back to built-in assets, so this is safe.
            $BB rm -rf "$U_DST"
            $BB mkdir -p "$U_DST"
            (
              cd "$U53D/files" || exit 0
              # everything EXCEPT the catalog first - the app switches over the moment it appears
              for e in *; do
                  [ "$e" = "catalog" ] && continue
                  $BB cp -r "$e" "$U_DST/" 2>/dev/null
              done
              [ -d catalog ] && {
                  $BB mkdir -p "$U_DST/catalog"
                  for c in catalog/*; do
                      [ "$c" = "catalog/catalog.json" ] && continue
                      $BB cp -r "$c" "$U_DST/catalog/" 2>/dev/null
                  done
                  # ...and the catalog index LAST
                  [ -f catalog/catalog.json ] && $BB cp catalog/catalog.json "$U_DST/catalog/"
              }
            ) &
            CP=$!
            i=60
            while $BB kill -0 $CP 2>/dev/null; do
                fb "$SCR/p$i.raw"
                [ $i -lt 80 ] && i=$(($i + 20))
                $BB sleep 5
            done
            wait $CP
        fi
    fi
    $BB sync
    $BB umount /sdcard 2>/dev/null
fi

# --- connect-backup: back up the HU to the stick, and arm the one-shot T-Box backup --------------
PARKED=0
if [ "$ADC_FLAVOUR" = "connect-backup" ]; then
    fb "$SCR/p60.raw"
    hu_backup_to_stick                       # read-only on the HU; writes only to the stick
    # One-shot, NOT an insert-watcher: Connect fires the Android-only T-Box backup once when it next
    # sees the stick, then clears this flag. (Owner vetoed autofire-on-insert, 2026-10-01 — this is a
    # single completion of THIS deliberate update, dispatched by Connect's EXISTING mount scan.)
    $BB mkdir -p /data/adc 2>/dev/null
    echo "1" > /data/adc/tbox_backup_pending 2>/dev/null
    $BB chown 1000:1000 /data/adc /data/adc/tbox_backup_pending 2>/dev/null
    $BB chmod 770 /data/adc 2>/dev/null
    $BB chmod 660 /data/adc/tbox_backup_pending 2>/dev/null
    log "armed one-shot T-Box backup flag"
    if park_trigger; then
        PARKED=1; log "boot trigger parked on the stick (adc-parked/) - rebooting WITH the stick in"
    else
        log "could not park the boot trigger - waiting for the stick to be removed (old flow)"
    fi
fi

$BB sync
$BB umount /vendor 2>/dev/null
$BB umount /data 2>/dev/null
$BB mount -o ro,remount /system 2>/dev/null
fb "$SCR/p100.raw"
$BB sleep 1

# --- wait for the stick, so u-boot has nothing to re-arm on -----------------
# u-boot re-arms recovery on EVERY boot while ota_update.ini + update_os.zip are readable on the
# stick (its trigger is pure file existence - it reads nothing from the head unit). We therefore
# do NOT reboot until the stick is out. That ends the reinstall loop WITHOUT modifying the stick,
# so it stays valid for the next car.
if [ "$PARKED" = 1 ]; then
    # connect-backup with the trigger parked: nothing left for u-boot to re-arm on, so reboot with
    # the stick IN - Android needs it for the T-Box half. The p100 frame reads "Do not switch off or
    # remove the stick", which is exactly right here.
    if [ -f "$SCR/parked.raw" ]; then fb "$SCR/parked.raw"; else fb "$SCR/p100.raw"; fi
    $BB sleep 3
else
    fb "$SCR/done.raw"
    n=0
    while stick_present; do
        $BB sleep 2
        n=$(($n + 1))
        [ $(($n % 15)) -eq 0 ] && fb "$SCR/done.raw"
        [ $n -gt 600 ] && break                       # ~20 min safety cap
    done
    fb "$SCR/bye.raw"
    $BB sleep 3
fi
thaw
$BB sleep 1
log "OK: install complete"
ui "Update complete."
exit 0
