Files
ShortX_ToolHub/code/th_06_icon_parser.js
linshenjianlu 9ad01b436d fix: 代码审查6项修复
- 689处空catch块补全日志
- eval远程代码增加SHA256校验
- 删除ToolHubLogger重复定义
- getParentFile()增加null保护
- 提取buildButtonEditorPanelView内通用工具函数到文件级
- 修复HandlerThread/ValueAnimator资源泄漏
2026-04-21 07:42:23 +08:00

533 lines
22 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.0
// =======================【工具:吸边数据】======================
FloatBallAppWM.prototype.getDockInfo = function() {
var ballSize = this.dp(this.config.BALL_SIZE_DP);
var visible = Math.max(1, Math.round(ballSize * this.config.EDGE_VISIBLE_RATIO));
var hidden = ballSize - visible;
return { ballSize: ballSize, visiblePx: visible, hiddenPx: hidden };
};
// =======================【工具:图标解析】======================
// =======================【工具:快捷方式图标文件路径】======================
FloatBallAppWM.prototype.getShortcutIconFilePath = function(pkg, shortcutId, userId) {
// # 主要用途:把快捷方式图标持久化到 shortcut_icons 目录,供按钮页/按钮管理页稳定显示(桌面移除后仍可显示)
try {
var p = (pkg == null) ? "" : String(pkg);
var s = (shortcutId == null) ? "" : String(shortcutId);
var u = (userId == null) ? "0" : String(userId);
// # 文件名去非法字符,避免路径注入或创建失败
function _sn(v) {
try {
var t = String(v == null ? "" : v);
t = t.replace(/[^a-zA-Z0-9._-]+/g, "_");
if (t.length > 120) t = t.substring(0, 120);
return t;
} catch(e) { return ""; }
}
var dir = String(APP_ROOT_DIR) + "/shortcut_icons";
var fn = _sn(p) + "__" + _sn(s) + "__u" + _sn(u) + ".png";
return dir + "/" + fn;
} catch (e0) {
return "";
}
};
FloatBallAppWM.prototype.resolveIconDrawable = function(btn) {
// # 主要用途:解析面板按钮图标(优先 app 包名图标,其次自定义 resId最后兜底
try {
if (!btn) return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
// # 0) 优先检查 iconPath (绝对路径)
// # 引用优化:复用 loadBallIconDrawableFromFile 安全加载逻辑
if (btn.iconPath) {
try {
var path = String(btn.iconPath);
if (path) {
// targetPx: 面板图标大小; Limit: 1MB, 1024px
var sizeDp = this.config.PANEL_ICON_SIZE_DP || 32;
var dr = this.loadBallIconDrawableFromFile(path, this.dp(sizeDp), 1048576, 1024);
if (dr) return dr;
}
} catch(ePath) { safeLog(null, 'e', "catch " + String(ePath)); }
}
// # 1) type=app 且配置了 pkg自动取应用图标
try {
var t = (btn.type == null) ? "" : String(btn.type);
if (t === "app") {
var pkg = (btn.pkg == null) ? "" : String(btn.pkg);
if (pkg.length > 0) {
// # 统一 LRU 缓存:避免频繁走 PackageManagerDrawable 可复用);并带容量上限,防止无限增长
var kApp = "app|" + pkg;
var hitApp = this._iconLruGet(kApp);
if (hitApp) return hitApp;
var pm = context.getPackageManager();
var drApp = pm.getApplicationIcon(pkg);
if (drApp != null) {
this._iconLruPut(kApp, drApp);
return drApp;
}
}
}
} catch(eApp) { safeLog(null, 'e', "catch " + String(eApp)); }
// # 1.5) type=shortcut尝试取 Shortcuts 快捷方式图标(显示与 shortcuts.js 页面一致)
// # 说明btn 需要包含 pkg + shortcutId图标获取可能较重因此做简单缓存失败则回退到应用图标。
try {
var t2 = (btn.type == null) ? "" : String(btn.type);
if (t2 === "shortcut") {
var pkg2 = (btn.pkg == null) ? "" : String(btn.pkg);
var sid2 = (btn.shortcutId == null) ? "" : String(btn.shortcutId);
if (pkg2.length > 0 && sid2.length > 0) {
// # 1.5.1) 优先从 shortcut_icons 持久化目录读取(桌面移除后仍可显示正确图标)
try {
var iconFilePath0 = this.getShortcutIconFilePath(pkg2, sid2, (btn.userId != null ? String(btn.userId) : "0"));
if (iconFilePath0) {
try {
var f0 = new java.io.File(iconFilePath0);
if (f0.exists() && f0.isFile()) {
var sizeDp0 = this.config.PANEL_ICON_SIZE_DP || 32;
var iconSizePx0 = this.dp(sizeDp0);
var dr0 = this.loadBallIconDrawableFromFile(iconFilePath0, iconSizePx0, 1048576, 1024);
if (dr0) {
// # 写入统一 LRU 缓存:同一个 shortcut 复用 Drawable避免反复解码 PNG
try {
var sk0 = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
this._iconLruPut("sc|" + sk0, dr0);
} catch(eSc0) { safeLog(null, 'e', "catch " + String(eSc0)); }
return dr0;
}
}
} catch(eF0) { safeLog(null, 'e', "catch " + String(eF0)); }
}
} catch(eFile0) { safeLog(null, 'e', "catch " + String(eFile0)); }
var skey = pkg2 + "@" + sid2 + "@" + (btn.userId != null ? String(btn.userId) : "");
var kSc = "sc|" + skey;
var hitSc = this._iconLruGet(kSc);
if (hitSc) return hitSc;
// # 失败冷却:某些 ROM/桌面在短时间内反复查询 Shortcuts 会很慢或直接抛异常。
// # 这里对同一个 shortcut key 做短暂冷却(默认 10 秒),避免面板频繁刷新导致卡顿/ANR 风险。
if (!this._shortcutIconFailTs) this._shortcutIconFailTs = {};
var nowTs = 0;
try { nowTs = new Date().getTime(); } catch(eNow) { nowTs = 0; }
var lastFailTs = this._shortcutIconFailTs[skey];
if (lastFailTs && nowTs > 0 && (nowTs - lastFailTs) < 10000) {
// # 冷却期内直接跳过 shortcut icon 查询,走回退逻辑(应用图标)
} else {
var la = context.getSystemService(android.content.Context.LAUNCHER_APPS_SERVICE);
if (la) {
// # 修复:按钮管理页/按钮面板中,微信小程序等 shortcut 图标显示成宿主 App 图标
// # 根因(工作假设):用 setShortcutIds() 精确查询时,部分 ROM 的实现会返回"退化"的 ShortcutInfo
// # getShortcutIconDrawable 结果被降级为宿主 App 图标。
// # 方案:完全复用 shortcuts.js 的做法--先按 package + flags 拉取该包的 shortcuts 列表,再在列表里按 id 过滤命中项取图标。
// # 注意:不依赖外部 buildShortcutItemsIndex(),避免主程序环境下未加载 shortcuts.js 导致直接回退。
// # 方案:完全复用"选择快捷方式列表"的取数路径,确保微信小程序等 pinned shortcut 能拿到正确的 ShortcutInfo
// # 1) 优先直连 shortcut service选择器同款信息更完整
// # 2) 失败再回退 LauncherApps.getShortcuts
var list = null;
// # 1) shortcut service 直连(与选择器保持一致)
try {
var userIdIntForSvc = 0;
try {
var buid2 = null;
if (btn.userId != null) buid2 = btn.userId;
if (buid2 == null && btn.user != null) buid2 = btn.user;
if (buid2 != null) {
var tmpUid2 = parseInt(String(buid2), 10);
if (!isNaN(tmpUid2)) userIdIntForSvc = tmpUid2;
}
} catch(eUidSvc) { safeLog(null, 'e', "catch " + String(eUidSvc)); }
var sm2 = android.os.ServiceManager;
var shortcutSvc = null;
try { shortcutSvc = sm2.getService("shortcut"); } catch(eSm2) { shortcutSvc = null; }
if (shortcutSvc) {
var CFG_MATCH_ALL2 = 0x0000000F;
var slice2 = null;
try { slice2 = shortcutSvc.getShortcuts(String(pkg2), CFG_MATCH_ALL2, userIdIntForSvc); } catch(eS0b) { slice2 = null; }
if (slice2) {
var listObj2 = null;
try { listObj2 = slice2.getList(); } catch(eS1b) { listObj2 = null; }
if (listObj2) list = listObj2;
}
}
} catch(eSvc2) { safeLog(null, 'e', "catch " + String(eSvc2)); }
// # 2) LauncherApps 回退(当 shortcut service 不可用或返回空时)
if (list == null) {
var q = new android.content.pm.LauncherApps.ShortcutQuery();
try { q.setPackage(pkg2); } catch(eSP) { safeLog(null, 'e', "catch " + String(eSP)); }
// # 重要:必须设置 QueryFlags否则 getShortcuts 可能返回空(默认 flags=0
// # 兼容性:不同 Android/ROM 可能缺少部分 FLAG逐个 try 叠加
try {
var qFlags = 0;
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; } catch(eF1) { safeLog(null, 'e', "catch " + String(eF1)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; } catch(eF2) { safeLog(null, 'e', "catch " + String(eF2)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; } catch(eF3) { safeLog(null, 'e', "catch " + String(eF3)); }
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; } catch(eF4) { safeLog(null, 'e', "catch " + String(eF4)); }
try { q.setQueryFlags(qFlags); } catch(eSF) { safeLog(null, 'e', "catch " + String(eSF)); }
} catch(eQF) { safeLog(null, 'e', "catch " + String(eQF)); }
// # 重要:用户句柄优先用按钮携带的 userId如有否则使用当前用户
var uh = android.os.Process.myUserHandle();
try {
var buid = null;
if (btn.userId != null) buid = btn.userId;
if (buid == null && btn.user != null) buid = btn.user;
if (buid != null) {
var uidInt = parseInt(String(buid), 10);
if (!isNaN(uidInt)) {
uh = android.os.UserHandle.of(uidInt);
}
}
} catch(eUH) { safeLog(null, 'e', "catch " + String(eUH)); }
try { list = la.getShortcuts(q, uh); } catch(eGS) { list = null; }
}
if (list && list.size && list.size() > 0) {
// # 在返回列表中按 shortcutId 精确命中
var si = null;
try {
for (var kk = 0; kk < list.size(); kk++) {
var s0 = list.get(kk);
if (s0 != null) {
var id0 = "";
try { id0 = String(s0.getId()); } catch(eId0) { id0 = ""; }
if (id0 === sid2) { si = s0; break; }
}
}
} catch(eFind) { si = null; }
if (si != null) {
// # 与 shortcuts.js 一致:优先 la.getShortcutIconDrawable(shortcutInfo, 0),再兜底 Icon.loadDrawable
var drSc = null;
try { drSc = la.getShortcutIconDrawable(si, 0); } catch(eIcon0) { drSc = null; }
if (drSc == null) {
try {
var ic = si.getIcon();
if (ic != null) {
var d2 = ic.loadDrawable(context);
if (d2 != null) drSc = d2;
}
} catch(eIcon1) { safeLog(null, 'e', "catch " + String(eIcon1)); }
}
if (drSc != null) {
try { this._iconLruPut("sc|" + skey, drSc); } catch(eSc1) { safeLog(null, 'e', "catch " + String(eSc1)); }
return drSc;
}
}
}
}
// # 如果没拿到 shortcut 图标,记录一次失败时间,触发冷却
try {
if (nowTs > 0) this._shortcutIconFailTs[skey] = nowTs;
else this._shortcutIconFailTs[skey] = new Date().getTime();
} catch(eFT) { safeLog(null, 'e', "catch " + String(eFT)); }
}
// # 回退:取应用图标,至少保证按钮有图标可见
try {
var pm2 = context.getPackageManager();
var drApp2 = pm2.getApplicationIcon(pkg2);
if (drApp2 != null) return drApp2;
} catch(eFall) { safeLog(null, 'e', "catch " + String(eFall)); }
}
}
} catch(eSc) { safeLog(null, 'e', "catch " + String(eSc)); }
// # 2) 显式指定 iconResName (String) 或 iconResId (int)
try {
if (btn.iconResName) {
var drShortx = this.resolveShortXDrawable(btn.iconResName, btn && btn.iconTint ? String(btn.iconTint) : "");
if (drShortx != null) return drShortx;
var name = this.normalizeShortXIconName(btn.iconResName, true);
// # 回退到 android 系统图标
var id = context.getResources().getIdentifier(name, "drawable", "android");
if (id > 0) return context.getResources().getDrawable(id, null);
}
if (btn.iconResId) return context.getResources().getDrawable(btn.iconResId, null);
} catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
// # 3) 兜底
return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
return null;
};
FloatBallAppWM.prototype.getShortXResHandle = function() {
if (this._shortxResHandle) return this._shortxResHandle;
try {
var flags = 0;
try { flags = android.content.Context.CONTEXT_INCLUDE_CODE | android.content.Context.CONTEXT_IGNORE_SECURITY; } catch (eF) { flags = android.content.Context.CONTEXT_RESTRICTED; }
var sxCtx = context.createPackageContext(CONST_SHORTX_PACKAGE, flags);
this._shortxResHandle = {
ctx: sxCtx,
res: sxCtx.getResources(),
cl: sxCtx.getClassLoader(),
pkg: CONST_SHORTX_PACKAGE
};
return this._shortxResHandle;
} catch (e) {
safeLog(this.L, 'w', "getShortXResHandle failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.normalizeShortXIconName = function(name, keepPrefix) {
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return "";
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
if (s.indexOf("ic_remix_") === 0) {
return keepPrefix ? s : s.substring("ic_remix_".length);
}
if (s.indexOf("ic_") === 0) {
return s;
}
return keepPrefix ? ("ic_remix_" + s) : s;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return "";
};
FloatBallAppWM.prototype.getShortXApkPaths = function() {
var out = [];
try {
var handle = this.getShortXResHandle();
var ai = null;
try { if (handle && handle.ctx) ai = handle.ctx.getApplicationInfo(); } catch (eAi0) { ai = null; }
if (ai == null) {
try { ai = context.getPackageManager().getApplicationInfo(CONST_SHORTX_PACKAGE, 0); } catch (eAi1) { ai = null; }
}
function pushPath(p) {
try {
p = String(p || "");
if (!p) return;
if (out.indexOf(p) < 0) out.push(p);
} catch(eP) { safeLog(null, 'e', "catch " + String(eP)); }
}
if (ai) {
try { pushPath(ai.sourceDir); } catch(e0) { safeLog(null, 'e', "catch " + String(e0)); }
try { pushPath(ai.publicSourceDir); } catch(e1) { safeLog(null, 'e', "catch " + String(e1)); }
try {
var ss = ai.splitSourceDirs;
if (ss) {
var i;
for (i = 0; i < ss.length; i++) pushPath(ss[i]);
}
} catch(e2) { safeLog(null, 'e', "catch " + String(e2)); }
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.scanShortXIconsFromApk = function() {
var out = [];
var seen = {};
var paths = this.getShortXApkPaths();
// 宽松匹配:匹配 res/drawable* 和 res/mipmap* 下所有以 ic_ 开头的图标
var regex = /^res\/(drawable[^\/]*|mipmap[^\/]*)\/(ic_[a-z0-9_]+)\.(xml|png|webp|jpg|jpeg)$/;
var lastErr = "";
var totalFiles = 0;
var pi;
for (pi = 0; pi < paths.length; pi++) {
var zip = null;
try {
zip = new java.util.zip.ZipFile(String(paths[pi]));
var en = zip.entries();
while (en.hasMoreElements()) {
var ze = en.nextElement();
var name = String(ze.getName());
totalFiles++;
var m = regex.exec(name);
if (!m) continue;
var fullName = String(m[2]);
// 过滤掉系统图标
if (fullName.indexOf("ic_launcher") === 0 || fullName.indexOf("ic_menu_") === 0) continue;
if (seen[fullName]) continue;
seen[fullName] = true;
out.push({
name: fullName,
shortName: this.normalizeShortXIconName(fullName, false),
id: 0
});
}
} catch (eZip) {
lastErr = String(eZip);
} finally {
try { if (zip) zip.close(); } catch(eClose) { safeLog(null, 'e', "catch " + String(eClose)); }
}
}
if ((!out || out.length === 0) && lastErr) this._shortxIconCatalogError = "APK扫描: " + lastErr + " (路径数=" + paths.length + ", 文件数=" + totalFiles + ")";
return out;
};
FloatBallAppWM.prototype.getShortXIconLookupNames = function(name) {
var out = [];
try {
var s = String(name == null ? "" : name).replace(/^\s+|\s+$/g, "");
if (!s) return out;
if (s.indexOf("@drawable/") === 0) s = s.substring(10);
if (s.indexOf(".") > 0) {
var parts = s.split(".");
s = parts[parts.length - 1];
}
function add(v) {
if (!v) return;
if (out.indexOf(v) < 0) out.push(v);
}
add(s);
if (s.indexOf("ic_remix_") === 0) {
add(s.substring("ic_remix_".length));
} else if (s.indexOf("ic_") !== 0) {
add("ic_remix_" + s);
add("ic_" + s);
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return out;
};
FloatBallAppWM.prototype.resolveShortXDrawableResId = function(name) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return 0;
var cands = this.getShortXIconLookupNames(name);
var i;
for (i = 0; i < cands.length; i++) {
var resId = 0;
try { resId = handle.res.getIdentifier(String(cands[i]), "drawable", handle.pkg); } catch (e1) { resId = 0; }
if (resId > 0) return resId;
}
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return 0;
};
FloatBallAppWM.prototype.resolveShortXDrawable = function(name, tintHex) {
try {
var handle = this.getShortXResHandle();
if (!handle || !handle.res) return null;
var resId = this.resolveShortXDrawableResId(name);
if (resId <= 0) return null;
var dr = handle.res.getDrawable(resId, null);
if (dr && tintHex) {
try {
dr = dr.mutate();
dr.setColorFilter(android.graphics.Color.parseColor(String(tintHex)), android.graphics.PorterDuff.Mode.SRC_IN);
} catch(eTint) { safeLog(null, 'e', "catch " + String(eTint)); }
}
return dr;
} catch(e) { safeLog(null, 'e', "catch " + String(e)); }
return null;
};
FloatBallAppWM.prototype.getShortXIconDrawable = function(name) {
try {
return this.resolveShortXDrawable(name, null);
} catch (e) {
safeLog(this.L, 'w', "getShortXIconDrawable failed: " + String(e));
return null;
}
};
FloatBallAppWM.prototype.getShortXIconCatalog = function(forceReload) {
if (!forceReload && this._shortxIconCatalog) return this._shortxIconCatalog;
var out = [];
this._shortxIconCatalogError = "";
try {
var handle = this.getShortXResHandle();
if (handle && handle.cl) {
// 策略1反射 R$drawable 类(未混淆时可用)
try {
var clz = handle.cl.loadClass(CONST_SHORTX_PACKAGE + ".R$drawable");
var fields = clz.getFields();
var i;
for (i = 0; i < fields.length; i++) {
try {
var f = fields[i];
var fname = String(f.getName());
if (fname.indexOf("ic_remix_") !== 0 && fname.indexOf("ic_") !== 0) continue;
out.push({
name: fname,
shortName: (fname.indexOf("ic_remix_") === 0) ? fname.substring("ic_remix_".length) : fname,
id: f.getInt(null)
});
} catch(eField) { safeLog(null, 'e', "catch " + String(eField)); }
}
} catch (eClz) {
this._shortxIconCatalogError = "R$drawable reflect: " + String(eClz);
safeLog(this.L, 'w', "getShortXIconCatalog R$drawable failed: " + String(eClz));
}
// 策略2直接遍历资源 ID绕过混淆更稳定
if (!out || out.length === 0) {
try {
var ctx = context.createPackageContext(CONST_SHORTX_PACKAGE, android.content.Context.CONTEXT_IGNORE_SECURITY);
var res = ctx.getResources();
var startId = 2131230000;
var endId = 2131240000;
var count = 0;
for (var id = startId; id < endId && count < 20000; id++) {
try {
var rname = res.getResourceName(id);
if (rname && rname.indexOf("/ic_remix_") > 0) {
var parts = rname.split("/");
var resName = parts[parts.length - 1];
out.push({
name: resName,
shortName: resName.substring("ic_remix_".length),
id: id
});
count++;
}
} catch (eId) {
// ID 无效,跳过
}
}
} catch (eRes) {
this._shortxIconCatalogError = (this._shortxIconCatalogError ? this._shortxIconCatalogError + "; " : "") + "res scan: " + String(eRes);
safeLog(this.L, 'w', "getShortXIconCatalog res scan failed: " + String(eRes));
}
}
}
// 策略3APK 文件扫描(最后兜底)
if (!out || out.length === 0) {
out = this.scanShortXIconsFromApk();
}
out.sort(function(a, b) {
var aw = a.name.indexOf("ic_remix_") === 0 ? 1 : 0;
var bw = b.name.indexOf("ic_remix_") === 0 ? 1 : 0;
if (aw !== bw) return aw - bw;
var as = String(a.shortName || a.name);
var bs = String(b.shortName || b.name);
return as < bs ? -1 : (as > bs ? 1 : 0);
});
if (!out || out.length === 0) {
if (!this._shortxIconCatalogError) this._shortxIconCatalogError = "所有策略均未获取到图标";
} else {
this._shortxIconCatalogError = "";
}
} catch (e) {
this._shortxIconCatalogError = String(e);
safeLog(this.L, 'w', "getShortXIconCatalog failed: " + String(e));
}
this._shortxIconCatalog = out;
return out;
};