// @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 缓存:避免频繁走 PackageManager(Drawable 可复用);并带容量上限,防止无限增长 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)); } } } // 策略3:APK 文件扫描(最后兜底) 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; };