HackerSim 完整源码（含 Root 版）
═══════════════════════════════════════════════
打包时间: 2026-10-31 15:58:51 CST

包含两个工程
────────────
HackerSim/         标准版      v8.9.0 (code 39)
                   关卡解谜 + 桌宠 + 本地 AI + 密钥包机制
HackerSimRoot/     Root 版     v9.3.0 (code 111)
                   额外含「真实权限终端」：root / Shizuku / shell 三通道

两个工程是独立的（不同包名、不同签名），可同时安装。

Root 版新增文件
───────────────
HackerSimRoot/app/src/main/java/com/example/hackersimroot/
  perm/PermissionBridge.java   权限通道统一入口（自动选择 + 手动切换）
  perm/RootChannel.java        su 通道（uid 0）
  perm/ShizukuChannel.java     Shizuku 通道（uid 2000 / ADB 级）
  perm/ShellChannel.java       本地 shell 降级兜底
  perm/CommandResult.java      命令结果模型
  PrivTerminalActivity.java    真实权限终端
  RootStatusActivity.java      Root 环境自检面板

HackerSimRoot/app/src/main/java/rikka/shizuku/
  ShizukuProcessBridge.java    Shizuku 进程创建桥接
    （ShizukuRemoteProcess 构造器为包级私有，须同包访问）

Root 版关键配置
───────────────
minSdk 24   Shizuku 硬性要求（其 APK 声明 sdkVersion 24）
依赖        dev.rikka.shizuku:api:13.1.5
            dev.rikka.shizuku:provider:13.1.5
清单        provider rikka.shizuku.ShizukuProvider
            authorities ${applicationId}.shizuku
            queries  moe.shizuku.privileged.api

⚠ llama.cpp 未包含
──────────────────
为控制体积，上游 llama.cpp 仓库未打进本包。
编译前需自行拉取（详见 app/src/main/cpp/llama/如何获取llama.cpp.txt）：

  cd HackerSimRoot/app/src/main/cpp
  git clone https://github.com/ggml-org/llama.cpp.git llama

  标准版同理：
  cd HackerSim/app/src/main/cpp
  git clone https://github.com/ggml-org/llama.cpp.git llama

⚠ Shizuku 未包含
─────────────────
Shizuku 是第三方开源软件（Rikka 所有，Apache-2.0），
本工程只通过官方客户端 SDK 连接它，不打包、不分发其本体。
用户需自行安装 Shizuku 并在其内启动服务、授予本应用权限。

v9.1.5 变更记录（权限版）—— 修复跳转入口消失
──────────────────────────────────────────────────────────
[现象] 用户反馈：权限版 AI 助手里「打开标准版 AI」按钮不见了。

[根因] 按钮显隐判断用的是 SharedModel.peerHasModel() —— 那是
  【bind-mount 探测】顺带得出的副产品。而那条探测链路有两个死穴：

  ① 前置条件挡住了结论
     doProbe() 里，非 root 通道会先 fail(R_NO_ROOT) 再往下走；
     若通道是 shell（Shizuku 没连/被系统杀），连「判定标准版有无模型」
     这一步都到不了，sReason 停在 R_NO_ROOT —— 按钮永不出现。

  ② 失败结果被 TTL 缓存
     sReason != R_NONE 且未过期时，ensureAsync 直接回调返回，
     根本不跑探测；期间反复进 AI 页读到的都是旧状态。

[修复] 把「要不要显示跳转按钮」从挂载探测里彻底解耦。
  PeerAiLauncher 新增独立状态缓存与探测：
    · ensureProbeAsync()  后台探测，幂等、不叠加线程
    · shouldOfferPeer()   主线程只读，不跑命令不阻塞
    · 三态：HAS_MODEL / NO_MODEL / UNKNOWN
    · 探测顺序：先公共目录直读（零特权）→ 再特权通道 stat
    · 不依赖 root —— 任何通道下都尝试判定，通道不可用时标 UNKNOWN
    · TTL 从 90s 缩到 20s，装好模型后能很快识别

[顺带]
  · 兜底入口：标准版已装但探不到模型时，也给「打开标准版看看」
  · AI 页横幅可点击 → 手动重置两处缓存并重探
  · MainActivity 首页也预热一次探测，进 AI 页按钮即现成

v9.1.4 变更记录（权限版）—— 本机 IP 查询
──────────────────────────────────────────────────────────
[背景] 用户问「软件里能不能查 IP」。排查后发现：命令仿真层里的
  ifconfig / ip 全是演出效果（写死 10.0.7.7），拿它当"查 IP"用
  会被误导；真正能查到 IP 的只有特权终端手敲 ip addr。

[做法] 新增 NetInfo.java —— 纯 Java 遍历本机网卡
  · 用 NetworkInterface 而非 WifiManager：
      WifiManager.getConnectionInfo() 在 Android 10+ 已废弃，
      且需要 ACCESS_WIFI_STATE、未连 WiFi 时返回 0。
      NetworkInterface 是 Java 标准库，读内核暴露的网卡列表，
      任何 App 都能读 —— 不需要权限、不需要 root、不需要 Shizuku。
  · 按网卡名打分挑「本机 IP」：
      wlan(100) > eth/en(90) > 热点(80) > VPN(60) > 蜂窝(20)
      因为用户问"我 IP 多少"时想要的是 WiFi 下的那个地址。
  · 拿不到公网出口 IP（那需要外部服务器回显），本类刻意不联网。

[接入的三个入口]
  1. PrivTerminalActivity：快捷按钮加「本机 IP」(__MYIP__ 标记)，
     点击直达内置实现，不走权限通道 —— 任何通道状态下都能出结果；
     另支持手敲 myip 内置命令。
  2. CommandEngine：ifconfig / ip 改为优先输出真机网卡，
     读不到才回落原模拟数据；新增 myip / ipconfig（轻量，不弹窗）。
  3. MainActivity：首页权限版横幅那行追加「本机 IP: x.x.x.x」，
     点击复制到剪贴板。

[验证] versionCode 104 / versionName 9.1.4，签名指纹与旧版一致
  （05:70:6B:E5:...:76:52），可覆盖安装。

v9.1.2 变更记录（权限版）—— 无 root 时引导跳转
──────────────────────────────────────────────────────────
[背景] 用户手机没有 root，只有 Shizuku（uid 2000）。
  v9.1 的模型共享靠 mount --bind，而 mount 需要 root（uid 0），
  所以在 Shizuku 通道下必然失败，AI 页显示「需要 root 通道」。

[排除的方案]
  · bind-mount          → 需 uid 0；Shizuku uid 2000 挂不了
  · 流式读给 llama.cpp  → 不可行。llama.cpp 用 mmap()，要求文件
                          可随机 seek；管道（pipe）不可 seek
  · 完整拷贝 468MB      → 可行，但用户不愿再多占空间
  · 公共目录共享        → 标准版 targetSdk 34 无法直接写公共目录；
                          权限版读他人写的公共文件亦受分区存储限制

[采用的方案] 引导跳转 —— 模型既已在标准版里，直接去标准版用
  1. 标准版 v8.7.3：给 AiChatActivity 加导出入口
       android:exported="true"
       intent-filter action = com.example.hackersim.action.OPEN_AI
     仅暴露「打开 AI 页」这一个无参、无回传的入口。
  2. 权限版 v9.1.2：
     · 新增 engine/PeerAiLauncher.java
         installed()      —— 探标准版是否安装
         peerModelReady() —— 用特权通道 stat 探标准版模型是否就绪
                             （只回一行字节数，绝不 cat）
         openPeerAi()     —— 优先显式 ComponentName 直达 AI 页；
                             失败则 getLaunchIntentForPackage 兜底
     · SharedModel 新增
         PUBLIC_PATHS 探测层（无需特权即可读的公共目录）
         R_PEER_HAS_MODEL 状态 + peerHasModel() 查询
         全部路径失败后判定「标准版是否有模型」以决定引导方向
     · AiChatActivity
         新增 btn_open_peer 按钮（默认隐藏）
         afterProbe() 三路分流：本机可用 / 引导跳转 / 常规下载
         checkModel() 去掉无脑 reset()，改由 ensureAsync 的 TTL 缓存
         控制重探频率，避免每进一次页面就跑特权命令
     · MainActivity 首页横幅加「标准版已有模型」分支

[降级链路]
  无标准版       → 提示下载
  有标准版无模型 → 提示下载
  有模型无 root  → 引导跳转（本次重点）
  有 root        → bind-mount 零拷贝（原有逻辑保留）

[保持] 权限版自身下载模型的能力保留（终端 ai download 照旧可用）。

v9.1.1 变更记录（权限版）—— 修 AI 调不到模型 + 点返回闪退 + 更名
──────────────────────────────────────────────────────────
[更名] 「Root 版」→「权限版」
  功能不变，只改用户可见文案。包名 com.example.hackersimroot 与
  签名指纹保持不变，可直接覆盖安装旧版。

[Bug A] AI 助手调不到标准版的模型
  三个叠加原因：
    1. AiChatActivity.checkModel() 在【主线程】调 SharedModel.resolve()。
       该方法会执行 stat / mountpoint / mount --bind 特权命令，且首次
       还要调 RootChannel 探测 su（内部 su -c id 最长阻塞 5s）。
       主线程跑阻塞 IO → 界面卡死甚至闪退。
    2. 探测时机太早：RootApp.onCreate 起线程探测时，root 通道可能还没
       握手完，isAvailable() 返回 false → 判定「root 不可用」。
    3. 失败结果【永久缓存】：原 sProbed 一旦置 true，之后永远返回
       null，即使后来 root 授权了、标准版装好了也不会重试。

  修复：
    · SharedModel 重构为异步模型
        - ensureAsync(ctx, cb)：后台线程探测（幂等，不叠加线程）
        - 主线程只读缓存：sharedPathIfReady / isShared
        - 失败原因分类：R_OK(终态) / R_NO_CHANNEL(不缓存，立即重试)
          / R_NO_ROOT / R_NO_PEER / R_NO_MODEL / R_ERROR
        - 失败缓存带 90s TTL，过期自动允许重探
        - 探测串行化（PROBE_LOCK），避免多线程同时跑特权命令
    · AiChatActivity.checkModel → 改走 ensureAsync + 回调刷新 UI
    · RootApp.onCreate → 改走 ensureAsync
    · 失败时横幅会展示具体原因（如「root 握手未完成」），并提示
      「装了标准版并下载模型后，本页可自动引用」

[Bug B] 点返回键闪退回桌面
  真根因：onDestroy() 里 pool.shutdownNow() 会把正在执行的原生推理
  （prewarm / ask 都提交到这个 pool）用 Thread.interrupt() 硬中断，
  llama.cpp 原生层收到打断信号后异常 → 进程级崩溃 → 直接退桌面
  （不是正常 finish）。

  修复：
    · onDestroy 改为：先 AiManager.stop()（置原生停止标志，让原生
      循环自己安全退出），再 pool.shutdown()（不再收新任务、已提交的
      跑完），且【不阻塞等待】—— onDestroy 不能卡主线程
    · 显式接管 onBackPressed()：生成中先安全中断再关页面
    · FloatingAiService 有完全相同的隐患（ask 也跑在它的 pool 里），
      onDestroy 的 shutdownNow 一并改为 shutdown

v9.1 变更记录（Root 版）—— 去密钥包 + 直接引用标准版模型
──────────────────────────────────────────────────────────
[变更 1] 去掉密钥包 / 内容包解锁机制，全部关卡直接开放
  Root 用户本就握有最高权限，再走一遍「导入密钥包 → 解锁关卡 2~6」
  纯属自我为难。本次把整套解锁链路整体短路：

  · ProgressManager
      ALL_UNLOCKED = true
      isLevelUnlocked()    → 直接 return true
      isLockedByActivation() → 直接 return false
      isLockedByContent()    → 直接 return false
      isPetUnlocked()        → 直接 return true
  · AiGate
      AI_ALWAYS_UNLOCKED = true
      isUnlocked() → 直接 return true（不再要求 ContentPack 就绪）
  · MainActivity
      隐藏 btnImportPack / btnImportEncrypted 两个入口
      btnGenAccess 置为「🗂 关卡系统」常亮可点，点击直接进关卡选择
      关卡列表去掉「🔒」锁标记，直接 switch 分发
      showActivationBanner() 重写为 Root 版状态横幅

  两个编译期常量（ALL_UNLOCKED / AI_ALWAYS_UNLOCKED）保留，
  需要回退标准版行为时改回 false 即可。

[变更 2] 直接引用标准版的本地 AI 模型（零拷贝）
  目标：Root 版不再重复下载 468MB 的 qwen2.5-0.5b 模型。
  前提：设备上已装标准版 com.example.hackersim 且其模型已下载。

  ⚠ 为什么必须用 bind-mount（这是本变更的技术核心）
  跨 App 直接读别家私有目录，三条路全是死路：
    · chmod o+rx   → 无效。/storage/emulated/0/Android/data 受
                     FUSE/sdcardfs 保护，权限位改了也过不了挂载层；
                     SELinux 对 untrusted_app 读别家 app_data_file
                     默认直接拒绝。
    · ln -s 软链   → 无效。软链能建，但 open 的是目标文件本身，
                     SELinux label 照样拦。
    · ln 硬链      → 无效。跨分区 EXDEV；即便同分区，共享 inode
                     label 相同，依然拦。
    · mount --bind → 有效。挂载后访问走的是本 App 自己的路径与
                     自己的 uid，SELinux label 是自己的，绕开全部限制。

  实现（engine/SharedModel.java，v9.1 新建）：
    SRC_DIRS = [
      /storage/emulated/0/Android/data/com.example.hackersim/files/models,
      /data/data/com.example.hackersim/files/models,
    ]
    SHARED_DIR = /data/data/com.example.hackersimroot/files/models_shared

    resolve(ctx):
      1. peerInstalled()  — getPackageInfo 探测标准版是否安装
                            （清单 <queries> 已声明该包名，否则
                             Android 11+ 会抛 NameNotFoundException）
      2. 必须是 root 通道（RootChannel）才继续，Shizuku 通道降级放弃
      3. 逐个 SRC_DIR 探测模型文件（stat -c '%s'，只回一行数字）
      4. mount(src)：先 mountpoint -q 幂等判断，已挂则直接复用；
                     否则 mkdir -p SHARED_DIR && mount --bind
      5. canOpen()   — 本进程亲自 new FileInputStream 读一个字节验证

  性能红线（务必遵守）：
    RootChannel/ShizukuChannel.exec() 会把 stdout 全部 drain 进
    StringBuilder。因此探测命令【绝不能 cat 模型文件】（468MB 必 OOM），
    只允许 stat -c '%s' / mountpoint -q 这类单行输出命令。

  幂等与重挂：
    bind-mount 不跨重启。SharedModel.resolve() 在 RootApp.onCreate
    后台线程里跑一次；AiChatActivity.checkModel() 还留了惰性兜底
    （reset() 后重探一次），保证重启后仍能自动接上。

  接入点：
    AiManager.loadPath()   → 优先返回共享路径，否则回自身 models/
    AiManager.modelReady() → SharedModel.isShared() 为真则直接 ready
    nativeLoadModel() 两处调用（ask / prewarm）统一走 loadPath()
    download() → 已共享时提示「已引用标准版模型，无需下载」
    RootStatusActivity / FreeTerminalActivity → 展示共享状态

  降级安全（任一不满足即干净回落到自身下载，不报错不崩溃）：
    · 标准版未安装
    · 没走 root 通道（只有 Shizuku）
    · root 不可用
    · 标准版未下模型 / 文件小于 EXPECTED_SIZE
    · bind 挂载失败

v9.0.3 修复记录（Root 版）—— 彻底修掉 process hasn't exited
──────────────────────────────────────────────────────────
[Bug 3 真根因] 拆 Shizuku SDK 字节码后确认：
  ShizukuRemoteProcess 覆盖了 waitFor() / waitForTimeout(long,TimeUnit)
  / alive() / destroy() / exitValue()，但【没有覆盖】
  java.lang.Process.waitFor(long, TimeUnit)。

  我们原代码 proc.waitFor(60, TimeUnit.SECONDS) 调的是 Java 基类的
  默认实现（内部轮询 exitValue），在 Shizuku 远程进程上不可靠：
  它会返回，但远端退出码尚未登记 → 随后取退出码抛异常。

  且该异常不是 IllegalStateException —— 看 ShizukuRemoteProcess 字节码，
  exitValue() 里 catch 的是 android.os.RemoteException 并重新抛出
  RuntimeException。所以 v9.0.2 只 catch IllegalStateException 抓不住，
  这正是「改了还是报同样错」的原因。

  正解：改用 Shizuku 自己提供的 API：
    boolean done = srp.waitForTimeout(60, TimeUnit.SECONDS);
    if (done) code = srp.waitFor();     // 阻塞到结束，返回退出码
  并对 waitForTimeout / waitFor 各自加了兜底重试。

  已确认 IShizukuService.newProcess(String[],String[],String) 签名正确。

v9.0.2 修复记录（Root 版）—— 未修净，已被 v9.0.3 取代
───────────────────────────────────────────────────────
尝试用「轮询 exitValue + catch IllegalStateException」修复，因异常类型
判断错误而无效。保留此记录以说明排查过程。

v9.0.1 修复记录（Root 版）
─────────────────────────
[Bug 3] Shizuku 通道所有命令都失败（process hasn't exited）
  现象：授权成功、通道显示已连接（提示符 shizuku@hackersim $），
        但一执行命令就报「执行异常: process hasn't exited」。
        复现：pm list packages / getprop | head -30 / dumpsys battery 全部失败。
  根因：Shizuku 的 IRemoteProcess 退出状态登记有微小延迟。原代码
        proc.waitFor(60s) 返回后直接 proc.exitValue()，此时远端退出码
        可能还没登记好 → 抛 IllegalStateException("process hasn't exited")
        → 被 catch(Throwable) 捕获 → 整条命令报「执行异常」。
  修复：改为轮询等待退出码 —— 循环里 try { exitValue(); break; }
        catch (IllegalStateException) { sleep(100); }，直到 60s 超时。
        另把读流线程 join 从 1000ms 放宽到 3000ms，防止输出被截断。

v9.0.1 修复记录（Root 版）
─────────────────────────
[Bug 1] Shizuku 授权后 App 闪退
  现象：点「申请授权」并同意后 App 退出；在外面同意后重进自检又退出。
  根因：监听器注册在 onStart/onStop。授权页盖上来 → 本页 onStop() →
        监听器被摘除 → 用户点同意时回调无人接收 → 结果投递失败崩溃。
  修复：ShizukuChannel.register/unregister 改为按引用精确摘除 + 幂等；
        RootStatusActivity 注册时机移到 onCreate/onDestroy（覆盖整页生命周期）；
        新增 pollPermissionGranted() + onResume() 主动查询作兜底。
  另修：exec() 里误调的 isInstalled(null) 必然 NPE → 改用 isRunning()。

[Bug 2] 特权终端打长输出闪退（退回主页面）
  现象：在终端里打一串含大量输出的内置指令，App 突然退回主页面。
  根因：append() 每追加一行都 new SpannableStringBuilder(全部历史) 并全量
        setText → 复杂度 O(n²) → 输出一长主线程卡死 → 判 ANR → 系统杀 Activity。
        另 SpannableStringBuilder(CharSequence) 不复制 span，每行重设导致 span 膨胀。
  修复：改为增量追加 + 复用同一个 SpannableStringBuilder；
        加长度上限 MAX_CHARS=200000 / TRIM_TO=150000（滚动窗口）。

[加固] 全局崩溃防护
  新增 CrashGuard（Thread.setDefaultUncaughtExceptionHandler）：
  后台线程异常只记录、不杀进程；仅 OOM/StackOverflow/VMError 且来自主线程
  时才交还系统。RootApp.onCreate() 里安装。终端 status 可查最近异常记录。

编译方式
────────
  cd HackerSimRoot
  export JAVA_HOME=<JDK 17>
  export ANDROID_HOME=<Android SDK>
  ./gradlew assembleRelease

需要 NDK（编译 llama.cpp）、CMake 3.22+、JDK 17。

双包解谜密码链路（标准版）
──────────────────────────
  密码 qewrdsgti → 倒序 → 凯撒前移3 → Base64 = ZnFkcGFvdGJu
  玩家反向解：Base64 解码 → 凯撒后移3 → 倒序
