Files
ShortX_ToolHub/code/th_01_base.js

1328 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @version 1.0.1
// ToolHub - Android 悬浮球工具 (ShortX / Rhino ES5)
// 来源: 阿然 (xin-blog.com)
//
// ============================================================================
// 【操作方法】
//
// 1. 悬浮球手势
// - 单击: 打开/关闭主面板
// - 长按: 打开设置面板
// - 拖拽: 移动悬浮球位置
//
// 2. 按钮编辑
// - 长按悬浮球 → 设置面板 → 按钮管理
// - 支持类型: Shell / App / Broadcast / Intent / Content / Shortcut
//
// ============================================================================
// =======================【优化:工具函数】======================
// 统一的空日志写入(避免到处写 if (this.L) this.L.xxx
function safeLog(logger, level, msg) {
if (!logger || !logger[level]) return;
try { logger[level](msg); } catch(e) {}
}
// # 分级操作封装:关键业务抛出错误,非关键业务静默处理
function safeOperation(opName, fn, critical, logger) {
try {
return fn();
} catch (e) {
if (critical) {
// 关键业务:记录详细堆栈并重新抛出
var stack = "";
try {
var sw = new java.io.StringWriter();
var pw = new java.io.PrintWriter(sw);
e.printStackTrace(pw);
stack = String(sw.toString());
} catch(e2) {}
safeLog(logger, 'e', opName + " CRITICAL: " + String(e) + "\n" + stack);
throw e;
} else {
// 非关键业务:静默记录并返回 null
safeLog(logger, 'd', opName + " skipped: " + String(e));
return null;
}
}
}
function parseBooleanLike(value) {
if (value === true || value === false) return value;
if (value == null) return false;
if (typeof value === "number") return value !== 0;
try {
var s = String(value).replace(/^\s+|\s+$/g, "").toLowerCase();
if (s === "" || s === "0" || s === "false" || s === "no" || s === "off" || s === "null" || s === "undefined") return false;
if (s === "1" || s === "true" || s === "yes" || s === "on") return true;
} catch (e) {}
return !!value;
}
// # 配置校验层:防止用户手动编辑 JSON 导致崩溃
var ConfigValidator = {
schemas: {
// 悬浮球核心配置
BALL_SIZE_DP: { type: "int", min: 20, max: 200, default: 45 },
BALL_INIT_X: { type: "int", min: 0, max: 2000, default: 0 },
BALL_INIT_Y_DP: { type: "int", min: 0, max: 1000, default: 220 },
// 面板布局配置
PANEL_COLS: { type: "int", min: 1, max: 6, default: 1 },
PANEL_ROWS: { type: "int", min: 1, max: 10, default: 6 },
PANEL_BG_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.85 },
PANEL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 24 },
PANEL_ITEM_SIZE_DP: { type: "int", min: 48, max: 120, default: 64 },
PANEL_GAP_DP: { type: "int", min: 4, max: 24, default: 8 },
PANEL_PADDING_DP: { type: "int", min: 8, max: 32, default: 12 },
// 主题配置
THEME_MODE: { type: "enum", values: [0, 1, 2], default: 1 },
THEME_ACCENT_LIGHT: { type: "string", default: "#FF3A86FF" },
THEME_ACCENT_DARK: { type: "string", default: "#FF90CAF9" },
// 图标配置
BALL_ICON_TYPE: { type: "enum", values: ["app", "file", "android", "shortx"], default: "app" },
BALL_ICON_RES_ID: { type: "int", min: 0, max: 999999, default: 0 },
BALL_ICON_SIZE_DP: { type: "int", min: 16, max: 64, default: 22 },
BALL_PNG_MODE: { type: "int", min: 0, max: 2, default: 1 },
BALL_IDLE_ALPHA: { type: "float", min: 0.1, max: 1.0, default: 0.6 },
// 交互配置
LONG_PRESS_MS: { type: "int", min: 200, max: 2000, default: 600 },
LONG_PRESS_VIBRATE_MS: { type: "int", min: 10, max: 100, default: 40 },
CLICK_SLOP_DP: { type: "int", min: 2, max: 20, default: 6 },
// 功能开关
ENABLE_SNAP_TO_EDGE: { type: "bool", default: true },
ENABLE_ANIMATIONS: { type: "bool", default: true },
ENABLE_LONG_PRESS: { type: "bool", default: true },
ENABLE_AUTO_IDLE_DOCK: { type: "bool", default: true },
// 边缘吸附配置
EDGE_VISIBLE_RATIO: { type: "float", min: 0.3, max: 1.0, default: 0.70 },
IDLE_TIMEOUT_MS: { type: "int", min: 500, max: 5000, default: 1500 },
// 面板位置配置
PANEL_POS_GRAVITY: { type: "enum", values: ["top", "bottom", "left", "right", "auto"], default: "bottom" },
PANEL_CUSTOM_OFFSET_Y: { type: "int", min: -500, max: 500, default: 0 },
BALL_PANEL_GAP_DP: { type: "int", min: 0, max: 50, default: 8 },
// 日志配置
LOG_ENABLE: { type: "bool", default: true },
LOG_DEBUG: { type: "bool", default: false },
LOG_KEEP_DAYS: { type: "int", min: 1, max: 30, default: 3 },
// 内容查看器配置
CONTENT_MAX_ROWS: { type: "int", min: 5, max: 100, default: 20 },
// ========== 以下配置在 Schema 中但原 ConfigValidator 中缺失 ==========
// 图标文件配置
BALL_ICON_FILE_PATH: { type: "string", default: "" },
BALL_ICON_FILE_MAX_PX: { type: "int", min: 128, max: 2048, default: 512 },
BALL_ICON_PKG: { type: "string", default: "" },
BALL_ICON_RES_NAME: { type: "string", default: "" },
BALL_ICON_TINT_HEX: { type: "string", default: "" },
// 回弹动画配置
BOUNCE_DECAY: { type: "float", min: 0.3, max: 0.95, default: 0.72 },
BOUNCE_MAX_SCALE: { type: "float", min: 0.6, max: 0.99, default: 0.88 },
BOUNCE_STEP_MS: { type: "int", min: 20, max: 500, default: 90 },
BOUNCE_TIMES: { type: "int", min: 1, max: 8, default: 2 },
ENABLE_BOUNCE: { type: "bool", default: true },
// 动画时长配置
DOCK_AFTER_IDLE_MS: { type: "int", min: 200, max: 8000, default: 1500 },
DOCK_ANIM_MS: { type: "int", min: 50, max: 2000, default: 260 },
UNDOCK_ANIM_MS: { type: "int", min: 50, max: 2000, default: 180 },
SAVE_THROTTLE_MS: { type: "int", min: 0, max: 5000, default: 220 },
// 长按配置
LONG_PRESS_HAPTIC_ENABLE: { type: "bool", default: true },
// 面板配置
PANEL_IDLE_CLOSE_AND_DOCK_MS: { type: "int", min: 200, max: 12000, default: 5000 },
PANEL_LABEL_ENABLED: { type: "bool", default: true },
PANEL_LABEL_TEXT_SIZE_SP: { type: "int", min: 8, max: 24, default: 12 },
PANEL_LABEL_TOP_MARGIN_DP: { type: "int", min: 0, max: 20, default: 4 },
// 主题颜色配置(留空则使用系统莫奈色)
THEME_DAY_BG_HEX: { type: "string", default: null },
THEME_DAY_TEXT_HEX: { type: "string", default: null },
THEME_NIGHT_BG_HEX: { type: "string", default: null },
THEME_NIGHT_TEXT_HEX: { type: "string", default: null }
},
validate: function(key, value) {
var schema = this.schemas[key];
if (!schema) return { valid: true, value: value }; // 未知 key 放行
var val = value;
// 类型转换
if (schema.type === "int") {
val = parseInt(String(value), 10);
if (isNaN(val)) return { valid: false, error: "必须为整数", fallback: schema.default };
} else if (schema.type === "float") {
val = parseFloat(String(value));
if (isNaN(val)) return { valid: false, error: "必须为数字", fallback: schema.default };
} else if (schema.type === "bool") {
val = parseBooleanLike(value);
} else if (schema.type === "string") {
val = String(value == null ? "" : value);
}
// 范围检查
if (schema.min !== undefined && val < schema.min) {
return { valid: false, error: "最小值为 " + schema.min, fallback: schema.min };
}
if (schema.max !== undefined && val > schema.max) {
return { valid: false, error: "最大值为 " + schema.max, fallback: schema.max };
}
// 枚举检查
if (schema.type === "enum" && schema.values.indexOf(val) < 0) {
return { valid: false, error: "可选值: " + schema.values.join(", "), fallback: schema.default };
}
return { valid: true, value: val };
},
sanitizeConfig: function(config) {
var out = {};
for (var k in config) {
var res = this.validate(k, config[k]);
if (res.valid) {
out[k] = res.value;
} else {
safeLog(null, 'w', "Config validation failed for " + k + ": " + res.error + ", using fallback");
out[k] = res.fallback !== undefined ? res.fallback : config[k];
}
}
return out;
}
};
// 安全获取嵌套对象属性
function safeGet(obj, path, defaultVal) {
try {
var parts = path.split('.');
var curr = obj;
for (var i = 0; i < parts.length; i++) {
if (curr == null) return defaultVal;
curr = curr[parts[i]];
}
return curr !== undefined ? curr : defaultVal;
} catch(e) { return defaultVal; }
}
// 全局反射缓存(避免重复 Class.forName
var ReflectionCache = {
_cache: {},
get: function(className) {
if (this._cache[className]) return this._cache[className];
try {
var clz = java.lang.Class.forName(className);
this._cache[className] = clz;
return clz;
} catch(e) { return null; }
},
getMethod: function(className, methodName, paramTypes) {
var key = className + '#' + methodName;
if (this._cache[key]) return this._cache[key];
try {
var clz = this.get(className);
if (!clz) return null;
var method = clz.getMethod(methodName, paramTypes);
this._cache[key] = method;
return method;
} catch(e) { return null; }
}
};
// 优化的防抖函数(使用 Handler 替代 Timer避免线程泄漏
function createDebouncedWriter(fn, delay) {
var _handler = null;
var _lastRunnable = null;
var _ht = null; // 保存 HandlerThread 引用以便清理
var writer = function(arg) {
// 延迟初始化 Handler复用实例
if (!_handler) {
try {
_handler = new android.os.Handler(android.os.Looper.getMainLooper());
} catch(e) {
// 兜底:创建独立的 HandlerThread仅创建一次
_ht = new android.os.HandlerThread("debounced-writer");
_ht.start();
_handler = new android.os.Handler(_ht.getLooper());
}
}
// 取消上一次的任务
if (_lastRunnable) {
try { _handler.removeCallbacks(_lastRunnable); } catch(e) {}
_lastRunnable = null;
}
var self = this;
var runnable = new java.lang.Runnable({
run: function() { fn.call(self, arg); }
});
_lastRunnable = runnable;
_handler.postDelayed(runnable, delay || 250);
};
writer.dispose = function() {
try {
if (_handler && _lastRunnable) _handler.removeCallbacks(_lastRunnable);
} catch (e0) {}
_lastRunnable = null;
try {
if (_ht) {
if (android.os.Build.VERSION.SDK_INT >= 18) _ht.quitSafely();
else _ht.quit();
}
} catch (e1) {}
_ht = null;
_handler = null;
};
return writer;
}
function resolveToolHubRootDir() {
try {
if (typeof shortx !== "undefined" && shortx && typeof shortx.getShortXDir === "function") {
var shortxDir = String(shortx.getShortXDir() || "");
if (shortxDir) return shortxDir + "/ToolHub";
}
} catch (eShortX) {}
try {
var logDirFile = new java.io.File(
Packages.tornaco.apps.shortx.core.OooO0O0.OooO00o().getLogDir()
);
var parent = logDirFile.getParentFile();
if (parent == null) {
return String(logDirFile.getAbsolutePath()) + "/ToolHub";
}
return String(parent.getAbsolutePath()) + "/ToolHub";
} catch (eRoot2) {}
return "/data/system/ShortX_ToolHub";
}
// =======================【全局路径与配置】======================
// 这段代码的主要内容/用途:动态获取 ShortX 根目录,并将 ToolHub 工作目录固定到「ShortX根目录/ToolHub」。
// 说明:使用 ShortX 内部 API 获取日志目录的上级目录作为根目录,避免硬编码路径导致 ROM/版本差异问题。
var APP_ROOT_DIR = resolveToolHubRootDir();
var PATH_SETTINGS = APP_ROOT_DIR + "/settings.json";
var PATH_BUTTONS = APP_ROOT_DIR + "/buttons.json";
var PATH_SCHEMA = APP_ROOT_DIR + "/schema.json";
var PATH_LOG_DIR = APP_ROOT_DIR + "/logs";
// =======================【内部常量配置(不开放 Settings】======================
var CONST_BALL_ICON_RES_ID = 0;
var CONST_BALL_ICON_FILE_MAX_BYTES = 524288;
var CONST_BALL_ICON_FILE_MAX_PX = 512;
var CONST_BALL_PNG_MODE = 1;
var CONST_BALL_INIT_X = 0;
var CONST_BALL_INIT_Y_DP = 220;
var CONST_BALL_FALLBACK_LIGHT = "#FF005BC0";
var CONST_BALL_FALLBACK_DARK = "#FFA8C7FA";
var CONST_SHORTX_PACKAGE = "tornaco.apps.shortx";
var CONST_BALL_RIPPLE_ALPHA_LIGHT = 0.22;
var CONST_BALL_RIPPLE_ALPHA_DARK = 0.28;
var CONST_ACTION_CLOSE_ALL_RULE = "shortx.wm.floatball.CLOSE";
var CONST_WM_THREAD_NAME = "SX-WM-FLOATBALL-HT";
var CONST_LOG_PREFIX = "ShortX_ToolHub";
var CONST_SHELL_BRIDGE_ACTION = "shortx.toolhub.SHELL";
var CONST_SHELL_BRIDGE_EXTRA_CMD = "cmd_b64";
var CONST_SHELL_BRIDGE_EXTRA_FROM = "from";
var CONST_SHELL_BRIDGE_EXTRA_ROOT = "root";
var CONST_SHELL_BRIDGE_DEFAULT_ROOT = true;
var CONST_CONTENT_MAX_ROWS = 20;
var CONST_CONFIG_SAVE_DEBOUNCE_MS = 250;
// # 交互常量配置:集中管理所有硬编码参数,便于维护和调优
var INTERACTION_CONSTANTS = {
// 触摸与手势
TOUCH_SLOP_DP: 6,
CLICK_SLOP_DP: 6,
LONG_PRESS_TIMEOUT_MS: 520,
CLICK_COOLDOWN_MS: 280,
FLING_VELOCITY_THRESHOLD: 1000,
// 动画时长
DOCK_ANIMATION_MS: 260,
UNDOCK_ANIMATION_MS: 180,
BOUNCE_STEP_MS: 90,
PANEL_ANIMATION_MS: 180,
DIALOG_ANIMATION_MS: 150,
// 自动吸附与超时
IDLE_DOCK_TIMEOUT_MS: 1500,
PANEL_IDLE_TIMEOUT_MS: 5000,
EDGE_VISIBLE_RATIO: 0.70,
SNAP_ANIMATION_MS: 260,
// 防抖与节流
SAVE_THROTTLE_MS: 250,
SCREEN_MONITOR_THROTTLE_MS: 300,
DEBOUNCE_WRITE_MS: 250,
WM_UPDATE_THROTTLE_MS: 10,
// 尺寸限制
MIN_PANEL_WIDTH_DP: 200,
MIN_PANEL_HEIGHT_DP: 150,
MAX_PANEL_HEIGHT_RATIO: 0.75,
MAX_PANEL_WIDTH_RATIO: 0.9,
// 缓存与性能
ICON_CACHE_MAX_SIZE: 120,
ICON_LRU_MAX: 80,
CFG_PAGE_BATCH: 40,
CFG_ICON_CACHE_MAX: 120,
CFG_ICON_FAIL_TTL_MS: 15000,
CFG_ICON_FAIL_MAX_RETRY: 2,
CFG_ICON_LOAD_CONCURRENCY: 2,
// 日志与监控
LOG_KEEP_DAYS: 3,
MAX_LOG_ROWS: 20,
MEMORY_CHECK_INTERVAL_MS: 30000,
MEMORY_HIGH_THRESHOLD: 0.75
};
// # 配置落盘去抖合并写入延迟ms
// =======================【文件 IO 工具】======================
var FileIO = {
ensureDir: function(dir) {
try {
var f = new java.io.File(dir);
if (!f.exists()) f.mkdirs();
return f.exists();
} catch (e) { return false; }
},
readText: function(path) {
// 这段代码的主要内容/用途:读取 UTF-8 文本文件内容,并确保流被正确关闭,避免 system_server 长时间运行下的句柄泄漏。
var fis = null;
var isr = null;
var br = null;
try {
var f = new java.io.File(path);
if (!f.exists()) return null;
fis = new java.io.FileInputStream(f);
isr = new java.io.InputStreamReader(fis, "UTF-8");
br = new java.io.BufferedReader(isr);
var sb = new java.lang.StringBuilder();
var line;
while ((line = br.readLine()) != null) sb.append(line).append("\n");
return sb.toString();
} catch (e) {
return null;
} finally {
try { if (br) br.close(); } catch (e1) {}
try { if (isr) isr.close(); } catch (e2) {}
try { if (fis) fis.close(); } catch (e3) {}
}
},
writeText: function(path, content) {
// 这段代码的主要内容/用途:写入 UTF-8 文本文件内容,并确保流被正确关闭,避免 system_server 资源泄漏。
var fos = null;
var osw = null;
try {
var f = new java.io.File(path);
var p = f.getParentFile();
if (p && !p.exists()) p.mkdirs();
fos = new java.io.FileOutputStream(f);
osw = new java.io.OutputStreamWriter(fos, "UTF-8");
osw.write(String(content));
osw.flush();
return true;
} catch (e) {
return false;
} finally {
try { if (osw) osw.close(); } catch (e1) {}
try { if (fos) fos.close(); } catch (e2) {}
}
},
// =======================【文件写入:原子写 + 去抖合并】=======================
// 这段代码的主要内容/用途:提供"原子写入"和"去抖合并写入",降低频繁写 JSON 配置带来的卡顿/抖动风险。
// # 注意:为保证功能与 UI 不受影响,本次仅替换配置落盘方式,不改变上层调用时机与数据结构。
// # 兼容性:系统 Server 常驻环境下优先保证"尽量不写坏文件",失败时回退到普通 writeText。
_debounceTimer: null,
_debounceJobs: {},
_ensureDebounceTimer: function() {
try {
if (!this._debounceTimer) {
this._debounceTimer = new java.util.Timer("sx-toolhub-filewrite", true);
}
} catch (e) {
// ignore
}
},
writeTextAtomic: function(path, content) {
// 这段代码的主要内容/用途:原子写入 UTF-8 文本:写临时文件 -> flush+sync -> rename 覆盖。
var fos = null;
var osw = null;
var tmpFile = null;
var bakFile = null;
try {
var target = new java.io.File(String(path));
var dir = target.getParentFile();
if (dir && !dir.exists()) dir.mkdirs();
var tmpName = target.getName() + ".tmp." + String(java.lang.System.nanoTime());
tmpFile = new java.io.File(dir, tmpName);
fos = new java.io.FileOutputStream(tmpFile);
osw = new java.io.OutputStreamWriter(fos, "UTF-8");
osw.write(String(content));
osw.flush();
try { fos.getFD().sync(); } catch (eSync) {}
try { if (osw) osw.close(); } catch (eC1) {}
try { if (fos) fos.close(); } catch (eC2) {}
osw = null;
fos = null;
// # 备份旧文件,避免 rename 覆盖失败时丢失
bakFile = new java.io.File(dir, target.getName() + ".bak");
try { if (bakFile.exists()) bakFile["delete"](); } catch (eDelBak0) {}
var hasBackup = false;
if (target.exists()) {
try { hasBackup = target.renameTo(bakFile); } catch (eMv0) { hasBackup = false; }
if (!hasBackup) {
try { if (tmpFile && tmpFile.exists()) tmpFile["delete"](); } catch (eDel0) {}
return this.writeText(path, content);
}
}
var ok = false;
try { ok = tmpFile.renameTo(target); } catch (eRn0) { ok = false; }
if (!ok) {
// # 恢复备份
try {
if (hasBackup && bakFile && bakFile.exists() && !target.exists()) {
bakFile.renameTo(target);
}
} catch (eRv0) {}
try { if (tmpFile && tmpFile.exists()) tmpFile["delete"](); } catch (eDelTmp0) {}
// # 回退普通写(尽力而为)
return this.writeText(path, content);
}
try { if (bakFile && bakFile.exists()) bakFile["delete"](); } catch (eDelBak1) {}
return true;
} catch (e) {
try { if (osw) osw.close(); } catch (e1) {}
try { if (fos) fos.close(); } catch (e2) {}
try { if (tmpFile && tmpFile.exists()) tmpFile["delete"](); } catch (e3) {}
// # 原子写失败则回退普通写
return this.writeText(path, content);
}
},
writeTextDebounced: function(path, content, delayMs) {
// 这段代码的主要内容/用途:对同一路径的连续写请求做去抖合并,只落盘最后一次内容。
var p = String(path);
var d = 0;
try { d = parseInt(String(delayMs), 10); } catch (eD0) { d = 0; }
if (isNaN(d) || d < 0) d = 0;
this._ensureDebounceTimer();
var old = this._debounceJobs[p];
if (old && old.task) {
try { old.task.cancel(); } catch (eC0) {}
}
var self = this;
var payload = String(content);
var version = 0;
try {
if (old && old.version) version = old.version;
} catch (eV0) { version = 0; }
version = version + 1;
var task = null;
try {
task = new JavaAdapter(java.util.TimerTask, {
run: function() {
var liveJob = null;
try { liveJob = self._debounceJobs[p]; } catch (eR0) { liveJob = null; }
if (!liveJob || liveJob.version !== version) return;
try { self.writeTextAtomic(p, payload); } catch (eW0) { try { self.writeText(p, payload); } catch (eW0b) {} }
try {
if (self._debounceJobs[p] && self._debounceJobs[p].version === version) delete self._debounceJobs[p];
} catch (eW1) { self._debounceJobs[p] = null; }
}
});
} catch (eT0) {
// # JavaAdapter 失败则直接写入(仍保证功能不受影响)
try { self.writeTextAtomic(p, payload); } catch (eT1) { self.writeText(p, payload); }
try { delete self._debounceJobs[p]; } catch (eT2) {}
return true;
}
var tsNow = 0;
try { tsNow = new Date().getTime(); } catch (eNow0) { tsNow = 0; }
this._debounceJobs[p] = { task: task, ts: tsNow, payload: payload, version: version, delayMs: d, path: p };
// # 修复Rhino 作用域下 now() 未定义导致"保存所有"报错,改用 Date.getTime() 生成时间戳
try {
if (this._debounceTimer) this._debounceTimer.schedule(task, d);
else this.writeTextAtomic(p, payload);
} catch (eS0) {
// # schedule 失败则立即写入
try { this.writeTextAtomic(p, payload); } catch (eS1) { this.writeText(p, payload); }
try { delete this._debounceJobs[p]; } catch (eS2) {}
}
return true;
},
flushDebouncedWrites: function() {
// 这段代码的主要内容/用途:立即落盘所有待写内容,并清空待写队列,确保 close/dispose 前最后一次修改不会丢失。
try {
for (var k in this._debounceJobs) {
var job = this._debounceJobs[k];
if (!job) continue;
try { if (job.task) job.task.cancel(); } catch (eC0) {}
try {
if (job.payload != null) this.writeTextAtomic(k, job.payload);
} catch (eW0) {
try { this.writeText(k, job.payload); } catch (eW1) {}
}
try { delete this._debounceJobs[k]; } catch (eD0) { this._debounceJobs[k] = null; }
}
if (this._debounceTimer) {
try { this._debounceTimer.cancel(); } catch (eC1) {}
try { this._debounceTimer.purge(); } catch (eP0) {}
this._debounceTimer = null;
}
} catch (e) {}
},
appendText: function(path, content) {
// 这段代码的主要内容/用途:追加 UTF-8 文本到文件,并确保流被正确关闭,避免 system_server 资源泄漏。
var fos = null;
var osw = null;
try {
var f = new java.io.File(path);
var p = f.getParentFile();
if (p && !p.exists()) p.mkdirs();
fos = new java.io.FileOutputStream(f, true);
osw = new java.io.OutputStreamWriter(fos, "UTF-8");
osw.write(String(content));
osw.flush();
return true;
} catch (e) {
return false;
} finally {
try { if (osw) osw.close(); } catch (e1) {}
try { if (fos) fos.close(); } catch (e2) {}
}
}
};
// =======================【工具Base64 编码UTF-8】=======================
function encodeBase64Utf8(str) {
try {
var s = String(str || "");
var bytes = new java.lang.String(s).getBytes("UTF-8");
return java.lang.String(android.util.Base64.encode(bytes, android.util.Base64.NO_WRAP));
} catch (e) { return ""; }
}
// # 系统级跨用户启动(可供快捷方式 JS 模板调用)
// 这段代码的主要内容/用途:在指定 userId 下解析并启动 intentUri优先使用 Context.startActivityAsUser失败时返回 err 供上层选择是否回退 Shell。
function startIntentAsUserByUri(intentUri, userId) {
// # 局部反射缓存(避免每次点击都重复 Class.forName/getMethod/getField
var __th_cache = startIntentAsUserByUri.__cache;
if (!__th_cache) {
__th_cache = { ok: false };
try {
var I0 = java.lang.Class.forName("android.content.Intent");
var U0 = java.lang.Class.forName("android.os.UserHandle");
var S0 = java.lang.Class.forName("java.lang.String");
var T0 = java.lang.Integer.TYPE;
__th_cache.IntentClz = I0;
__th_cache.UserHandleClz = U0;
__th_cache.StringClz = S0;
__th_cache.IntType = T0;
__th_cache.parseUriM = I0.getMethod("parseUri", S0, T0);
__th_cache.flagNewTask = I0.getField("FLAG_ACTIVITY_NEW_TASK").getInt(null);
__th_cache.ofM = U0.getMethod("of", T0);
__th_cache.startAsUserM = context.getClass().getMethod("startActivityAsUser", I0, U0);
__th_cache.zeroInt = new java.lang.Integer(0);
__th_cache.ok = true;
startIntentAsUserByUri.__cache = __th_cache;
} catch (eInit) {
__th_cache.ok = false;
__th_cache.err = String(eInit);
startIntentAsUserByUri.__cache = __th_cache;
}
}
var ret = { ok: false, err: "", via: "" };
try {
var u = 0;
try { u = parseInt(String(userId), 10); } catch(eU0) { u = 0; }
if (isNaN(u)) u = 0;
var it;
var uh;
if (__th_cache && __th_cache.ok) {
it = __th_cache.parseUriM.invoke(null, String(intentUri), __th_cache.zeroInt);
it.addFlags(__th_cache.flagNewTask);
uh = __th_cache.ofM.invoke(null, new java.lang.Integer(u));
__th_cache.startAsUserM.invoke(context, it, uh);
} else {
var I = java.lang.Class.forName("android.content.Intent");
var U = java.lang.Class.forName("android.os.UserHandle");
var S = java.lang.Class.forName("java.lang.String");
var T = java.lang.Integer.TYPE;
it = I.getMethod("parseUri", S, T).invoke(null, String(intentUri), new java.lang.Integer(0));
it.addFlags(I.getField("FLAG_ACTIVITY_NEW_TASK").getInt(null));
uh = U.getMethod("of", T).invoke(null, new java.lang.Integer(u));
context.getClass().getMethod("startActivityAsUser", I, U).invoke(context, it, uh);
}
ret.ok = true;
ret.via = "js_startActivityAsUser";
return ret;
} catch (e) {
ret.ok = false;
ret.err = String(e);
ret.via = "js_startActivityAsUser";
return ret;
}
}
// =======================【配置管理】======================
var ConfigManager = {
_settingsCache: null,
_buttonsCache: null,
defaultSettings: {
DOCK_AFTER_IDLE_MS: 1500,
PANEL_IDLE_CLOSE_AND_DOCK_MS: 5000,
EDGE_VISIBLE_RATIO: 0.70,
DOCK_ANIM_MS: 260,
UNDOCK_ANIM_MS: 180,
SAVE_THROTTLE_MS: 220,
LONG_PRESS_HAPTIC_ENABLE: true,
LONG_PRESS_VIBRATE_MS: 18,
ENABLE_LONG_PRESS: true,
LONG_PRESS_MS: 520,
CLICK_SLOP_DP: 6,
ENABLE_BOUNCE: true,
BOUNCE_TIMES: 2,
BOUNCE_MAX_SCALE: 0.88,
BOUNCE_STEP_MS: 90,
BOUNCE_DECAY: 0.72,
BALL_SIZE_DP: 45,
BALL_ICON_TYPE: "app",
BALL_ICON_PKG: CONST_SHORTX_PACKAGE,
BALL_ICON_FILE_PATH: APP_ROOT_DIR + "/ball.png",
BALL_ICON_FILE_MAX_PX: 512,
BALL_ICON_RES_NAME: "",
BALL_ICON_SIZE_DP: 22,
BALL_ICON_TINT_HEX: "",
BALL_IDLE_ALPHA: 0.6,
PANEL_POS_GRAVITY: "bottom",
PANEL_CUSTOM_OFFSET_Y: 0,
PANEL_COLS: 1,
PANEL_ROWS: 4,
PANEL_ITEM_SIZE_DP: 60,
PANEL_GAP_DP: 5,
PANEL_PADDING_DP: 7,
PANEL_ICON_SIZE_DP: 32,
PANEL_LABEL_ENABLED: true,
PANEL_LABEL_TEXT_SIZE_SP: 12,
PANEL_LABEL_TOP_MARGIN_DP: 4,
PANEL_BG_FALLBACK_HEX: "#EE1E1E1E",
PANEL_BG_ALPHA: 0.85,
THEME_MODE: 1,
THEME_DAY_BG_HEX: null,
THEME_DAY_TEXT_HEX: null,
THEME_NIGHT_BG_HEX: null,
THEME_NIGHT_TEXT_HEX: null,
BALL_PANEL_GAP_DP: 10,
LOG_ENABLE: true,
LOG_DEBUG: true,
LOG_KEEP_DAYS: 3
},
defaultButtons: [
// # 默认按钮已迁移至 buttons.json
// # Default buttons migrated to buttons.json
],
defaultSchema: [
{ type: "section", name: "外观" },
{ key: "THEME_MODE", name: "主题(0跟随/1白/2黑)", type: "int", min: 0, max: 2, step: 1 },
{ key: "THEME_DAY_BG_HEX", name: "日间背景色(#RRGGBB)", type: "text" },
{ key: "THEME_DAY_TEXT_HEX", name: "日间文字色(#RRGGBB)", type: "text" },
{ key: "THEME_NIGHT_BG_HEX", name: "夜间背景色(#RRGGBB)", type: "text" },
{ key: "THEME_NIGHT_TEXT_HEX", name: "夜间文字色(#RRGGBB)", type: "text" },
{ key: "PANEL_BG_ALPHA", name: "面板背景透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ type: "section", name: "悬浮球" },
{ key: "BALL_SIZE_DP", name: "悬浮球大小(dp)", type: "int", min: 28, max: 120, step: 1 },
{ key: "BALL_PANEL_GAP_DP", name: "球与面板间距(dp)", type: "int", min: 0, max: 60, step: 1 },
{ key: "BALL_ICON_TYPE", name: "图标类型", type: "single_choice", options: [
{ label: "应用图标 (app)", value: "app" },
{ label: "文件图标 (file)", value: "file" },
{ label: "ShortX内置 (shortx)", value: "shortx" }
]},
{ key: "BALL_ICON_FILE_PATH", name: "图标路径(file模式)", type: "text" },
{ key: "BALL_ICON_RES_NAME", name: "ShortX图标", type: "ball_shortx_icon" },
{ key: "BALL_ICON_TINT_HEX", name: "图标颜色", type: "ball_color" },
{ key: "BALL_IDLE_ALPHA", name: "闲置不透明度(0.1~1.0)", type: "float", min: 0.1, max: 1.0, step: 0.05 },
{ type: "section", name: "面板布局" },
{ key: "PANEL_ROWS", name: "面板可视行数(超出滚动)", type: "int", min: 1, max: 12, step: 1 },
{ key: "PANEL_COLS", name: "面板列数", type: "int", min: 1, max: 6, step: 1 },
{ key: "PANEL_ITEM_SIZE_DP", name: "面板单元格(dp)", type: "int", min: 36, max: 120, step: 1 },
{ key: "PANEL_GAP_DP", name: "格子间距(dp)", type: "int", min: 0, max: 30, step: 1 },
{ key: "PANEL_PADDING_DP", name: "面板内边距(dp)", type: "int", min: 0, max: 40, step: 1 },
{ key: "PANEL_ICON_SIZE_DP", name: "图标大小(dp)", type: "int", min: 16, max: 80, step: 1 },
{ type: "section", name: "面板文字" },
{ key: "PANEL_LABEL_ENABLED", name: "显示按钮文字", type: "bool" },
{ key: "PANEL_LABEL_TEXT_SIZE_SP", name: "文字大小(sp)", type: "int", min: 8, max: 24, step: 1 },
{ key: "PANEL_LABEL_TOP_MARGIN_DP", name: "文字上边距(dp)", type: "int", min: 0, max: 20, step: 1 },
{ type: "section", name: "吸边与位置" },
{ key: "PANEL_POS_GRAVITY", name: "面板默认位置", type: "single_choice", options: [
{ label: "自动 (Auto)", value: "auto" },
{ label: "下方 (Bottom)", value: "bottom" },
{ label: "上方 (Top)", value: "top" }
]},
{ key: "PANEL_CUSTOM_OFFSET_Y", name: "手动垂直偏移(dp)", type: "int", min: -500, max: 500, step: 1 },
{ key: "ENABLE_SNAP_TO_EDGE", name: "启用自动吸边", type: "bool" },
{ key: "DOCK_AFTER_IDLE_MS", name: "无操作吸边延迟(ms)", type: "int", min: 200, max: 8000, step: 100 },
{ key: "PANEL_IDLE_CLOSE_AND_DOCK_MS", name: "面板无操作:关面板再吸边(ms)", type: "int", min: 200, max: 12000, step: 100 },
{ key: "EDGE_VISIBLE_RATIO", name: "吸边露出比例(0~1)", type: "float", min: 0.20, max: 1.00, step: 0.05 },
{ key: "SAVE_THROTTLE_MS", name: "保存位置节流(ms)", type: "int", min: 0, max: 5000, step: 10 },
{ type: "section", name: "动画" },
{ key: "ENABLE_ANIMATIONS", name: "启用动画效果", type: "bool" },
{ key: "DOCK_ANIM_MS", name: "吸边动画时长(ms)", type: "int", min: 50, max: 2000, step: 10 },
{ key: "UNDOCK_ANIM_MS", name: "退出吸边动画时长(ms)", type: "int", min: 50, max: 2000, step: 10 },
{ key: "ENABLE_BOUNCE", name: "启用点击回弹", type: "bool" },
{ key: "BOUNCE_TIMES", name: "回弹次数", type: "int", min: 1, max: 8, step: 1 },
{ key: "BOUNCE_MAX_SCALE", name: "回弹最小缩放(0~1)", type: "float", min: 0.60, max: 0.99, step: 0.01 },
{ key: "BOUNCE_STEP_MS", name: "回弹步进时长(ms)", type: "int", min: 20, max: 500, step: 10 },
{ key: "BOUNCE_DECAY", name: "回弹衰减(0~1)", type: "float", min: 0.30, max: 0.95, step: 0.01 },
{ type: "section", name: "触摸与手势" },
{ key: "CLICK_SLOP_DP", name: "点击位移阈值(dp)", type: "int", min: 1, max: 40, step: 1 },
{ key: "ENABLE_LONG_PRESS", name: "启用长按", type: "bool" },
{ key: "LONG_PRESS_MS", name: "长按判定(ms)", type: "int", min: 200, max: 2000, step: 10 },
{ key: "LONG_PRESS_HAPTIC_ENABLE", name: "长按震动反馈", type: "bool" },
{ key: "LONG_PRESS_VIBRATE_MS", name: "震动时长(ms)", type: "int", min: 1, max: 120, step: 1 },
{ type: "section", name: "日志" },
{ key: "LOG_ENABLE", name: "写文件日志", type: "bool" },
{ key: "LOG_DEBUG", name: "详细日志DEBUG", type: "bool" },
{ key: "LOG_KEEP_DAYS", name: "日志保留天数", type: "int", min: 1, max: 30, step: 1 }
],
_schemaCache: null,
loadSchema: function(forceReload) {
if (!forceReload && this._schemaCache) return this._schemaCache;
var txt = FileIO.readText(PATH_SCHEMA);
// # 容错重试
if (!txt) {
try {
var f = new java.io.File(PATH_SCHEMA);
if (f.exists()) {
java.lang.Thread.sleep(200);
txt = FileIO.readText(PATH_SCHEMA);
}
} catch(e) {}
}
var s = null;
if (txt) {
try { s = JSON.parse(txt); } catch (e) {}
}
// 检查 Schema 完整性:如果缺少新添加的关键字段,则强制更新
// 这解决了脚本更新后,旧的 schema.json 缓存导致新开关不显示的问题
var needReset = false;
if (s) {
var sStr = JSON.stringify(s);
if (sStr.indexOf("ENABLE_SNAP_TO_EDGE") < 0 || sStr.indexOf("ENABLE_ANIMATIONS") < 0 || sStr.indexOf("BALL_IDLE_ALPHA") < 0 || sStr.indexOf("PANEL_POS_GRAVITY") < 0 || sStr.indexOf("single_choice") < 0 || sStr.indexOf("ball_shortx_icon") < 0 || sStr.indexOf("ball_color") < 0) {
needReset = true;
}
} else {
// # 仅当文件不存在时才标记为需要重置(新建),避免因读取失败导致覆盖
try {
var f = new java.io.File(PATH_SCHEMA);
if (!f.exists()) needReset = true;
} catch(e) { needReset = true; }
}
if (needReset) {
s = JSON.parse(JSON.stringify(this.defaultSchema));
// # 原子写:避免 schema.json 写一半导致后续解析失败
FileIO.writeTextAtomic(PATH_SCHEMA, JSON.stringify(s, null, 2));
}
this._schemaCache = s;
return s;
},
saveSchema: function(s) {
this._schemaCache = s;
// # 去抖合并:短时间多次保存仅落盘最后一次(不影响 UI/功能)
return FileIO.writeTextDebounced(PATH_SCHEMA, JSON.stringify(s, null, 2), CONST_CONFIG_SAVE_DEBOUNCE_MS);
},
resetSchema: function() {
var s = JSON.parse(JSON.stringify(this.defaultSchema));
this.saveSchema(s);
return s;
},
loadSettings: function(forceReload) {
if (!forceReload && this._settingsCache) return this._settingsCache;
var txt = FileIO.readText(PATH_SETTINGS);
// # 容错重试:如果读取失败但文件存在(可能是启动时 IO 繁忙),稍后重试一次
if (!txt) {
try {
var f = new java.io.File(PATH_SETTINGS);
if (f.exists()) {
java.lang.Thread.sleep(200);
txt = FileIO.readText(PATH_SETTINGS);
}
} catch(e) {}
}
var merged = JSON.parse(JSON.stringify(this.defaultSettings));
var loaded = false;
if (txt) {
try {
var user = JSON.parse(txt);
// 合并用户设置(允许新增键值)
for (var k in user) {
merged[k] = user[k];
}
loaded = true;
} catch (e) {}
}
// # 仅当文件不存在时才写入默认值,避免因读取失败导致用户配置被覆盖
if (!loaded) {
try {
var f = new java.io.File(PATH_SETTINGS);
if (!f.exists()) {
// # 原子写:避免 settings.json 写一半导致配置损坏
FileIO.writeTextAtomic(PATH_SETTINGS, JSON.stringify(merged, null, 2));
}
} catch(e) {}
}
this._settingsCache = ConfigValidator.sanitizeConfig(merged);
return this._settingsCache;
},
saveSettings: function(obj) {
// # 保存前进行配置校验,防止无效值写入
var sanitized = ConfigValidator.sanitizeConfig(obj);
this._settingsCache = sanitized;
// # 去抖合并:短时间多次保存仅落盘最后一次(不影响 UI/功能)
return FileIO.writeTextDebounced(PATH_SETTINGS, JSON.stringify(sanitized, null, 2), CONST_CONFIG_SAVE_DEBOUNCE_MS);
},
saveButtons: function(btns) {
this._buttonsCache = btns;
// # 去抖合并:短时间多次保存仅落盘最后一次(不影响 UI/功能)
return FileIO.writeTextDebounced(PATH_BUTTONS, JSON.stringify(btns, null, 2), CONST_CONFIG_SAVE_DEBOUNCE_MS);
},
loadButtons: function(forceReload) {
if (!forceReload && this._buttonsCache) return this._buttonsCache;
var txt = FileIO.readText(PATH_BUTTONS);
// # 容错重试
if (!txt) {
try {
var f = new java.io.File(PATH_BUTTONS);
if (f.exists()) {
java.lang.Thread.sleep(200);
txt = FileIO.readText(PATH_BUTTONS);
}
} catch(e) {}
}
var btns = null;
if (txt) {
try { btns = JSON.parse(txt); } catch (e) {}
}
var dirty = false;
if (!btns) {
// # 仅当文件不存在时才使用默认值并写入
try {
var f = new java.io.File(PATH_BUTTONS);
if (!f.exists()) {
btns = JSON.parse(JSON.stringify(this.defaultButtons));
dirty = true;
}
} catch(e) {}
// # 如果 btns 仍为空(读取失败且文件存在),则暂时返回默认值但不回写 dirty
if (!btns) {
// # 救援模式:如果配置文件损坏,提供一个"关闭"按钮,防止无法退出
btns = [
{ title: "Rescue: Close", type: "broadcast", action: "shortx.wm.floatball.CLOSE", iconResName: "ic_menu_close_clear_cancel" }
];
// dirty = false; // 默认就是 false
}
}
// Upgrade
if (btns) {
for (var i=0; i<btns.length; i++) {
var b = btns[i];
// # 兼容升级:增加 enabled 字段(按钮管理页可禁用/启用;禁用后按钮页不显示)
// # 说明:旧版 buttons.json 没有 enabled默认视为启用=true
if (typeof b.enabled === "undefined") {
b.enabled = true;
dirty = true;
}
if (b.type === "shell" && b.cmd && !b.cmd_b64) {
b.cmd_b64 = encodeBase64Utf8(b.cmd);
dirty = true;
}
}
}
if (dirty) {
// # 原子写:升级修复字段时落盘,避免写坏 buttons.json
FileIO.writeTextAtomic(PATH_BUTTONS, JSON.stringify(btns, null, 2));
}
this._buttonsCache = btns;
return btns;
}
};
// =======================【基础工具:进程输出】=======================
function getProcessInfo(tag) {
var info = {
tag: tag || "",
uid: -1,
pid: -1,
tid: -1,
packageName: "",
processName: "",
threadName: "",
hasMyLooper: false,
looperIsMain: false
};
try { info.uid = android.os.Process.myUid(); } catch (e0) {}
try { info.pid = android.os.Process.myPid(); } catch (e1) {}
try { info.tid = android.os.Process.myTid(); } catch (e2) {}
try { info.packageName = String(context.getPackageName()); } catch (e3) {}
try { info.threadName = String(java.lang.Thread.currentThread().getName()); } catch (e4) {}
try { info.hasMyLooper = (android.os.Looper.myLooper() !== null); } catch (e5) {}
try {
var mainLooper = android.os.Looper.getMainLooper();
var myLooper = android.os.Looper.myLooper();
info.looperIsMain = (myLooper !== null && mainLooper !== null && myLooper === mainLooper);
} catch (e6) {}
try {
var fis = new java.io.FileInputStream("/proc/self/cmdline");
var bos = new java.io.ByteArrayOutputStream();
var buf = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 256);
while (true) {
var n = fis.read(buf);
if (n <= 0) break;
bos.write(buf, 0, n);
if (bos.size() >= 4096) break;
}
try { fis.close(); } catch (eC) {}
var raw = new java.lang.String(bos.toByteArray());
var p = String(raw).split("\u0000")[0];
if (p && p.length > 0) info.processName = p;
} catch (e7) {}
if (!info.processName) {
try {
var am = context.getSystemService(android.content.Context.ACTIVITY_SERVICE);
var list = am.getRunningAppProcesses();
if (list) {
var i;
for (i = 0; i < list.size(); i++) {
var rp = list.get(i);
if (rp && rp.pid === info.pid) {
info.processName = String(rp.processName);
break;
}
}
}
} catch (e8) {}
}
return info;
}
// =======================【工具Base64 解码UTF-8】=======================
// # 这段代码的主要内容/用途:把 cmd_b64 还原成原始 shell 文本
function decodeBase64Utf8(b64) {
try {
var s = String(b64 || "");
if (!s) return "";
// # 兼容:优先按 NO_WRAP 解码(与 encodeBase64Utf8 一致),失败再用 DEFAULT 兜底
var bytes = null;
try {
bytes = android.util.Base64.decode(s, android.util.Base64.NO_WRAP);
} catch (e0) {
bytes = android.util.Base64.decode(s, android.util.Base64.DEFAULT);
}
return new java.lang.String(bytes, "UTF-8");
} catch (e) {
return "";
}
}
// =======================【工具:字符串替换占位符】=======================
function applyRule(rule, kv) {
var s = String(rule || "");
if (!s) return s;
var k;
for (k in kv) {
if (!kv.hasOwnProperty(k)) continue;
var token = "{" + String(k) + "}";
s = s.split(token).join(String(kv[k]));
}
return s;
}
// =======================【日志:文件写入器(尽力落盘 + 自动清理旧日志)】=======================
// =======================【日志:文件写入器(全局统一目录 + 分级)】=======================
// 优化后的日志系统(带缓冲,减少文件 IO
function ToolHubLogger(procInfo) {
this.proc = procInfo || {};
this.dir = PATH_LOG_DIR;
this.prefix = "ShortX_ToolHub";
this.keepDays = 3;
this.enable = true;
this.debug = false;
this.initOk = false;
this.lastInitErr = "";
// 新增:日志缓冲
this._buffer = [];
this._bufferSize = 20; // 每 20 条写一次磁盘
this._flushTimer = null;
this._initOnce();
}
ToolHubLogger.prototype._now = function() { return new Date().getTime(); };
ToolHubLogger.prototype._initOnce = function() {
try {
if (FileIO.ensureDir(this.dir)) {
this.initOk = true;
this.cleanupOldFiles();
} else {
this.initOk = false;
this.lastInitErr = "Mkdirs failed: " + this.dir;
}
} catch (e) {
this.initOk = false;
this.lastInitErr = String(e);
}
};
ToolHubLogger.prototype.updateConfig = function(cfg) {
if (!cfg) return;
if (typeof cfg.LOG_KEEP_DAYS === "number") this.keepDays = cfg.LOG_KEEP_DAYS;
if (typeof cfg.LOG_ENABLE !== "undefined") this.enable = !!cfg.LOG_ENABLE;
if (typeof cfg.LOG_DEBUG !== "undefined") this.debug = !!cfg.LOG_DEBUG;
};
ToolHubLogger.prototype._line = function(level, msg) {
var ts = this._now();
var d = new Date(ts);
function pad2(x) { return (x < 10 ? "0" : "") + x; }
var t = d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) +
" " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
return t + " [" + level + "] " + msg + "\n";
};
ToolHubLogger.prototype._scheduleFlush = function() {
if (this._flushTimer) try { this._flushTimer.cancel(); } catch(e) {}
var self = this;
this._flushTimer = new java.util.Timer();
this._flushTimer.schedule(new java.util.TimerTask({
run: function() { self._flushBuffer(); }
}), 3000); // 3秒后强制刷新
};
ToolHubLogger.prototype._flushBuffer = function() {
if (this._buffer.length === 0) return;
var content = this._buffer.join('');
this._buffer = [];
var path = this.dir + "/" + this.prefix + "_" + this._ymd() + ".log";
FileIO.appendText(path, content);
};
ToolHubLogger.prototype._ymd = function() {
var d = new Date();
return "" + d.getFullYear() +
((d.getMonth() < 9 ? "0" : "") + (d.getMonth() + 1)) +
((d.getDate() < 10 ? "0" : "") + d.getDate());
};
ToolHubLogger.prototype._write = function(level, msg) {
if (!this.enable) return false;
this._buffer.push(this._line(level, msg));
// 缓冲满或错误级别立即写入
if (this._buffer.length >= this._bufferSize || level === 'F' || level === 'E') {
this._flushBuffer();
} else {
this._scheduleFlush(); // 延迟写入
}
return true;
};
ToolHubLogger.prototype.d = function(msg) { if (this.debug) this._write("D", msg); };
ToolHubLogger.prototype.i = function(msg) { this._write("I", msg); };
ToolHubLogger.prototype.w = function(msg) { this._write("W", msg); };
ToolHubLogger.prototype.e = function(msg) { this._write("E", msg); };
ToolHubLogger.prototype.fatal = function(msg) { this._write("F", msg); this._flushBuffer(); };
ToolHubLogger.prototype.cleanupOldFiles = function() {
try {
if (!this.initOk) return false;
var dirF = new java.io.File(this.dir);
var files = dirF.listFiles();
if (!files) return false;
var now = this._now();
var cutoff = now - this.keepDays * 24 * 60 * 60 * 1000;
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (f && f.isFile() && f.getName().indexOf(this.prefix) === 0 && f.lastModified() < cutoff) {
f["delete"]();
}
}
return true;
} catch (e) { return false; };
};
ToolHubLogger.prototype._filePathForToday = function() {
var name = this.prefix + "_" + this._ymd(this._now()) + ".log";
return this.dir + "/" + name;
};
ToolHubLogger.prototype._initOnce = function() {
try {
// # 尝试创建目录
if (FileIO.ensureDir(this.dir)) {
this.initOk = true;
// # 清理旧日志
this.cleanupOldFiles();
} else {
this.initOk = false;
this.lastInitErr = "Mkdirs failed: " + this.dir;
}
} catch (e) {
this.initOk = false;
this.lastInitErr = String(e);
}
};
ToolHubLogger.prototype.updateConfig = function(cfg) {
if (!cfg) return;
if (typeof cfg.LOG_KEEP_DAYS === "number") this.keepDays = cfg.LOG_KEEP_DAYS;
if (typeof cfg.LOG_ENABLE !== "undefined") this.enable = !!cfg.LOG_ENABLE;
if (typeof cfg.LOG_DEBUG !== "undefined") this.debug = !!cfg.LOG_DEBUG;
};
ToolHubLogger.prototype._line = function(level, msg) {
var ts = this._now();
var d = new Date(ts);
function pad2(x) { return (x < 10 ? "0" : "") + x; }
var t = d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) +
" " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
var proc = "";
try {
proc = " uid=" + String(this.proc.uid) + " pid=" + String(this.proc.pid) + " tid=" + String(this.proc.tid) +
" th=" + String(this.proc.threadName) + " proc=" + String(this.proc.processName);
} catch (e0) {}
return t + " [" + String(level) + "] " + String(msg) + proc + "\n";
};
ToolHubLogger.prototype._writeRaw = function(level, msg) {
if (!this.initOk) return false;
var p = this._filePathForToday();
return FileIO.appendText(p, this._line(level, msg));
};
ToolHubLogger.prototype._write = function(level, msg) {
if (!this.enable) return false;
return this._writeRaw(level, msg);
};
ToolHubLogger.prototype.d = function(msg) { if (this.debug) this._write("D", msg); };
ToolHubLogger.prototype.i = function(msg) { this._write("I", msg); };
ToolHubLogger.prototype.w = function(msg) { this._write("W", msg); };
ToolHubLogger.prototype.e = function(msg) { this._write("E", msg); };
ToolHubLogger.prototype.fatal = function(msg) { this._writeRaw("F", msg); };
ToolHubLogger.prototype.cleanupOldFiles = function() {
try {
if (!this.initOk) return false;
var dirF = new java.io.File(this.dir);
var files = dirF.listFiles();
if (!files) return false;
var now = this._now();
var cutoff = now - this.keepDays * 24 * 60 * 60 * 1000;
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (f && f.isFile() && f.getName().indexOf(this.prefix) === 0 && f.lastModified() < cutoff) {
f["delete"]();
}
}
return true;
} catch (e) { return false; }
};
// =======================【崩溃兜底:线程 UncaughtExceptionHandler】=======================
function installCrashHandler(logger) {
try {
if (!logger) return false;
var old = java.lang.Thread.getDefaultUncaughtExceptionHandler();
var h = new JavaAdapter(java.lang.Thread.UncaughtExceptionHandler, {
uncaughtException: function(t, e) {
try {
var tn = "";
try { tn = (t ? String(t.getName()) : ""); } catch (eT) {}
var es = "";
try { es = (e ? String(e) : ""); } catch (eE) {}
logger.fatal("UNCAUGHT thread=" + tn + " err=" + es);
try {
var sw = new java.io.StringWriter();
var pw = new java.io.PrintWriter(sw);
e.printStackTrace(pw);
pw.flush();
logger.fatal("STACKTRACE " + String(sw.toString()));
} catch (eST) {}
} catch (e0) {}
try { if (old) old.uncaughtException(t, e); } catch (e1) {}
}
});
java.lang.Thread.setDefaultUncaughtExceptionHandler(h);
return true;
} catch (e) { return false; }
}
// =======================【主类WM 专属线程模型】=======================