找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 24|回复: 1

设备体检报告(device 模块 + adb.exec 双源互证版)

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
设备体检报告(device 模块 + adb.exec 双源互证版)

QQ截图20261103082228.png

QQ截图20261103082215.png

QQ截图20261103082205.png

  1. /**
  2. * 案例30:设备体检报告(device 模块 + adb.exec 双源互证版)
  3. *
  4. * 教学主线——"同一件事问两个来源,答案对不对当场对账":
  5. *   来源A device 模块:SDK 原生 API(getBatteryLevel/isCharging/getIP...);
  6. *   来源B adb.exec:安卓 shell 命令(dumpsys battery / df / getprop / id);
  7. *   每轮体检把两边读数摆一起:电池电量 A==B 说明链路可信;不一致就有坑。
  8. *   这就是案例20"OCR vs 控件树互证"思路在系统层信息的翻版。
  9. *
  10. * 铁律(本案例换来的,比成功值钱):
  11. *   **adb.exec 非 root 身份跑 input 系列 = 自杀**——实测 errorMsg 里
  12. *   "call killProcess pid=xxx"+"Killed",自动化 App 进程直接被打死:
  13. *   input tap/swipe 需要 INJECT_EVENTS 权限,普通 App uid 没有,
  14. *   未捕获异常触发 KillApplicationHandler。所以本案例开头先 `id` 探测身份,
  15. *   非 root 只跑只读命令(getprop/dumpsys/df),绝不碰 input。
  16. *
  17. * 新知识点:
  18. * 1. device.getBrand()/getModel()/getVersion()/getDeviceID()/getIMEI()/getOAID()——设备指纹;
  19. * 2. device.getBatteryLevel() → int(0~100)、device.isCharging() → boolean、device.getIP() → String;
  20. * 3. adb.exec(命令, isroot) → String:**省略 "adb shell" 前缀**,只填后半段;
  21. *    返回永远是 JSON 包 {"errorMsg":"","success":"命令输出"}——
  22. *    成功时输出装在 success(errorMsg 空),失败时异常栈装在 errorMsg(success 空);
  23. *    判失败必须解析字段,**拿 indexOf("errorMsg") 判会全军覆没**(键名永远在,第一轮实测锤死);
  24. *    之前那次"崩 App"就是 errorMsg 装着 killProcess 栈——非 root 跑 input 的铁证。
  25. * 4. dumpsys battery 输出里有 "level: 85" 一行——shell 文本用 indexOf 抠数字(ES5);
  26. * 5. df /sdcard 输出最后一行是挂载点数据,倒数第4列是剩余空间。
  27. *
  28. * 实测踩坑表(跑完按日志修订):
  29. * - adb.exec 的 isroot=true 前提是模拟器开了 root(雷电:设置→其他设置→ROOT→开→重启),
  30. *   没开 root 填 true 的后果未知——本案例只在 `id` 探到 uid=0 后才演示 root 命令;
  31. * - getIMEI 在模拟器上可能是空串或固定值(合法隐私限制,空不等于失败);
  32. * - dumpsys battery 在部分 ROM 上 level 是假的(USB 模拟 100),和 device 读数不一致时
  33. *   先怀疑 ROM 而不是怀疑代码;
  34. * - 长耗时 native 调用前先打日志(案例28 铁律,adb.exec 同样适用)。
  35. */

  36. // ========== 配置区(改这里) ==========
  37. var 体检间隔秒 = 15;              // 每 15 秒一轮体检
  38. var SQLite路径 = "/sdcard/体检记录.db";
  39. var 表名 = "health_records";

  40. // ========== 全局变量顶部声明 ==========
  41. var gStop = false;
  42. var 数据库连接 = null;
  43. var 数据库可用 = false;
  44. var 累计轮数 = 0;
  45. var 累计入库条数 = 0;
  46. var 品牌 = "";
  47. var 型号 = "";
  48. var 系统版本 = "";
  49. var 设备ID = "";
  50. var root可用 = false;             // id 探测结果,决定要不要演示 root 命令

  51. // ========== importClass 只能放最外层 ==========
  52. importClass(android.database.sqlite.SQLiteDatabase);
  53. importClass(java.io.File);

  54. // ========== 工具函数(先定义后调用) ==========

  55. // adb.exec 安全封装
  56. // 实测锤死返回格式:永远是 JSON 包 {"errorMsg":"...","success":"..."}——
  57. // 成功:success=命令输出, errorMsg 空;失败:errorMsg=异常栈, success 空。
  58. // 千万别拿 indexOf("errorMsg") 判失败——键名永远都在(第一轮实测全误判的根因)
  59. function adb跑(命令, 用root) {
  60.     var out = "";
  61.     try {
  62.         printl("🖥 shell> " + 命令 + (用root ? " (root)" : ""));
  63.         out = "" + adb.exec(命令, 用root);
  64.     } catch (e) {
  65.         return { ok: false, text: "抛异常: " + e };
  66.     }
  67.     var 输出 = out;
  68.     var 错误 = "";
  69.     if (out.indexOf(""success"") >= 0) {
  70.         try {
  71.             var obj = JSON.parse(out);
  72.             输出 = obj.success != null ? "" + obj.success : "";
  73.             错误 = obj.errorMsg != null ? "" + obj.errorMsg : "";
  74.         } catch (e2) {
  75.             // JSON.parse 挂了就正则抠 success 字段
  76.             var m = out.match(/"success":"([\s\S]*?)"\}/);
  77.             输出 = m ? m[1].replace(/\\n/g, "\n") : out;
  78.         }
  79.     }
  80.     if (错误.length > 0) {
  81.         return { ok: false, text: 错误 };
  82.     }
  83.     return { ok: true, text: 输出 };
  84. }

  85. // 从 shell 文本里抠 "关键字: 数字" 的数字(ES5,不依赖正则也行,这里用正则更清楚)
  86. function 抠数字(文本, 关键字) {
  87.     var p = new RegExp(关键字 + ":\\s*(\\d+)");
  88.     var m = 文本.match(p);
  89.     if (m) return parseInt(m[1], 10);
  90.     return -1;
  91. }

  92. function dbConnect() {
  93.     try {
  94.         数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
  95.         数据库可用 = true;
  96.         printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
  97.     } catch (e) {
  98.         数据库可用 = false;
  99.         printl("[DB] ⚠ 打开失败(只打印不入库): " + e);
  100.     }
  101. }

  102. function dbEnsureTable() {
  103.     if (!数据库可用) return;
  104.     try {
  105.         var sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
  106.             "id INTEGER PRIMARY KEY AUTOINCREMENT," +
  107.             "brand TEXT, model TEXT, ip TEXT," +
  108.             "battery_sdk INTEGER, battery_shell INTEGER, charging INTEGER," +
  109.             "storage_free TEXT, root INTEGER, collected_at INTEGER)";
  110.         数据库连接.execSQL(sql);
  111.         printl("[DB] ✅ 表已就绪: " + 表名);
  112.     } catch (e) {
  113.         printl("[DB] ⚠ 建表失败: " + e);
  114.         数据库可用 = false;
  115.     }
  116. }

  117. function dbInsertHealth(信息) {
  118.     if (!数据库可用) return false;
  119.     try {
  120.         var stmt = 数据库连接.compileStatement("INSERT INTO " + 表名 +
  121.             " (brand, model, ip, battery_sdk, battery_shell, charging, storage_free, root, collected_at)" +
  122.             " VALUES (?,?,?,?,?,?,?,?,?)");
  123.         stmt.bindString(1, "" + 信息.品牌);
  124.         stmt.bindString(2, "" + 信息.型号);
  125.         stmt.bindString(3, "" + 信息.ip);
  126.         stmt.bindLong(4, 信息.电量sdk);
  127.         stmt.bindLong(5, 信息.电量shell);
  128.         stmt.bindLong(6, 信息.充电中 ? 1 : 0);
  129.         stmt.bindString(7, "" + 信息.剩余空间);
  130.         stmt.bindLong(8, root可用 ? 1 : 0);
  131.         stmt.bindLong(9, time.nowStamp());
  132.         stmt.executeInsert();
  133.         stmt.close();
  134.         return true;
  135.     } catch (e) {
  136.         printl("[DB] ❌ 入库失败: " + e);
  137.         return false;
  138.     }
  139. }

  140. function dbClose() {
  141.     if (数据库可用 && 数据库连接 != null) {
  142.         try { 数据库连接.close(); } catch (e) {}
  143.         数据库连接 = null;
  144.     }
  145. }

  146. // 静态指纹只采一次(device 模块全家桶)
  147. function 采集设备指纹() {
  148.     try { 品牌 = "" + device.getBrand(); } catch (e) { 品牌 = "取失败:" + e; }
  149.     try { 型号 = "" + device.getModel(); } catch (e) { 型号 = "取失败:" + e; }
  150.     try { 系统版本 = "" + device.getVersion(); } catch (e) { 系统版本 = "取失败:" + e; }
  151.     try { 设备ID = "" + device.getDeviceID(); } catch (e) { 设备ID = "取失败:" + e; }
  152.     var imei = "";
  153.     var oaid = "";
  154.     try { imei = "" + device.getIMEI(); } catch (e) { imei = "异常"; }
  155.     try { oaid = "" + device.getOAID(); } catch (e) { oaid = "异常"; }
  156.     printl("📱 指纹: " + 品牌 + " / " + 型号 + " / Android " + 系统版本 + " / ID=" + 设备ID);
  157.     printl("🔐 IMEI='" + imei + "' OAID='" + oaid + "'(模拟器上常为空/固定值,空≠失败)");
  158. }

  159. // 身份探测:决定本轮起所有 root 演示能不能跑
  160. function 探测root身份() {
  161.     var r = adb跑("id", false);
  162.     if (!r.ok) {
  163.         printl("⚠ adb.exec('id') 都失败,shell 通道本身有问题: " + r.text);
  164.         root可用 = false;
  165.         return;
  166.     }
  167.     root可用 = r.text.indexOf("uid=0") >= 0;
  168.     printl("🎭 shell 身份: " + r.text.replace(/\n/g, " ") +
  169.         " → " + (root可用 ? "✅ root 可用" : "⚠ 非 root——input 系列命令一律禁跑(跑了 App 就崩)"));
  170. }

  171. // 一轮体检:device 读数 vs shell 读数对账 + 存储 + IP
  172. function 体检一轮() {
  173.     // 读数A:SDK 侧
  174.     var 电量sdk = -1;
  175.     var 充电中 = false;
  176.     var ip = "";
  177.     try { 电量sdk = device.getBatteryLevel(); } catch (e) { printl("⚠ getBatteryLevel 异常: " + e); }
  178.     try { 充电中 = device.isCharging(); } catch (e) { printl("⚠ isCharging 异常: " + e); }
  179.     try { ip = "" + device.getIP(); } catch (e) { ip = "取失败"; }

  180.     // 读数B:shell 侧(dumpsys battery 里抠 level)
  181.     var 电量shell = -1;
  182.     var r = adb跑("dumpsys battery", false);
  183.     if (r.ok) 电量shell = 抠数字(r.text, "level");

  184.     // 对账
  185.     if (电量sdk >= 0 && 电量shell >= 0) {
  186.         if (电量sdk === 电量shell) {
  187.             printl("🔋 电量双源一致: SDK=" + 电量sdk + " shell=" + 电量shell + " ✅ 链路可信");
  188.         } else {
  189.             printl("🔋 电量双源不一致: SDK=" + 电量sdk + " shell=" + 电量shell +
  190.                 " ⚠ 先怀疑 ROM(dumpsys 可能是 USB 模拟值),再怀疑代码");
  191.         }
  192.     } else {
  193.         printl("🔋 电量读数缺失: SDK=" + 电量sdk + " shell=" + 电量shell + "(某一源没拿到)");
  194.     }
  195.     printl("⚡ 充电状态(SDK): " + (充电中 ? "充电中" : "未充电") + " | IP(SDK): " + ip);

  196.     // 存储:df /sdcard 最后一行,倒数第4列是剩余
  197.     var 剩余空间 = "取失败";
  198.     var r2 = adb跑("df /sdcard", false);
  199.     if (r2.ok) {
  200.         var 行 = r2.text.replace(/\s+$/g, "").split("\n");
  201.         var 末行 = 行[行.length - 1].split(/\s+/);
  202.         if (末行.length >= 4) 剩余空间 = 末行[末行.length - 4];
  203.         printl("💽 /sdcard 剩余: " + 剩余空间 + " (df 末行倒数第4列)");
  204.     } else {
  205.         printl("⚠ df 失败: " + r2.text);
  206.     }

  207.     // root 演示:只在身份探测通过后跑,只读命令
  208.     if (root可用) {
  209.         var r3 = adb跑("getprop ro.product.manufacturer", true);
  210.         printl("👑 root 演示 getprop 厂商: " + (r3.ok ? r3.text.replace(/\n/g, "") : "失败 " + r3.text));
  211.     }

  212.     var 信息 = { 品牌: 品牌, 型号: 型号, ip: ip, 电量sdk: 电量sdk,
  213.         电量shell: 电量shell, 充电中: 充电中, 剩余空间: 剩余空间 };
  214.     if (dbInsertHealth(信息)) {
  215.         累计入库条数++;
  216.         printl("💾 已入库 [" + 累计入库条数 + "] 电量 " + 电量sdk + "/" + 电量shell + " 剩余 " + 剩余空间);
  217.     }
  218. }

  219. // ========== 主流程 ==========
  220. printl("===== 案例30 设备体检报告(device + adb.exec 双源互证)启动 =====");
  221. dbConnect();
  222. dbEnsureTable();
  223. printl("⚙ 配置:每 " + 体检间隔秒 + " 秒一轮 · 库: " + SQLite路径);
  224. printl("ℹ 铁律:adb.exec 非 root 跑 input 系列会把自己 App 打死(实测 killProcess),本案例只跑只读命令");
  225. 采集设备指纹();
  226. 探测root身份();

  227. // 停止按钮清理(顺序:先置标志,stopAllRuning 放最后)
  228. event.onStopBtnEvent(function () {
  229.     gStop = true;
  230.     dbClose();
  231.     runTime.stopAllRuning();
  232. });

  233. while (!gStop) {
  234.     累计轮数++;
  235.     printl("\n---- 第 " + 累计轮数 + " 轮体检 ----");
  236.     体检一轮();

  237.     // 分片等待,每 5 秒打进度
  238.     var 剩余 = 体检间隔秒;
  239.     while (剩余 > 0 && !gStop) {
  240.         var 步长 = 剩余 >= 5 ? 5 : 剩余;
  241.         printl("⏳ 下轮体检...剩余 " + 剩余 + " 秒(累计 " + 累计轮数 + " 轮/入库 " + 累计入库条数 + " 条)");
  242.         sleep.second(秒 = 步长);
  243.         剩余 = 剩余 - 步长;
  244.     }
  245. }

  246. dbClose();
  247. printl("===== 流程结束:共 " + 累计轮数 + " 轮,入库 " + 累计入库条数 + " 条,库: " + SQLite路径 + " =====");
复制代码


0

主题

2

回帖

4

积分

软件网站新手

积分
4
发表于 半小时前 | 显示全部楼层

设备体检报告(device 模块 + adb.exec 双源互证版)

看了下图,device 模块和 adb.exec 双源互证确实挺有意思。不过我注意到你提到 adb.exec 非 root 身份,那能跑哪些命令呢?好像只有系统级的吧,不 root 身份能执行哪些具体命令来验证呢?
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|

GMT+8, 2026-11-4 07:48

安卓脚本软件 · 键鼠盒子 · HID · 机械臂

快速回复 返回顶部 返回列表

随机推荐 · 热帖 / 新帖 / 随便看看

社区会员(随机展示)