- Split th_2_core.js (4715 lines, 177KB) into: th_02_core.js, th_03_icon.js, th_04_theme.js, th_05_persistence.js, th_06_icon_parser.js, th_07_shortcut.js, th_08_content.js, th_09_animation.js, th_10_shell.js, th_11_action.js, th_12_rebuild.js, th_13_panel_ui.js - Rename existing: th_1_base→th_01_base, th_3_panels→th_14_panels, th_4_extra→th_15_extra, th_5_entry→th_16_entry - Update ToolHub.js MODULE_MANIFEST, modules array, and critical module check
1176 lines
43 KiB
JavaScript
1176 lines
43 KiB
JavaScript
// @version 1.0.0
|
||
// =======================【工具:快捷方式选择器(内置,合并 shortcuts.js)】======================
|
||
// 这段代码的主要内容/用途:在 toolhub.js 内部提供与 shortcuts.js 等价的"快捷方式浏览/选择"页面(仅在点击"选择快捷方式"时触发)。
|
||
// 设计要点:
|
||
// 1) 双线程流水线:UI 线程专管 WM/View;BG 线程专管 icon 加载,避免卡顿/ANR 风险。
|
||
// 2) 稳定性:失败 TTL 熔断 + 限次重试 + icon LRU 上限;滚动触底用轮询,避免 interface listener 的 JavaAdapter 风险。
|
||
// 3) UI 风格:复用 ToolHub 主题(白天/夜晚),避免与主面板割裂;支持关闭与安全销毁。
|
||
// 4) 日志:复用 ToolHubLogger(self.L / _th_log)记录关键步骤与异常。
|
||
FloatBallAppWM.prototype.showShortcutPicker = function(opts) {
|
||
var self = this;
|
||
var opt = opts || {};
|
||
var mode = (opt.mode != null) ? String(opt.mode) : "browse"; // "pick" | "browse"
|
||
var onPick = (typeof opt.onPick === "function") ? opt.onPick : null;
|
||
|
||
// # 会话隔离:生成唯一会话 ID,防止状态污染
|
||
var sessionId = String(new Date().getTime()) + "_" + Math.random().toString(36).substr(2, 9);
|
||
var currentSession = sessionId;
|
||
|
||
// 保存当前会话 ID 到实例(用于新选择器)
|
||
this.__currentShortcutSession = sessionId;
|
||
|
||
// 会话检查函数:所有异步回调都必须检查
|
||
function checkSession() {
|
||
if (self.__currentShortcutSession !== currentSession) {
|
||
safeLog(self.L, 'w', "Shortcut picker session expired, dropping callback");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// 清理函数
|
||
function destroySession() {
|
||
if (self.__currentShortcutSession === currentSession) {
|
||
self.__currentShortcutSession = null;
|
||
}
|
||
}
|
||
|
||
// # 稳定性:复用单例选择器,避免频繁 add/remove View 与线程创建销毁导致 system_server 概率性崩溃
|
||
try {
|
||
if (self.__shortcutPickerSingleton && typeof self.__shortcutPickerSingleton.show === "function") {
|
||
// # 新会话覆盖旧会话
|
||
self.__shortcutPickerSingleton.show(opts);
|
||
return;
|
||
}
|
||
} catch(eSingle) {}
|
||
|
||
|
||
// # 兼容:toolhub.js 全局未必定义 Context 别名,这里显式绑定,避免 ReferenceError。
|
||
var Context = android.content.Context;
|
||
|
||
// # 常量:可按需调小以降低 system_server 内存峰值
|
||
var CFG_PAGE_BATCH = 40;
|
||
var CFG_ICON_CACHE_MAX = 120;
|
||
var CFG_ICON_FAIL_TTL_MS = 15000;
|
||
var CFG_ICON_FAIL_MAX_RETRY = 2;
|
||
var CFG_ICON_LOAD_CONCURRENCY = 2;
|
||
// # 这段代码的主要内容/用途:ShortcutService.getShortcuts 的匹配 flags(与 shortcuts.js 保持一致)
|
||
// # 说明:使用 0x0000000F 可尽量覆盖 Dynamic / Manifest / Pinned / Cached 等类型
|
||
var CFG_MATCH_ALL = 0x0000000F;
|
||
|
||
function now() { try { return new Date().getTime(); } catch(e) { return 0; } }
|
||
function lower(s) { try { return String(s || "").toLowerCase(); } catch(e) { return ""; } }
|
||
function safeStr(s) { try { return String(s == null ? "" : s); } catch(e) { return ""; } }
|
||
|
||
// # 增强版:动态获取系统用户目录(适配不同 ROM)
|
||
function getSystemUserDir() {
|
||
var candidates = [
|
||
"/data/system_ce", // 标准 AOSP
|
||
"/data/system/users", // 部分 MIUI/HyperOS
|
||
"/data/data/system_ce" // 极少数定制系统
|
||
];
|
||
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
try {
|
||
var f = new java.io.File(candidates[i]);
|
||
if (f.exists() && f.isDirectory() && f.canRead()) {
|
||
return candidates[i];
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
// 反射获取环境变量(最可靠但较慢)
|
||
try {
|
||
var env = java.lang.System.getenv("ANDROID_DATA");
|
||
if (env) {
|
||
var envPath = String(env) + "/system_ce";
|
||
var envDir = new java.io.File(envPath);
|
||
if (envDir.exists() && envDir.isDirectory()) {
|
||
return envPath;
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
|
||
// 最终兜底
|
||
return "/data/system_ce";
|
||
}
|
||
|
||
// ==================== Shortcut 枚举(system_ce + shortcut service) ====================
|
||
function listUserIdsFromSystemCE() {
|
||
// # 这段代码的主要内容/用途:扫描 /data/system_ce 下的用户目录,得到可用 userId 列表。
|
||
var out = [];
|
||
try {
|
||
var basePath = getSystemUserDir();
|
||
var base = new java.io.File(basePath);
|
||
if (!base.exists() || !base.isDirectory()) return out;
|
||
var arr = base.listFiles();
|
||
if (!arr) return out;
|
||
for (var i = 0; i < arr.length; i++) {
|
||
var f = arr[i];
|
||
if (!f || !f.isDirectory()) continue;
|
||
var name = safeStr(f.getName());
|
||
if (!name) continue;
|
||
// 只收数字目录
|
||
var ok = true;
|
||
for (var j = 0; j < name.length; j++) {
|
||
var c = name.charCodeAt(j);
|
||
if (c < 48 || c > 57) { ok = false; break; }
|
||
}
|
||
if (!ok) continue;
|
||
out.push(parseInt(name, 10));
|
||
}
|
||
} catch(e) {}
|
||
// 默认兜底:至少有 user 0
|
||
if (out.length === 0) out.push(0);
|
||
return out;
|
||
}
|
||
|
||
function listPackagesHavingShortcuts(userId) {
|
||
// # 这段代码的主要内容/用途:从 shortcut_service 的持久化目录推断哪些包存在快捷方式记录(快速筛选)。
|
||
var out = [];
|
||
try {
|
||
// # 使用动态获取的系统用户目录
|
||
var basePath = "/data/system_ce";
|
||
try {
|
||
if (typeof getSystemUserDir === "function") {
|
||
basePath = getSystemUserDir();
|
||
}
|
||
} catch(eGSD2) {}
|
||
|
||
var dir = new java.io.File(basePath + "/" + String(userId) + "/shortcut_service/packages");
|
||
if (!dir.exists() || !dir.isDirectory()) return out;
|
||
var fs = dir.listFiles();
|
||
if (!fs) return out;
|
||
for (var i = 0; i < fs.length; i++) {
|
||
var f = fs[i];
|
||
if (!f || !f.isFile()) continue;
|
||
var name = safeStr(f.getName());
|
||
if (!name) continue;
|
||
if (name.indexOf(".xml") > 0) {
|
||
var pkg = name.substring(0, name.length - 4);
|
||
if (pkg && pkg.length > 0) out.push(pkg);
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
return out;
|
||
}
|
||
|
||
function getShortcutServiceBinder() {
|
||
// # 这段代码的主要内容/用途:获取 "shortcut" service 的 Binder(不同 ROM 上可能失败,失败则回退到 LauncherApps API 取 icon)。
|
||
try {
|
||
var sm = android.os.ServiceManager;
|
||
return sm.getService("shortcut");
|
||
} catch(e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function getShortcutsForPackage(pkg, userId) {
|
||
// # 这段代码的主要内容/用途:尝试从 shortcut service 拉取指定包的 ShortcutInfo 列表。
|
||
// 说明:此处采用"尽力而为",因为 ROM 兼容性差;失败时返回空数组即可。
|
||
var out = [];
|
||
try {
|
||
// # 修复:与 shortcuts.js 行为一致,优先走 shortcut 服务直连(可拿到"添加到桌面"的微信小程序这类入口)
|
||
// # 说明:LauncherApps.getShortcuts 在部分 ROM/桌面上对非 Launcher 调用者可见性不足,导致列表缺项。
|
||
try {
|
||
var svc = getShortcutServiceBinder();
|
||
if (svc) {
|
||
var slice = null;
|
||
try { slice = svc.getShortcuts(String(pkg), CFG_MATCH_ALL, parseInt(String(userId), 10)); } catch(eS0) { slice = null; }
|
||
if (slice) {
|
||
var listObj = null;
|
||
try { listObj = slice.getList(); } catch(eS1) { listObj = null; }
|
||
if (listObj) {
|
||
try {
|
||
var sz = listObj.size();
|
||
for (var si0 = 0; si0 < sz; si0++) {
|
||
try {
|
||
var s0 = listObj.get(si0);
|
||
if (s0) out.push(s0);
|
||
} catch(eS2) {}
|
||
}
|
||
} catch(eS3) {}
|
||
if (out.length > 0) return out;
|
||
}
|
||
}
|
||
}
|
||
} catch(eSvc) {
|
||
// ignore and fallback
|
||
}
|
||
|
||
// # 兜底:走 LauncherApps.getShortcuts(某些 ROM 上 shortcut 服务直连可能不可用)
|
||
var la = context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
|
||
if (!la) return out;
|
||
|
||
var LauncherApps = android.content.pm.LauncherApps;
|
||
var LauncherAppsShortcutQuery = android.content.pm.LauncherApps.ShortcutQuery;
|
||
|
||
var q = new LauncherAppsShortcutQuery();
|
||
// FLAG_MATCH_*:尽量全拿(动态/固定/清单)
|
||
try {
|
||
q.setQueryFlags(
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_DYNAMIC |
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_PINNED |
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_MANIFEST |
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_CACHED
|
||
);
|
||
} catch(eF) {
|
||
// 某些 ROM 没有 FLAG_MATCH_CACHED
|
||
try {
|
||
q.setQueryFlags(
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_DYNAMIC |
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_PINNED |
|
||
LauncherAppsShortcutQuery.FLAG_MATCH_MANIFEST
|
||
);
|
||
} catch(eF2) {}
|
||
}
|
||
try { q.setPackage(safeStr(pkg)); } catch(eP) {}
|
||
|
||
// user 处理:尽量兼容多用户
|
||
var userHandle = null;
|
||
try {
|
||
if (userId != null) {
|
||
var UserHandle = android.os.UserHandle;
|
||
userHandle = UserHandle.of(parseInt(String(userId), 10));
|
||
}
|
||
} catch(eU) { userHandle = null; }
|
||
|
||
var list = null;
|
||
try {
|
||
if (userHandle) list = la.getShortcuts(q, userHandle);
|
||
else list = la.getShortcuts(q, android.os.Process.myUserHandle());
|
||
} catch(eQ) {
|
||
list = null;
|
||
}
|
||
|
||
if (!list) return out;
|
||
for (var i = 0; i < list.size(); i++) {
|
||
try {
|
||
var si = list.get(i);
|
||
if (si) out.push(si);
|
||
} catch(eI) {}
|
||
}
|
||
} catch(e) {}
|
||
return out;
|
||
}
|
||
|
||
function getAppLabelAsUser(pkg, userId) {
|
||
// # 这段代码的主要内容/用途:获取应用名称(用于分组/搜索显示),失败则返回包名。
|
||
try {
|
||
var pm = context.getPackageManager();
|
||
var ai = pm.getApplicationInfo(String(pkg), 0);
|
||
var lb = pm.getApplicationLabel(ai);
|
||
if (lb != null) return String(lb);
|
||
} catch(e) {}
|
||
return String(pkg);
|
||
}
|
||
|
||
function buildShortcutItemsIndex() {
|
||
// # 这段代码的主要内容/用途:汇总所有 userId 下的快捷方式为统一列表,结构与 shortcuts.js 保持一致。
|
||
var items = [];
|
||
var users = listUserIdsFromSystemCE();
|
||
for (var ui = 0; ui < users.length; ui++) {
|
||
var uid = users[ui];
|
||
var pkgs = listPackagesHavingShortcuts(uid);
|
||
for (var pi = 0; pi < pkgs.length; pi++) {
|
||
var pkg = pkgs[pi];
|
||
if (!pkg) continue;
|
||
var sis = getShortcutsForPackage(pkg, uid);
|
||
if (!sis || sis.length === 0) continue;
|
||
for (var si = 0; si < sis.length; si++) {
|
||
var sInfo = sis[si];
|
||
if (!sInfo) continue;
|
||
var id = "";
|
||
var label = "";
|
||
var intentUri = "";
|
||
try { id = safeStr(sInfo.getId()); } catch(eId) { id = ""; }
|
||
try { label = safeStr(sInfo.getShortLabel()); } catch(eLb) { label = ""; }
|
||
try {
|
||
var it = sInfo.getIntent();
|
||
if (it) intentUri = safeStr(it.toUri(0));
|
||
} catch(eIt) { intentUri = ""; }
|
||
|
||
items.push({
|
||
userId: uid,
|
||
pkg: safeStr(pkg),
|
||
id: id,
|
||
label: label,
|
||
intentUri: intentUri,
|
||
shortcutInfo: sInfo
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return items;
|
||
}
|
||
|
||
// ==================== icon 加载(与 resolveIconDrawable 回退策略一致) ====================
|
||
function loadShortcutIconDrawable(item) {
|
||
// # 这段代码的主要内容/用途:尽力加载 shortcut icon;失败时回退到 app icon。
|
||
try {
|
||
if (!item) return null;
|
||
var la = context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
|
||
if (la && item.shortcutInfo) {
|
||
try {
|
||
// density: 0 让系统自适应(部分 ROM 更稳)
|
||
var dr = la.getShortcutIconDrawable(item.shortcutInfo, 0);
|
||
if (dr) return dr;
|
||
} catch(eLa) {}
|
||
}
|
||
} catch(e0) {}
|
||
// fallback: app icon
|
||
try {
|
||
var pm = context.getPackageManager();
|
||
return pm.getApplicationIcon(String(item.pkg));
|
||
} catch(e1) {}
|
||
return null;
|
||
}
|
||
|
||
// ==================== 分组/过滤 ====================
|
||
function groupItems(items) {
|
||
// # 这段代码的主要内容/用途:按应用分组,tab 显示用;同时保留 "__ALL__"。
|
||
var groups = {};
|
||
var order = [];
|
||
function ensure(key, title) {
|
||
if (groups[key]) return;
|
||
groups[key] = { key: key, title: title, items: [] };
|
||
order.push(key);
|
||
}
|
||
ensure("__ALL__", "全部");
|
||
for (var i = 0; i < items.length; i++) {
|
||
var it = items[i];
|
||
if (!it) continue;
|
||
groups["__ALL__"].items.push(it);
|
||
var k = safeStr(it.pkg);
|
||
if (!groups[k]) {
|
||
ensure(k, getAppLabelAsUser(k, it.userId));
|
||
}
|
||
groups[k].items.push(it);
|
||
}
|
||
// 让 "__ALL__" 固定在第一位,其余按 title 排序(中文/英文混排就随缘)
|
||
try {
|
||
var rest = [];
|
||
for (var j = 0; j < order.length; j++) if (order[j] !== "__ALL__") rest.push(order[j]);
|
||
rest.sort(function(a, b) {
|
||
var ta = safeStr(groups[a].title);
|
||
var tb = safeStr(groups[b].title);
|
||
return ta.localeCompare(tb);
|
||
});
|
||
order = ["__ALL__"].concat(rest);
|
||
} catch(eS) {}
|
||
return { groups: groups, order: order };
|
||
}
|
||
|
||
function filterItems(items, q) {
|
||
var qLower = lower(q || "");
|
||
if (!qLower) return items;
|
||
var out = [];
|
||
for (var i = 0; i < items.length; i++) {
|
||
var it = items[i];
|
||
if (!it) continue;
|
||
var hit = false;
|
||
if (lower(it.label).indexOf(qLower) > -1) hit = true;
|
||
else if (lower(it.pkg).indexOf(qLower) > -1) hit = true;
|
||
else if (lower(it.id).indexOf(qLower) > -1) hit = true;
|
||
else if (lower(it.intentUri).indexOf(qLower) > -1) hit = true;
|
||
if (hit) out.push(it);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// ==================== 主 UI(WM 双线程 + 轮询触底) ====================
|
||
var HandlerThread = android.os.HandlerThread;
|
||
var Handler = android.os.Handler;
|
||
var Runnable = java.lang.Runnable;
|
||
var Thread = java.lang.Thread;
|
||
|
||
var ht = new HandlerThread("sx-toolhub-shortcut-ui");
|
||
ht.start();
|
||
var h = new Handler(ht.getLooper());
|
||
|
||
var bgHt = new HandlerThread("sx-toolhub-shortcut-bg");
|
||
bgHt.start();
|
||
var bgH = new Handler(bgHt.getLooper());
|
||
|
||
var wm = context.getSystemService(Context.WINDOW_SERVICE);
|
||
|
||
var state = {
|
||
destroyed: false,
|
||
hidden: false,
|
||
// # 单例标识:用于避免旧实例的异步回调误操作新实例
|
||
instanceId: String(now()) + "_" + String(Math.random()),
|
||
root: null,
|
||
params: null,
|
||
isAdded: false,
|
||
|
||
allItems: [],
|
||
groups: null,
|
||
groupOrder: [],
|
||
currentGroupKey: "__ALL__",
|
||
|
||
query: "",
|
||
lastFilterToken: 0,
|
||
|
||
iconCache: {},
|
||
iconCacheOrder: [],
|
||
iconCacheMax: CFG_ICON_CACHE_MAX,
|
||
|
||
iconFail: {},
|
||
|
||
iconQ: [],
|
||
iconInFlight: 0,
|
||
iconConc: CFG_ICON_LOAD_CONCURRENCY,
|
||
|
||
grid: null,
|
||
tvStat: null,
|
||
etSearch: null,
|
||
tabRow: null,
|
||
scrollView: null,
|
||
|
||
renderList: [],
|
||
renderCursor: 0,
|
||
isAppending: false,
|
||
lastAppendTryTs: 0,
|
||
|
||
scrollPollRunning: false,
|
||
|
||
// # 回调:关闭/隐藏后通知外层恢复上层 UI(避免被新增按钮页遮挡)
|
||
onDismiss: (opt && typeof opt.onDismiss === "function") ? opt.onDismiss : null,
|
||
onDismissCalled: false
|
||
};
|
||
|
||
|
||
function Ld(msg) { try { if (self.L) self.L.d("[shortcut] " + msg); } catch(e) {} }
|
||
function Li(msg) { try { if (self.L) self.L.i("[shortcut] " + msg); } catch(e) {} }
|
||
function Le(msg) { try { if (self.L) self.L.e("[shortcut] " + msg); } catch(e) {} }
|
||
|
||
function runOn(handler, fn) {
|
||
try {
|
||
handler.post(new JavaAdapter(Runnable, { run: function() { try { fn(); } catch(e) {} } }));
|
||
return true;
|
||
} catch(e) { return false; }
|
||
}
|
||
function hide() {
|
||
// # 这段代码的主要内容/用途:隐藏"选择快捷方式"窗口(不 removeView、不销毁线程),显著降低 system_server 概率性崩溃
|
||
runOn(h, function() {
|
||
// # 会话检查:过期会话直接丢弃
|
||
if (!checkSession()) return;
|
||
|
||
if (state.destroyed) return;
|
||
if (state.hidden) return;
|
||
state.hidden = true;
|
||
state.scrollPollRunning = false;
|
||
|
||
// # 停止队列与异步:隐藏后不再触发 UI/图标加载逻辑
|
||
try { h.removeCallbacksAndMessages(null); } catch(eCB0) {}
|
||
try { bgH.removeCallbacksAndMessages(null); } catch(eCB1) {}
|
||
|
||
// # 输入法/焦点:无论是否弹出输入法,都先退焦点并尝试隐藏软键盘
|
||
try {
|
||
if (state.etSearch) {
|
||
try { state.etSearch.clearFocus(); } catch(eK0) {}
|
||
try {
|
||
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
|
||
if (imm) imm.hideSoftInputFromWindow(state.etSearch.getWindowToken(), 0);
|
||
} catch(eK1) {}
|
||
}
|
||
} catch(eK2) {}
|
||
|
||
// # 仅隐藏 View:不触碰 WM removeView,避免 WM/IME token 状态机被打乱
|
||
try {
|
||
if (state.root) {
|
||
state.root.setVisibility(android.view.View.GONE);
|
||
}
|
||
} catch(eV0) {}
|
||
|
||
// # 退出线程:hide 时也释放线程,避免反复打开/隐藏导致线程堆积
|
||
try {
|
||
var killer = new Thread(new JavaAdapter(Runnable, {
|
||
run: function() {
|
||
try { bgHt.quitSafely(); } catch(e2) {}
|
||
try { ht.quitSafely(); } catch(e3) {}
|
||
}
|
||
}));
|
||
killer.start();
|
||
} catch(eQuit) {}
|
||
|
||
// # 通知外层:选择器已隐藏,可恢复上层面板显示
|
||
try {
|
||
if (state.onDismiss && !state.onDismissCalled) {
|
||
state.onDismissCalled = true;
|
||
try { state.onDismiss(); } catch(eOD0) {}
|
||
}
|
||
} catch(eOD1) {}
|
||
});
|
||
}
|
||
|
||
function destroy() {
|
||
// # 会话清理:标记当前会话已结束
|
||
destroySession();
|
||
|
||
// # 这段代码的主要内容/用途:彻底销毁选择器(仅在 wm_add_failed 等失败场景使用)
|
||
runOn(h, function() {
|
||
if (state.destroyed) return;
|
||
state.destroyed = true;
|
||
state.hidden = true;
|
||
state.scrollPollRunning = false;
|
||
|
||
// # 清理消息队列
|
||
try { h.removeCallbacksAndMessages(null); } catch(eCB0) {}
|
||
try { bgH.removeCallbacksAndMessages(null); } catch(eCB1) {}
|
||
|
||
try { state.iconQ = []; } catch(e0) {}
|
||
state.iconInFlight = 0;
|
||
|
||
// # 通知外层:选择器即将销毁,可恢复上层面板显示
|
||
try {
|
||
if (state.onDismiss && !state.onDismissCalled) {
|
||
state.onDismissCalled = true;
|
||
try { state.onDismiss(); } catch(eOD0) {}
|
||
}
|
||
} catch(eOD1) {}
|
||
|
||
// # 输入法/焦点清理
|
||
try {
|
||
if (state.etSearch) {
|
||
try { state.etSearch.clearFocus(); } catch(eK0) {}
|
||
try {
|
||
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
|
||
if (imm) imm.hideSoftInputFromWindow(state.etSearch.getWindowToken(), 0);
|
||
} catch(eK1) {}
|
||
}
|
||
} catch(eK2) {}
|
||
|
||
// # 尝试移除 View(失败也吞掉,避免把 system_server 再次打穿)
|
||
var rootRef = state.root;
|
||
var wasAdded = state.isAdded;
|
||
try {
|
||
if (rootRef && wasAdded) {
|
||
try { wm.removeViewImmediate(rootRef); } catch(eR0) { try { wm.removeView(rootRef); } catch(eR1) {} }
|
||
}
|
||
} catch(eR2) {}
|
||
|
||
state.isAdded = false;
|
||
state.root = null;
|
||
|
||
// # 单例清理
|
||
try { if (self.__shortcutPickerSingleton && self.__shortcutPickerSingleton.instanceId === state.instanceId) self.__shortcutPickerSingleton = null; } catch(eS0) {}
|
||
|
||
// # 退出线程
|
||
var killer = new Thread(new JavaAdapter(Runnable, {
|
||
run: function() {
|
||
try { bgHt.quitSafely(); } catch(e2) {}
|
||
try { ht.quitSafely(); } catch(e3) {}
|
||
}
|
||
}));
|
||
killer.start();
|
||
});
|
||
}
|
||
|
||
function cacheGet(key) {
|
||
if (state.iconCache.hasOwnProperty(key)) return state.iconCache[key];
|
||
return null;
|
||
}
|
||
|
||
function cachePut(key, drawable) {
|
||
state.iconCache[key] = drawable;
|
||
var seen = false;
|
||
for (var i = state.iconCacheOrder.length - 1; i >= 0; i--) {
|
||
if (state.iconCacheOrder[i] === key) { seen = true; break; }
|
||
}
|
||
if (!seen) state.iconCacheOrder.push(key);
|
||
|
||
while (state.iconCacheOrder.length > state.iconCacheMax) {
|
||
var oldKey = state.iconCacheOrder.shift();
|
||
if (oldKey == null) continue;
|
||
if (oldKey === key) continue;
|
||
try { delete state.iconCache[oldKey]; } catch(e1) { state.iconCache[oldKey] = null; }
|
||
try { delete state.iconFail[oldKey]; } catch(e2) { state.iconFail[oldKey] = null; }
|
||
}
|
||
}
|
||
|
||
function canTryLoadIcon(key) {
|
||
var rec = state.iconFail[key];
|
||
if (!rec) return true;
|
||
var t = now();
|
||
if (rec.count >= CFG_ICON_FAIL_MAX_RETRY) {
|
||
if (t - rec.ts < 300000) return false;
|
||
return true;
|
||
}
|
||
if (t - rec.ts < CFG_ICON_FAIL_TTL_MS) return false;
|
||
return true;
|
||
}
|
||
|
||
function markIconFail(key) {
|
||
var t = now();
|
||
var rec = state.iconFail[key];
|
||
if (!rec) rec = { ts: t, count: 1 };
|
||
else { rec.ts = t; rec.count = (rec.count || 0) + 1; }
|
||
state.iconFail[key] = rec;
|
||
}
|
||
|
||
function enqueueIconLoad(item, iv, key) {
|
||
if (!item || !iv || !key) return;
|
||
if (!canTryLoadIcon(key)) return;
|
||
|
||
state.iconQ.push({ item: item, iv: iv, key: key });
|
||
pumpIconQ();
|
||
}
|
||
|
||
function pumpIconQ() {
|
||
if (state.destroyed) return;
|
||
while (state.iconInFlight < state.iconConc && state.iconQ.length > 0) {
|
||
var job = state.iconQ.shift();
|
||
if (!job) continue;
|
||
state.iconInFlight++;
|
||
|
||
(function(j) {
|
||
runOn(bgH, function() {
|
||
var dr = null;
|
||
try { dr = loadShortcutIconDrawable(j.item); } catch(e0) { dr = null; }
|
||
runOn(h, function() {
|
||
state.iconInFlight--;
|
||
if (state.destroyed) return;
|
||
|
||
if (dr) {
|
||
try { j.iv.setImageDrawable(dr); } catch(e1) {}
|
||
cachePut(j.key, dr);
|
||
} else {
|
||
markIconFail(j.key);
|
||
}
|
||
pumpIconQ();
|
||
});
|
||
});
|
||
})(job);
|
||
}
|
||
}
|
||
|
||
function setStat(text) { try { if (state.tvStat) state.tvStat.setText(String(text)); } catch(e) {} }
|
||
|
||
function rebuildRenderList() {
|
||
var g = state.groups ? state.groups[state.currentGroupKey] : null;
|
||
var base = (g && g.items) ? g.items : state.allItems;
|
||
var filtered = filterItems(base, state.query);
|
||
|
||
state.renderList = filtered;
|
||
state.renderCursor = 0;
|
||
state.isAppending = false;
|
||
state.lastAppendTryTs = 0;
|
||
}
|
||
|
||
function clearGrid() {
|
||
try {
|
||
if (state.grid) state.grid.removeAllViews();
|
||
} catch(e) {}
|
||
}
|
||
|
||
function appendBatch() {
|
||
if (state.destroyed) return;
|
||
if (!state.grid) return;
|
||
|
||
if (state.isAppending) return;
|
||
state.isAppending = true;
|
||
|
||
var start = state.renderCursor;
|
||
var end = Math.min(state.renderCursor + CFG_PAGE_BATCH, state.renderList.length);
|
||
|
||
for (var i = start; i < end; i++) {
|
||
(function(idx) {
|
||
var it = state.renderList[idx];
|
||
|
||
var row = new android.widget.LinearLayout(context);
|
||
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||
|
||
// # 条目间距:卡片式列表,每条之间留 8dp 间隔
|
||
try {
|
||
var lpRow = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||
lpRow.setMargins(0, 0, 0, self.dp(8));
|
||
row.setLayoutParams(lpRow);
|
||
} catch(eLpR) {}
|
||
|
||
row.setPadding(self.dp(12), self.dp(10), self.dp(12), self.dp(10));
|
||
|
||
// # 行背景:与 ToolHub 卡片色一致,并加轻微描边增强层次
|
||
try {
|
||
var isDark = self.isDarkTheme();
|
||
var bgColor = isDark ? self.ui.colors.cardDark : self.ui.colors.cardLight;
|
||
var stroke = isDark ? self.ui.colors.dividerDark : self.ui.colors.dividerLight;
|
||
row.setBackground(self.ui.createStrokeDrawable(bgColor, stroke, self.dp(1), self.dp(12)));
|
||
} catch(eBg) {}
|
||
|
||
var iv = new android.widget.ImageView(context);
|
||
var lpI = new android.widget.LinearLayout.LayoutParams(self.dp(40), self.dp(40));
|
||
lpI.setMargins(0, 0, self.dp(12), 0);
|
||
iv.setLayoutParams(lpI);
|
||
try { iv.setImageResource(android.R.drawable.sym_def_app_icon); } catch(eI0) {}
|
||
|
||
var key = safeStr(it.pkg) + "@" + safeStr(it.id) + "@" + safeStr(it.userId);
|
||
var cached = cacheGet(key);
|
||
if (cached) {
|
||
try { iv.setImageDrawable(cached); } catch(eIC) {}
|
||
} else {
|
||
enqueueIconLoad(it, iv, key);
|
||
}
|
||
|
||
var col = new android.widget.LinearLayout(context);
|
||
col.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||
var lpC = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||
lpC.weight = 1;
|
||
col.setLayoutParams(lpC);
|
||
|
||
var tv1 = new android.widget.TextView(context);
|
||
tv1.setText(safeStr(it.label || it.id || it.pkg));
|
||
tv1.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
|
||
try { tv1.setTypeface(null, android.graphics.Typeface.BOLD); } catch(eB) {}
|
||
try { tv1.setTextColor(self.isDarkTheme() ? self.ui.colors.textPriDark : self.ui.colors.textPriLight); } catch(eC1) {}
|
||
tv1.setSingleLine(true);
|
||
tv1.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||
col.addView(tv1);
|
||
|
||
var tv2 = new android.widget.TextView(context);
|
||
tv2.setText(safeStr(it.pkg) + " / " + safeStr(it.id));
|
||
tv2.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
|
||
tv2.setSingleLine(true);
|
||
tv2.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||
try { tv2.setTextColor(self.isDarkTheme() ? self.ui.colors.textSecDark : self.ui.colors.textSecLight); } catch(eC2) {}
|
||
col.addView(tv2);
|
||
|
||
// # pick 模式:额外显示 userId,避免多用户/工作资料混淆
|
||
if (mode === "pick") {
|
||
var tv3 = new android.widget.TextView(context);
|
||
tv3.setText("userId: " + safeStr(it.userId));
|
||
tv3.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
|
||
tv3.setSingleLine(true);
|
||
try { tv3.setTextColor(self.isDarkTheme() ? self.ui.colors.textSecDark : self.ui.colors.textSecLight); } catch(eC3) {}
|
||
col.addView(tv3);
|
||
}
|
||
|
||
row.addView(iv);
|
||
row.addView(col);
|
||
|
||
// 点击:pick 模式回传;browse 模式尝试启动
|
||
row.setOnClickListener(new android.view.View.OnClickListener({
|
||
onClick: function() {
|
||
try {
|
||
if (mode === "pick") {
|
||
if (onPick) onPick({ pkg: it.pkg, shortcutId: it.id, label: it.label, userId: it.userId, intentUri: it.intentUri });
|
||
hide();
|
||
return;
|
||
}
|
||
// browse:尽力启动(用 LauncherApps.startShortcut)
|
||
try {
|
||
var la = context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
|
||
if (la) {
|
||
var uh = android.os.UserHandle.of(parseInt(String(it.userId), 10));
|
||
la.startShortcut(String(it.pkg), String(it.id), null, null, uh);
|
||
self.toast("已尝试启动: " + safeStr(it.label || it.id));
|
||
} else {
|
||
self.toast("LauncherApps 不可用");
|
||
}
|
||
} catch(eStart) {
|
||
self.toast("启动失败: " + eStart);
|
||
}
|
||
} catch(eClick) {}
|
||
}
|
||
}));
|
||
|
||
state.grid.addView(row);
|
||
|
||
})(i);
|
||
}
|
||
|
||
state.renderCursor = end;
|
||
state.isAppending = false;
|
||
|
||
// 统计
|
||
setStat("快捷方式: " + String(state.renderList.length) + " 已渲染: " + String(state.renderCursor));
|
||
}
|
||
|
||
function setupTabs() {
|
||
try {
|
||
if (!state.tabRow) return;
|
||
state.tabRow.removeAllViews();
|
||
var order = state.groupOrder || [];
|
||
for (var i = 0; i < order.length; i++) {
|
||
(function(key) {
|
||
var g = state.groups[key];
|
||
var tv = new android.widget.TextView(context);
|
||
tv.setText(safeStr(g ? g.title : key));
|
||
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
|
||
tv.setPadding(self.dp(12), self.dp(8), self.dp(12), self.dp(8));
|
||
tv.setSingleLine(true);
|
||
tv.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
||
|
||
var isSel = (key === state.currentGroupKey);
|
||
var isDark = self.isDarkTheme();
|
||
|
||
// # Tabs 选中态:用 accent 低透明底 + 更醒目的文字;未选中态:轻描边
|
||
try {
|
||
var tPri = isDark ? self.ui.colors.textPriDark : self.ui.colors.textPriLight;
|
||
var tSec = isDark ? self.ui.colors.textSecDark : self.ui.colors.textSecLight;
|
||
|
||
var bgColor = isSel ? self.withAlpha(self.ui.colors.accent, isDark ? 0.22 : 0.14)
|
||
: (isDark ? self.withAlpha(android.graphics.Color.BLACK, 0.10)
|
||
: self.withAlpha(android.graphics.Color.BLACK, 0.04));
|
||
var stroke = isDark ? self.ui.colors.dividerDark : self.ui.colors.dividerLight;
|
||
|
||
tv.setTextColor(isSel ? tPri : tSec);
|
||
tv.setBackground(self.ui.createStrokeDrawable(bgColor, stroke, self.dp(1), self.dp(16)));
|
||
} catch(eBg) {}
|
||
|
||
tv.setOnClickListener(new android.view.View.OnClickListener({
|
||
onClick: function() {
|
||
state.currentGroupKey = key;
|
||
rebuildRenderList();
|
||
clearGrid();
|
||
appendBatch();
|
||
setupTabs();
|
||
}
|
||
}));
|
||
|
||
var lp = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||
lp.setMargins(0, 0, self.dp(8), 0);
|
||
tv.setLayoutParams(lp);
|
||
state.tabRow.addView(tv);
|
||
})(order[i]);
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function startScrollPoll() {
|
||
if (state.scrollPollRunning) return;
|
||
state.scrollPollRunning = true;
|
||
|
||
function tick() {
|
||
if (!state.scrollPollRunning || state.destroyed) return;
|
||
runOn(h, function() {
|
||
try {
|
||
if (!state.scrollView) { schedule(); return; }
|
||
var sv = state.scrollView;
|
||
var child = sv.getChildAt(0);
|
||
if (!child) { schedule(); return; }
|
||
|
||
var dy = sv.getScrollY();
|
||
var vh = sv.getHeight();
|
||
var ch = child.getHeight();
|
||
|
||
// 触底阈值:80dp
|
||
var near = (dy + vh) >= (ch - self.dp(80));
|
||
if (near) {
|
||
if (state.renderCursor < state.renderList.length) {
|
||
appendBatch();
|
||
}
|
||
}
|
||
} catch(e) {}
|
||
schedule();
|
||
});
|
||
}
|
||
|
||
function schedule() {
|
||
try {
|
||
h.postDelayed(new JavaAdapter(Runnable, { run: tick }), 180);
|
||
} catch(e) {}
|
||
}
|
||
|
||
schedule();
|
||
}
|
||
|
||
function buildUI() {
|
||
// # 这段代码的主要内容/用途:构建"选择快捷方式"页面 UI(颜色/间距/字体与 ToolHub 面板统一)。
|
||
// 说明:尽量复用 ToolHub 的 UI helper 色板(light/dark),避免出现"黑底黑字/黄底刺眼"等问题。
|
||
|
||
// # root(标准面板容器:圆角 + 轻阴影 + 统一 padding)
|
||
var root = self.ui.createStyledPanel(self, 12);
|
||
|
||
// # 顶栏(标题 + 右侧关闭)
|
||
var top = self.ui.createStyledHeader(self, 10);
|
||
|
||
var title = new android.widget.TextView(context);
|
||
title.setText(mode === "pick" ? "选择快捷方式" : "快捷方式浏览器");
|
||
title.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
|
||
try { title.setTypeface(null, android.graphics.Typeface.BOLD); } catch(e) {}
|
||
try { title.setTextColor(self.isDarkTheme() ? self.ui.colors.textPriDark : self.ui.colors.textPriLight); } catch(e) {}
|
||
|
||
var lpT = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||
lpT.weight = 1;
|
||
title.setLayoutParams(lpT);
|
||
|
||
// 关闭按钮:用 flat 风格,颜色与危险色一致(更像"工具面板"的关闭)
|
||
var btnClose = self.ui.createFlatButton(self, (mode === "pick" ? "取消" : "关闭"), self.ui.colors.danger, function() {
|
||
hide();
|
||
});
|
||
try {
|
||
btnClose.setPadding(self.dp(12), self.dp(6), self.dp(12), self.dp(6));
|
||
btnClose.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 13);
|
||
} catch(e) {}
|
||
|
||
top.addView(title);
|
||
top.addView(btnClose);
|
||
|
||
// # 搜索框(可聚焦 + 统一输入背景)
|
||
var et = new android.widget.EditText(context);
|
||
et.setHint("搜索:名称 / 包名 / ID");
|
||
et.setSingleLine(true);
|
||
et.setPadding(self.dp(12), self.dp(10), self.dp(12), self.dp(10));
|
||
try {
|
||
et.setTextColor(self.isDarkTheme() ? self.ui.colors.textPriDark : self.ui.colors.textPriLight);
|
||
et.setHintTextColor(self.isDarkTheme() ? self.ui.colors.textSecDark : self.ui.colors.textSecLight);
|
||
} catch(e) {}
|
||
|
||
try {
|
||
var inBg = self.ui.createRoundDrawable(self.isDarkTheme() ? self.ui.colors.inputBgDark : self.ui.colors.inputBgLight, self.dp(12));
|
||
et.setBackground(inBg);
|
||
} catch(eBg) {}
|
||
|
||
// # Tabs(横向滚动:统一为"胶囊按钮"风格,选中态更明确)
|
||
var tabSv = new android.widget.HorizontalScrollView(context);
|
||
tabSv.setHorizontalScrollBarEnabled(false);
|
||
tabSv.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER);
|
||
|
||
var tabRow = new android.widget.LinearLayout(context);
|
||
tabRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
|
||
try { tabRow.setPadding(0, self.dp(8), 0, self.dp(6)); } catch(e) {}
|
||
tabSv.addView(tabRow);
|
||
|
||
// # 状态栏(数量/渲染进度)
|
||
var tvStat = new android.widget.TextView(context);
|
||
tvStat.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
|
||
try { tvStat.setTextColor(self.isDarkTheme() ? self.ui.colors.textSecDark : self.ui.colors.textSecLight); } catch(e) {}
|
||
tvStat.setPadding(0, self.dp(6), 0, self.dp(6));
|
||
|
||
// # 列表(卡片式条目列表)
|
||
var sv = new android.widget.ScrollView(context);
|
||
sv.setFillViewport(true);
|
||
sv.setOverScrollMode(android.view.View.OVER_SCROLL_NEVER);
|
||
|
||
var list = new android.widget.LinearLayout(context);
|
||
list.setOrientation(android.widget.LinearLayout.VERTICAL);
|
||
try { list.setPadding(0, self.dp(2), 0, self.dp(2)); } catch(e) {}
|
||
sv.addView(list);
|
||
|
||
// bind
|
||
state.root = root;
|
||
state.etSearch = et;
|
||
state.tabRow = tabRow;
|
||
state.tvStat = tvStat;
|
||
state.grid = list;
|
||
state.scrollView = sv;
|
||
|
||
// listeners:实时过滤(TextWatcher)
|
||
try {
|
||
var TextWatcher = android.text.TextWatcher;
|
||
et.addTextChangedListener(new JavaAdapter(TextWatcher, {
|
||
beforeTextChanged: function() {},
|
||
onTextChanged: function() {},
|
||
afterTextChanged: function(s) {
|
||
try {
|
||
if (state.destroyed) return;
|
||
state.query = safeStr(s);
|
||
rebuildRenderList();
|
||
clearGrid();
|
||
appendBatch();
|
||
} catch(e) {}
|
||
}
|
||
}));
|
||
} catch(eTw) {}
|
||
|
||
// layout
|
||
root.addView(top);
|
||
|
||
// 搜索框与 tabs 之间留一点呼吸空间
|
||
try {
|
||
var lpEt = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
|
||
lpEt.setMargins(0, 0, 0, self.dp(8));
|
||
et.setLayoutParams(lpEt);
|
||
} catch(eLpEt) {}
|
||
root.addView(et);
|
||
|
||
root.addView(tabSv);
|
||
root.addView(tvStat);
|
||
|
||
var lpSv = new android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0);
|
||
lpSv.weight = 1;
|
||
sv.setLayoutParams(lpSv);
|
||
root.addView(sv);
|
||
|
||
// 主题:用 ToolHub 现有的背景更新逻辑兜底(防止外部主题配置影响)
|
||
try { self.updatePanelBackground(root); } catch(eTheme) {}
|
||
|
||
return root;
|
||
}
|
||
|
||
function show() {
|
||
runOn(h, function() {
|
||
// # 会话检查:过期会话直接丢弃
|
||
if (!checkSession()) return;
|
||
|
||
// # 稳定性:复用已添加的 root,避免频繁 add/removeView 触发 system_server WM 状态机崩溃
|
||
if (state.root && state.isAdded) {
|
||
state.hidden = false;
|
||
// # 每次显示前允许再次触发 onDismiss(用于外层恢复)
|
||
state.onDismissCalled = false;
|
||
// # UI 修复:每次重新显示时都重新把窗口定位到屏幕顶部(横向居中 + y=0),避免被其它面板遮挡或位置漂移
|
||
try {
|
||
if (state.params) {
|
||
state.params.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
|
||
// # UI 修复:复用显示时也强制关闭 IME 调整,避免上次输入导致位置被系统推下去
|
||
try {
|
||
state.params.softInputMode = android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
|
||
| android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
|
||
} catch(eSIM1) {}
|
||
try {
|
||
state.params.flags = state.params.flags
|
||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
|
||
} catch(eTop1) {}
|
||
var sw2 = (self.state && self.state.screen && self.state.screen.w) ? self.state.screen.w : 0;
|
||
if (sw2 > 0) state.params.x = Math.max(0, Math.round((sw2 - state.params.width) / 2));
|
||
else state.params.x = 0;
|
||
state.params.y = 0;
|
||
wm.updateViewLayout(state.root, state.params);
|
||
}
|
||
} catch(ePos2) {}
|
||
|
||
// # 层级修复:同类型(TYPE_APPLICATION_OVERLAY)窗口之间的上下层由 addView 顺序决定
|
||
// # 说明:当"新增按钮页/主面板"在本窗口之后 addView 时,会把本窗口盖住;复用 show 仅 setVisibility 无法提升层级
|
||
// # 处理:在 WM 线程内对本窗口做一次 removeViewImmediate + addView 以"提到最上层"(不在关闭时 remove,避免旧崩溃路径)
|
||
try {
|
||
var tsNow = now();
|
||
if (!state.lastRaiseTs || (tsNow - state.lastRaiseTs) > 300) {
|
||
state.lastRaiseTs = tsNow;
|
||
try { wm.removeViewImmediate(state.root); } catch(eZ0) {}
|
||
try { wm.addView(state.root, state.params); } catch(eZ1) {}
|
||
state.isAdded = true;
|
||
}
|
||
} catch(eZ2) {}
|
||
try { state.root.setVisibility(android.view.View.VISIBLE); } catch(eVis) {}
|
||
try {
|
||
setStat("正在加载快捷方式...");
|
||
Li("reloading shortcuts index...");
|
||
} catch(e0) {}
|
||
try {
|
||
state.allItems = buildShortcutItemsIndex();
|
||
var gg = groupItems(state.allItems);
|
||
state.groups = gg.groups;
|
||
state.groupOrder = gg.order;
|
||
} catch(e1) {
|
||
state.allItems = [];
|
||
state.groups = groupItems([]).groups;
|
||
state.groupOrder = ["__ALL__"];
|
||
Le("build index err=" + String(e1));
|
||
}
|
||
try { setupTabs(); } catch(eT0) {}
|
||
try { rebuildRenderList(); } catch(eT1) {}
|
||
try { clearGrid(); } catch(eT2) {}
|
||
try { appendBatch(); } catch(eT3) {}
|
||
try { startScrollPoll(); } catch(eT4) {}
|
||
Li("shortcut picker reused items=" + String(state.allItems.length));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// build data
|
||
setStat("正在加载快捷方式...");
|
||
Li("loading shortcuts index...");
|
||
} catch(e0) {}
|
||
|
||
try {
|
||
state.allItems = buildShortcutItemsIndex();
|
||
var gg = groupItems(state.allItems);
|
||
state.groups = gg.groups;
|
||
state.groupOrder = gg.order;
|
||
} catch(e1) {
|
||
state.allItems = [];
|
||
state.groups = groupItems([]).groups;
|
||
state.groupOrder = ["__ALL__"];
|
||
Le("build index err=" + String(e1));
|
||
}
|
||
|
||
// build UI
|
||
var root = buildUI();
|
||
|
||
// wm params
|
||
var p = new android.view.WindowManager.LayoutParams();
|
||
p.type = android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG; // 修改:提升窗口层级,确保多次打开后仍在最顶层
|
||
// # 说明:TYPE_APPLICATION_OVERLAY 与其他面板同层时会按 add 顺序叠放,频繁开关后容易被后开的面板盖住。
|
||
// # TYPE_SYSTEM_DIALOG 层级更高,可稳定压过 ToolHub 其他面板(仍不频繁 add/remove,避免崩溃)。
|
||
p.flags = android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||
p.format = android.graphics.PixelFormat.TRANSLUCENT;
|
||
// # UI 修复:固定在顶部显示时,避免被输入法/系统 inset 影响导致二次打开位置下移
|
||
// # 说明:不使用 ADJUST_RESIZE/ADJUST_PAN,让窗口位置不被 IME 推动;同时默认隐藏软键盘状态
|
||
try {
|
||
p.softInputMode = android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
|
||
| android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
|
||
} catch(eSIM0) {}
|
||
// # 允许窗口覆盖到屏幕顶部区域(含状态栏区域),避免视觉上"不是贴顶"
|
||
try {
|
||
p.flags = p.flags
|
||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
|
||
| android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
|
||
} catch(eTop0) {}
|
||
p.width = self.dp(340);
|
||
p.height = self.dp(520);
|
||
// # UI 修复:选择快捷方式页应贴近屏幕顶部显示(与"新增按钮页"的顶部布局一致),而不是居中
|
||
// # 说明:居中会让用户感觉"弹窗不是在顶部",也更容易被其它面板遮挡。
|
||
p.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
|
||
try {
|
||
var sw = (self.state && self.state.screen && self.state.screen.w) ? self.state.screen.w : 0;
|
||
// 顶部显示:水平居中 + y=0
|
||
if (sw > 0) p.x = Math.max(0, Math.round((sw - p.width) / 2));
|
||
else p.x = 0;
|
||
p.y = 0;
|
||
} catch(ePos) {
|
||
p.x = 0;
|
||
p.y = 0;
|
||
}
|
||
|
||
// 允许输入法:搜索框要能聚焦
|
||
try {
|
||
p.flags = p.flags & (~android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||
} catch(eF) {}
|
||
|
||
state.params = p;
|
||
|
||
try {
|
||
wm.addView(root, p);
|
||
state.isAdded = true;
|
||
} catch(eAdd) {
|
||
Le("wm addView err=" + String(eAdd));
|
||
destroy();
|
||
self.toast("快捷方式选择器打开失败: wm_add_failed");
|
||
return;
|
||
}
|
||
|
||
// tabs + first render
|
||
setupTabs();
|
||
rebuildRenderList();
|
||
appendBatch();
|
||
startScrollPoll();
|
||
|
||
Li("shortcut picker shown items=" + String(state.allItems.length));
|
||
});
|
||
}
|
||
|
||
// 入口
|
||
// # 创建单例 API:后续再次打开直接复用已添加的 root 与线程,只做"显示/刷新",不做 removeView
|
||
var api = {
|
||
instanceId: state.instanceId,
|
||
show: function(newOpts) {
|
||
try {
|
||
var o = newOpts || {};
|
||
mode = (o.mode != null) ? String(o.mode) : mode;
|
||
onPick = (typeof o.onPick === "function") ? o.onPick : onPick;
|
||
} catch(eOpt) {}
|
||
// # 显示前先把隐藏标记清掉
|
||
try { state.hidden = false; } catch(eH0) {}
|
||
show();
|
||
},
|
||
hide: hide,
|
||
destroy: destroy
|
||
};
|
||
try { self.__shortcutPickerSingleton = api; } catch(eSet) {}
|
||
api.show(opts);
|
||
};
|
||
|