|
|
设备体检报告(device 模块 + adb.exec 双源互证版)
- /**
- * 案例30:设备体检报告(device 模块 + adb.exec 双源互证版)
- *
- * 教学主线——"同一件事问两个来源,答案对不对当场对账":
- * 来源A device 模块:SDK 原生 API(getBatteryLevel/isCharging/getIP...);
- * 来源B adb.exec:安卓 shell 命令(dumpsys battery / df / getprop / id);
- * 每轮体检把两边读数摆一起:电池电量 A==B 说明链路可信;不一致就有坑。
- * 这就是案例20"OCR vs 控件树互证"思路在系统层信息的翻版。
- *
- * 铁律(本案例换来的,比成功值钱):
- * **adb.exec 非 root 身份跑 input 系列 = 自杀**——实测 errorMsg 里
- * "call killProcess pid=xxx"+"Killed",自动化 App 进程直接被打死:
- * input tap/swipe 需要 INJECT_EVENTS 权限,普通 App uid 没有,
- * 未捕获异常触发 KillApplicationHandler。所以本案例开头先 `id` 探测身份,
- * 非 root 只跑只读命令(getprop/dumpsys/df),绝不碰 input。
- *
- * 新知识点:
- * 1. device.getBrand()/getModel()/getVersion()/getDeviceID()/getIMEI()/getOAID()——设备指纹;
- * 2. device.getBatteryLevel() → int(0~100)、device.isCharging() → boolean、device.getIP() → String;
- * 3. adb.exec(命令, isroot) → String:**省略 "adb shell" 前缀**,只填后半段;
- * 返回永远是 JSON 包 {"errorMsg":"","success":"命令输出"}——
- * 成功时输出装在 success(errorMsg 空),失败时异常栈装在 errorMsg(success 空);
- * 判失败必须解析字段,**拿 indexOf("errorMsg") 判会全军覆没**(键名永远在,第一轮实测锤死);
- * 之前那次"崩 App"就是 errorMsg 装着 killProcess 栈——非 root 跑 input 的铁证。
- * 4. dumpsys battery 输出里有 "level: 85" 一行——shell 文本用 indexOf 抠数字(ES5);
- * 5. df /sdcard 输出最后一行是挂载点数据,倒数第4列是剩余空间。
- *
- * 实测踩坑表(跑完按日志修订):
- * - adb.exec 的 isroot=true 前提是模拟器开了 root(雷电:设置→其他设置→ROOT→开→重启),
- * 没开 root 填 true 的后果未知——本案例只在 `id` 探到 uid=0 后才演示 root 命令;
- * - getIMEI 在模拟器上可能是空串或固定值(合法隐私限制,空不等于失败);
- * - dumpsys battery 在部分 ROM 上 level 是假的(USB 模拟 100),和 device 读数不一致时
- * 先怀疑 ROM 而不是怀疑代码;
- * - 长耗时 native 调用前先打日志(案例28 铁律,adb.exec 同样适用)。
- */
- // ========== 配置区(改这里) ==========
- var 体检间隔秒 = 15; // 每 15 秒一轮体检
- var SQLite路径 = "/sdcard/体检记录.db";
- var 表名 = "health_records";
- // ========== 全局变量顶部声明 ==========
- var gStop = false;
- var 数据库连接 = null;
- var 数据库可用 = false;
- var 累计轮数 = 0;
- var 累计入库条数 = 0;
- var 品牌 = "";
- var 型号 = "";
- var 系统版本 = "";
- var 设备ID = "";
- var root可用 = false; // id 探测结果,决定要不要演示 root 命令
- // ========== importClass 只能放最外层 ==========
- importClass(android.database.sqlite.SQLiteDatabase);
- importClass(java.io.File);
- // ========== 工具函数(先定义后调用) ==========
- // adb.exec 安全封装
- // 实测锤死返回格式:永远是 JSON 包 {"errorMsg":"...","success":"..."}——
- // 成功:success=命令输出, errorMsg 空;失败:errorMsg=异常栈, success 空。
- // 千万别拿 indexOf("errorMsg") 判失败——键名永远都在(第一轮实测全误判的根因)
- function adb跑(命令, 用root) {
- var out = "";
- try {
- printl("🖥 shell> " + 命令 + (用root ? " (root)" : ""));
- out = "" + adb.exec(命令, 用root);
- } catch (e) {
- return { ok: false, text: "抛异常: " + e };
- }
- var 输出 = out;
- var 错误 = "";
- if (out.indexOf(""success"") >= 0) {
- try {
- var obj = JSON.parse(out);
- 输出 = obj.success != null ? "" + obj.success : "";
- 错误 = obj.errorMsg != null ? "" + obj.errorMsg : "";
- } catch (e2) {
- // JSON.parse 挂了就正则抠 success 字段
- var m = out.match(/"success":"([\s\S]*?)"\}/);
- 输出 = m ? m[1].replace(/\\n/g, "\n") : out;
- }
- }
- if (错误.length > 0) {
- return { ok: false, text: 错误 };
- }
- return { ok: true, text: 输出 };
- }
- // 从 shell 文本里抠 "关键字: 数字" 的数字(ES5,不依赖正则也行,这里用正则更清楚)
- function 抠数字(文本, 关键字) {
- var p = new RegExp(关键字 + ":\\s*(\\d+)");
- var m = 文本.match(p);
- if (m) return parseInt(m[1], 10);
- return -1;
- }
- function dbConnect() {
- try {
- 数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
- 数据库可用 = true;
- printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
- } catch (e) {
- 数据库可用 = false;
- printl("[DB] ⚠ 打开失败(只打印不入库): " + e);
- }
- }
- function dbEnsureTable() {
- if (!数据库可用) return;
- try {
- var sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
- "id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "brand TEXT, model TEXT, ip TEXT," +
- "battery_sdk INTEGER, battery_shell INTEGER, charging INTEGER," +
- "storage_free TEXT, root INTEGER, collected_at INTEGER)";
- 数据库连接.execSQL(sql);
- printl("[DB] ✅ 表已就绪: " + 表名);
- } catch (e) {
- printl("[DB] ⚠ 建表失败: " + e);
- 数据库可用 = false;
- }
- }
- function dbInsertHealth(信息) {
- if (!数据库可用) return false;
- try {
- var stmt = 数据库连接.compileStatement("INSERT INTO " + 表名 +
- " (brand, model, ip, battery_sdk, battery_shell, charging, storage_free, root, collected_at)" +
- " VALUES (?,?,?,?,?,?,?,?,?)");
- stmt.bindString(1, "" + 信息.品牌);
- stmt.bindString(2, "" + 信息.型号);
- stmt.bindString(3, "" + 信息.ip);
- stmt.bindLong(4, 信息.电量sdk);
- stmt.bindLong(5, 信息.电量shell);
- stmt.bindLong(6, 信息.充电中 ? 1 : 0);
- stmt.bindString(7, "" + 信息.剩余空间);
- stmt.bindLong(8, root可用 ? 1 : 0);
- stmt.bindLong(9, time.nowStamp());
- stmt.executeInsert();
- stmt.close();
- return true;
- } catch (e) {
- printl("[DB] ❌ 入库失败: " + e);
- return false;
- }
- }
- function dbClose() {
- if (数据库可用 && 数据库连接 != null) {
- try { 数据库连接.close(); } catch (e) {}
- 数据库连接 = null;
- }
- }
- // 静态指纹只采一次(device 模块全家桶)
- function 采集设备指纹() {
- try { 品牌 = "" + device.getBrand(); } catch (e) { 品牌 = "取失败:" + e; }
- try { 型号 = "" + device.getModel(); } catch (e) { 型号 = "取失败:" + e; }
- try { 系统版本 = "" + device.getVersion(); } catch (e) { 系统版本 = "取失败:" + e; }
- try { 设备ID = "" + device.getDeviceID(); } catch (e) { 设备ID = "取失败:" + e; }
- var imei = "";
- var oaid = "";
- try { imei = "" + device.getIMEI(); } catch (e) { imei = "异常"; }
- try { oaid = "" + device.getOAID(); } catch (e) { oaid = "异常"; }
- printl("📱 指纹: " + 品牌 + " / " + 型号 + " / Android " + 系统版本 + " / ID=" + 设备ID);
- printl("🔐 IMEI='" + imei + "' OAID='" + oaid + "'(模拟器上常为空/固定值,空≠失败)");
- }
- // 身份探测:决定本轮起所有 root 演示能不能跑
- function 探测root身份() {
- var r = adb跑("id", false);
- if (!r.ok) {
- printl("⚠ adb.exec('id') 都失败,shell 通道本身有问题: " + r.text);
- root可用 = false;
- return;
- }
- root可用 = r.text.indexOf("uid=0") >= 0;
- printl("🎭 shell 身份: " + r.text.replace(/\n/g, " ") +
- " → " + (root可用 ? "✅ root 可用" : "⚠ 非 root——input 系列命令一律禁跑(跑了 App 就崩)"));
- }
- // 一轮体检:device 读数 vs shell 读数对账 + 存储 + IP
- function 体检一轮() {
- // 读数A:SDK 侧
- var 电量sdk = -1;
- var 充电中 = false;
- var ip = "";
- try { 电量sdk = device.getBatteryLevel(); } catch (e) { printl("⚠ getBatteryLevel 异常: " + e); }
- try { 充电中 = device.isCharging(); } catch (e) { printl("⚠ isCharging 异常: " + e); }
- try { ip = "" + device.getIP(); } catch (e) { ip = "取失败"; }
- // 读数B:shell 侧(dumpsys battery 里抠 level)
- var 电量shell = -1;
- var r = adb跑("dumpsys battery", false);
- if (r.ok) 电量shell = 抠数字(r.text, "level");
- // 对账
- if (电量sdk >= 0 && 电量shell >= 0) {
- if (电量sdk === 电量shell) {
- printl("🔋 电量双源一致: SDK=" + 电量sdk + " shell=" + 电量shell + " ✅ 链路可信");
- } else {
- printl("🔋 电量双源不一致: SDK=" + 电量sdk + " shell=" + 电量shell +
- " ⚠ 先怀疑 ROM(dumpsys 可能是 USB 模拟值),再怀疑代码");
- }
- } else {
- printl("🔋 电量读数缺失: SDK=" + 电量sdk + " shell=" + 电量shell + "(某一源没拿到)");
- }
- printl("⚡ 充电状态(SDK): " + (充电中 ? "充电中" : "未充电") + " | IP(SDK): " + ip);
- // 存储:df /sdcard 最后一行,倒数第4列是剩余
- var 剩余空间 = "取失败";
- var r2 = adb跑("df /sdcard", false);
- if (r2.ok) {
- var 行 = r2.text.replace(/\s+$/g, "").split("\n");
- var 末行 = 行[行.length - 1].split(/\s+/);
- if (末行.length >= 4) 剩余空间 = 末行[末行.length - 4];
- printl("💽 /sdcard 剩余: " + 剩余空间 + " (df 末行倒数第4列)");
- } else {
- printl("⚠ df 失败: " + r2.text);
- }
- // root 演示:只在身份探测通过后跑,只读命令
- if (root可用) {
- var r3 = adb跑("getprop ro.product.manufacturer", true);
- printl("👑 root 演示 getprop 厂商: " + (r3.ok ? r3.text.replace(/\n/g, "") : "失败 " + r3.text));
- }
- var 信息 = { 品牌: 品牌, 型号: 型号, ip: ip, 电量sdk: 电量sdk,
- 电量shell: 电量shell, 充电中: 充电中, 剩余空间: 剩余空间 };
- if (dbInsertHealth(信息)) {
- 累计入库条数++;
- printl("💾 已入库 [" + 累计入库条数 + "] 电量 " + 电量sdk + "/" + 电量shell + " 剩余 " + 剩余空间);
- }
- }
- // ========== 主流程 ==========
- printl("===== 案例30 设备体检报告(device + adb.exec 双源互证)启动 =====");
- dbConnect();
- dbEnsureTable();
- printl("⚙ 配置:每 " + 体检间隔秒 + " 秒一轮 · 库: " + SQLite路径);
- printl("ℹ 铁律:adb.exec 非 root 跑 input 系列会把自己 App 打死(实测 killProcess),本案例只跑只读命令");
- 采集设备指纹();
- 探测root身份();
- // 停止按钮清理(顺序:先置标志,stopAllRuning 放最后)
- event.onStopBtnEvent(function () {
- gStop = true;
- dbClose();
- runTime.stopAllRuning();
- });
- while (!gStop) {
- 累计轮数++;
- printl("\n---- 第 " + 累计轮数 + " 轮体检 ----");
- 体检一轮();
- // 分片等待,每 5 秒打进度
- var 剩余 = 体检间隔秒;
- while (剩余 > 0 && !gStop) {
- var 步长 = 剩余 >= 5 ? 5 : 剩余;
- printl("⏳ 下轮体检...剩余 " + 剩余 + " 秒(累计 " + 累计轮数 + " 轮/入库 " + 累计入库条数 + " 条)");
- sleep.second(秒 = 步长);
- 剩余 = 剩余 - 步长;
- }
- }
- dbClose();
- printl("===== 流程结束:共 " + 累计轮数 + " 轮,入库 " + 累计入库条数 + " 条,库: " + SQLite路径 + " =====");
复制代码
|
|