Files
ShortX_ToolHub/code/th_2_core.js

4716 lines
173 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
function FloatBallAppWM(logger) {
this.L = logger || null;
// # 加载配置
this.config = ConfigManager.loadSettings();
this.currentPanelKey = "main";
this.panels = { main: ConfigManager.loadButtons() };
// # 更新 Logger 配置(因为 Logger 初始化时是默认值)
if (this.L) this.L.updateConfig(this.config);
this.state = {
receivers: [], // 存储广播接收器引用,用于 close 时注销
wm: null,
dm: null,
density: 1.0,
screen: { w: 0, h: 0 },
lastRotation: -1,
lastMonitorTs: 0,
ht: null,
h: null,
addedBall: false,
addedPanel: false,
addedSettings: false,
addedViewer: false,
addedMask: false,
ballRoot: null,
ballContent: null,
ballLp: null,
panel: null,
panelLp: null,
settingsPanel: null,
settingsPanelLp: null,
viewerPanel: null,
viewerPanelLp: null,
mask: null,
maskLp: null,
loadedPos: null,
lastSaveTs: 0,
dragging: false,
rawX: 0,
rawY: 0,
downX: 0,
downY: 0,
docked: false,
dockSide: null,
lastMotionTs: 0,
idleDockRunnable: null,
longPressArmed: false,
longPressTriggered: false,
longPressRunnable: null,
displayListener: null,
// # 设置面板:临时编辑缓存
pendingUserCfg: null,
pendingDirty: false,
closing: false
};
// # 创建实例独立的 UI 工具对象,避免多实例共享颜色状态
this.ui = {};
var protoUi = FloatBallAppWM.prototype.ui;
for (var _uiKey in protoUi) {
this.ui[_uiKey] = protoUi[_uiKey];
}
this.ui.colors = {};
// # 初始化莫奈动态配色(传入当前主题避免重复检测)
try { this.refreshMonetColors(this.isDarkTheme()); } catch (eM) {}
}
// =======================【工具dp/now/clamp】======================
FloatBallAppWM.prototype.dp = function(v) { return Math.floor(Number(v) * this.state.density); };
FloatBallAppWM.prototype.sp = function(v) { try { return Math.floor(Number(v) * context.getResources().getDisplayMetrics().scaledDensity); } catch (e) { return Math.floor(Number(v) * this.state.density); } };
FloatBallAppWM.prototype.now = function() { return new Date().getTime(); };
FloatBallAppWM.prototype.clamp = function(v, min, max) { if (v < min) return min; if (v > max) return max; return v; };
FloatBallAppWM.prototype.rectIntersect = function(ax, ay, aw, ah, bx, by, bw, bh) {
return !(ax + aw <= bx || bx + bw <= ax || ay + ah <= by || by + bh <= ay);
};
// # 这段代码的主要内容/用途:安全地在 UI 线程执行代码(用于后台线程回调更新 UI
FloatBallAppWM.prototype.runOnUiThreadSafe = function(fn) {
try {
if (!fn) return;
var self = this;
// 优先使用 Activity 的 runOnUiThread否则使用 View.post
if (this.state && this.state.ballRoot) {
this.state.ballRoot.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) {} }
}));
} else {
// 兜底:直接执行(如果已经在 UI 线程)或尝试使用 Handler
try {
var h = new android.os.Handler(android.os.Looper.getMainLooper());
h.post(new java.lang.Runnable({
run: function() { try { fn.call(self); } catch(e) {} }
}));
} catch(e) {
// 最后兜底:直接执行
try { fn.call(self); } catch(e2) {}
}
}
} catch(e) {}
};
// # 这段代码的主要内容/用途统一图标缓存LRU减少反复解码/反复走 PackageManager降低卡顿与内存波动不改变 UI 与功能)
// 优化后的图标缓存(带 Bitmap 回收,防止内存泄漏)
FloatBallAppWM.prototype._iconCache = {
map: {},
keys: [],
max: 80, // 减少缓存数量,降低内存压力
get: function(key) {
var item = this.map[key];
if (!item) return null;
// 移动到末尾(最近使用)
var idx = this.keys.indexOf(key);
if (idx > -1) {
this.keys.splice(idx, 1);
this.keys.push(key);
}
return item.dr;
},
put: function(key, drawable) {
// 清理旧的
if (this.map[key]) {
this._remove(key);
}
// 空间检查:超过 80% 时批量清理 20%
if (this.keys.length >= this.max * 0.8) {
var removeCount = Math.floor(this.max * 0.2);
for (var i = 0; i < removeCount && this.keys.length > 0; i++) {
var oldKey = this.keys.shift();
this._remove(oldKey);
}
}
this.keys.push(key);
this.map[key] = {dr: drawable, ts: Date.now()};
},
_remove: function(key) {
var item = this.map[key];
if (item && item.dr) {
// 关键:回收 Bitmap防止内存泄漏
try {
if (item.dr instanceof android.graphics.drawable.BitmapDrawable) {
var bmp = item.dr.getBitmap();
if (bmp && !bmp.isRecycled()) bmp.recycle();
}
} catch(e) {}
delete this.map[key];
}
},
clear: function() {
for (var i = 0; i < this.keys.length; i++) {
this._remove(this.keys[i]);
}
this.keys = [];
}
};
// 兼容性封装(保持原有调用方式不变)
FloatBallAppWM.prototype._iconLruEnsure = function() {};
FloatBallAppWM.prototype._iconLruGet = function(key) {
return this._iconCache.get(key);
};
FloatBallAppWM.prototype._iconLruPut = function(key, val) {
try {
this._iconLruEnsure(120);
var k = String(key || "");
if (!k) return;
if (val == null) return;
// # 若已存在,先移除旧顺序位置
try {
var ord = this._iconLru.order;
for (var i = ord.length - 1; i >= 0; i--) {
if (ord[i] === k) { ord.splice(i, 1); break; }
}
ord.push(k);
} catch (eLru3) {}
this._iconLru.map[k] = val;
// # 超限清理:按最久未使用淘汰
try {
var maxN = Math.max(20, Math.floor(Number(this._iconLru.max || 120)));
var ord2 = this._iconLru.order;
while (ord2.length > maxN) {
var oldK = ord2.shift();
if (oldK != null) {
try { delete this._iconLru.map[oldK]; } catch (eDel) {}
}
}
} catch (eLru4) {}
} catch (eLru5) {}
};
// =======================【工具悬浮球图标PNG 文件)】======================
// # 这段代码的主要内容/用途:从指定路径加载透明 PNG 作为悬浮球图标;带"文件大小/像素上限"保护;按目标尺寸采样解码,避免 system_server OOM。
FloatBallAppWM.prototype.loadBallIconDrawableFromFile = function(path, targetPx, maxBytes, maxPx) {
try {
var p = String(path || "");
if (!p) return null;
// # 统一 LRU 缓存:文件图标(按 path + targetPx + mtime + size 复用 Drawable避免反复解码
var f = new java.io.File(p);
if (!f.exists() || !f.isFile()) return null;
var ckLru = null;
try {
ckLru = "file|" + p + "@" + String(targetPx == null ? "" : targetPx) + "@" + String(f.lastModified()) + "@" + String(f.length());
var hitLru = this._iconLruGet(ckLru);
if (hitLru) return hitLru;
} catch (eLruF0) {}
// # 文件大小限制(字节)
var limitBytes = Math.max(0, Math.floor(Number(maxBytes || 0)));
if (limitBytes > 0) {
try {
var sz = Number(f.length());
if (sz > limitBytes) return null;
} catch (eSz) { return null; }
}
// # 先只读尺寸(不解码)
var opt = new android.graphics.BitmapFactory.Options();
opt.inJustDecodeBounds = true;
try { android.graphics.BitmapFactory.decodeFile(p, opt); } catch (eB0) { return null; }
var w = Number(opt.outWidth || 0);
var h = Number(opt.outHeight || 0);
if (w <= 0 || h <= 0) return null;
// # 像素边长上限(宽/高任意一边超限则拒绝)
var limitPx = Math.max(0, Math.floor(Number(maxPx || 0)));
if (limitPx > 0) {
if (w > limitPx || h > limitPx) return null;
}
// # 计算采样倍率:按目标尺寸(一般为 iconSizePx采样
var tp = Math.max(1, Math.floor(Number(targetPx || 1)));
// # 允许解码到目标的 2 倍以内,减少锯齿又不浪费内存
var desired = Math.max(tp * 2, tp);
var sample = 1;
while ((w / sample) > desired || (h / sample) > desired) sample = sample * 2;
if (sample < 1) sample = 1;
var opt2 = new android.graphics.BitmapFactory.Options();
opt2.inJustDecodeBounds = false;
opt2.inSampleSize = sample;
opt2.inPreferredConfig = android.graphics.Bitmap.Config.ARGB_8888;
var bmp = null;
try { bmp = android.graphics.BitmapFactory.decodeFile(p, opt2); } catch (eB1) { bmp = null; }
if (bmp == null) return null;
var d = new android.graphics.drawable.BitmapDrawable(context.getResources(), bmp);
// # 写入统一 LRU 缓存
try {
if (ckLru) this._iconLruPut(ckLru, d);
} catch (eLruF1) {}
return d;
} catch (e0) {
return null;
}
};
// =======================【工具:屏幕/旋转】======================
FloatBallAppWM.prototype.getScreenSizePx = function() {
var m = new android.util.DisplayMetrics();
try { this.state.wm.getDefaultDisplay().getRealMetrics(m); } catch (e) { this.state.wm.getDefaultDisplay().getMetrics(m); }
return { w: m.widthPixels, h: m.heightPixels };
};
FloatBallAppWM.prototype.getRotation = function() { try { return this.state.wm.getDefaultDisplay().getRotation(); } catch (e) {} return -1; };
// =======================【工具alpha/toast/vibrate】======================
FloatBallAppWM.prototype.withAlpha = function(colorInt, alpha01) { var a = Math.floor(Number(alpha01) * 255); return (colorInt & 0x00FFFFFF) | (a << 24); };
FloatBallAppWM.prototype.toast = function(msg) { try { android.widget.Toast.makeText(context, String(msg), 0).show(); } catch (e) {} };
FloatBallAppWM.prototype.vibrateOnce = function(ms) {
if (!this.config.LONG_PRESS_HAPTIC_ENABLE) return;
try {
var vib = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (!vib) return;
var dur = Math.max(1, Math.floor(ms));
if (android.os.Build.VERSION.SDK_INT >= 26) {
var ve = android.os.VibrationEffect.createOneShot(dur, android.os.VibrationEffect.DEFAULT_AMPLITUDE);
vib.vibrate(ve);
} else {
vib.vibrate(dur);
}
} catch (e) {}
};
// =======================【工具UI样式辅助】======================
FloatBallAppWM.prototype.ui = {
// 基础颜色
colors: {
// 以下为默认回退值,实例化时会被 refreshMonetColors() 覆盖为系统莫奈色
primary: android.graphics.Color.parseColor("#005BC0"),
primaryDark: android.graphics.Color.parseColor("#041E49"),
accent: android.graphics.Color.parseColor("#00639B"),
danger: android.graphics.Color.parseColor("#BA1A1A"),
success: android.graphics.Color.parseColor("#15803d"),
warning: android.graphics.Color.parseColor("#b45309"),
bgLight: android.graphics.Color.parseColor("#F8F9FA"),
bgDark: android.graphics.Color.parseColor("#131314"),
cardLight: android.graphics.Color.parseColor("#E1E3E1"),
cardDark: android.graphics.Color.parseColor("#49454F"),
textPriLight: android.graphics.Color.parseColor("#1F1F1F"),
textPriDark: android.graphics.Color.parseColor("#E3E3E3"),
textSecLight: android.graphics.Color.parseColor("#5F6368"),
textSecDark: android.graphics.Color.parseColor("#C4C7C5"),
dividerLight: android.graphics.Color.parseColor("#747775"),
dividerDark: android.graphics.Color.parseColor("#8E918F"),
inputBgLight: android.graphics.Color.parseColor("#F8F9FA"),
inputBgDark: android.graphics.Color.parseColor("#131314"),
// Monet 扩展字段(供面板直接使用)
_monetSurface: android.graphics.Color.parseColor("#F8F9FA"),
_monetOnSurface: android.graphics.Color.parseColor("#1F1F1F"),
_monetOutline: android.graphics.Color.parseColor("#747775"),
_monetOnPrimary: android.graphics.Color.parseColor("#FFFFFF"),
_monetPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
_monetOnPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
_monetSecondary: android.graphics.Color.parseColor("#00639B"),
_monetTertiary: android.graphics.Color.parseColor("#5C5891")
},
// 创建圆角背景 (Solid)
createRoundDrawable: function(color, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
d.setColor(color);
d.setCornerRadius(radiusPx);
return d;
},
// 创建圆角描边背景 (Stroke)
createStrokeDrawable: function(fillColor, strokeColor, strokeWidthPx, radiusPx) {
var d = new android.graphics.drawable.GradientDrawable();
d.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
if (fillColor) d.setColor(fillColor);
d.setCornerRadius(radiusPx);
d.setStroke(strokeWidthPx, strokeColor);
return d;
},
// 创建按压反馈背景 (StateList)
createRippleDrawable: function(normalColor, pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = this.createRoundDrawable(normalColor, radiusPx);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 创建纯色按压反馈 (StateList) - 用于透明背景按钮
createTransparentRippleDrawable: function(pressedColor, radiusPx) {
var sd = new android.graphics.drawable.StateListDrawable();
var p = this.createRoundDrawable(pressedColor, radiusPx);
var n = new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT);
sd.addState([android.R.attr.state_pressed], p);
sd.addState([], n);
return sd;
},
// 辅助:创建扁平按钮
createFlatButton: function(app, txt, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setPadding(app.dp(12), app.dp(6), app.dp(12), app.dp(6));
btn.setGravity(android.view.Gravity.CENTER);
// use divider color or just low alpha text color for ripple
var rippleColor = app.withAlpha ? app.withAlpha(txtColor, 0.1) : 0x22888888;
btn.setBackground(this.createTransparentRippleDrawable(rippleColor, app.dp(8)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建实心按钮
createSolidButton: function(app, txt, bgColor, txtColor, onClick) {
var btn = new android.widget.TextView(context);
btn.setText(txt);
btn.setTextColor(txtColor);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setPadding(app.dp(16), app.dp(8), app.dp(16), app.dp(8));
btn.setGravity(android.view.Gravity.CENTER);
var pressedColor = app.withAlpha ? app.withAlpha(bgColor, 0.8) : bgColor;
btn.setBackground(this.createRippleDrawable(bgColor, pressedColor, app.dp(24)));
try { btn.setElevation(app.dp(2)); } catch(e){}
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) { app.touchActivity(); app.guardClick("ui_btn", INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS, function(){ if(onClick) onClick(v); }); }
}));
return btn;
},
// 辅助:创建带标签的输入组(支持粘贴)
createInputGroup: function(app, label, initVal, isMultiLine, hint) {
var box = new android.widget.LinearLayout(context);
box.setOrientation(android.widget.LinearLayout.VERTICAL);
box.setPadding(0, 0, 0, app.dp(12));
var topLine = new android.widget.LinearLayout(context);
topLine.setOrientation(android.widget.LinearLayout.HORIZONTAL);
topLine.setGravity(android.view.Gravity.CENTER_VERTICAL);
box.addView(topLine);
var lb = new android.widget.TextView(context);
lb.setText(label);
lb.setTextColor(this.colors.textSecLight); // 默认用浅色主题副文本色,外部可覆盖
try { if (app.isDarkTheme && app.isDarkTheme()) lb.setTextColor(this.colors.textSecDark); } catch(e){}
lb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
var lpLb = new android.widget.LinearLayout.LayoutParams(0, -2);
lpLb.weight = 1;
topLine.addView(lb, lpLb);
var et = new android.widget.EditText(context);
et.setText(initVal ? String(initVal) : "");
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setTextColor(this.colors.textPriLight);
try { if (app.isDarkTheme && app.isDarkTheme()) et.setTextColor(this.colors.textPriDark); } catch(e){}
// 输入框背景优化
var strokeColor = this.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = this.colors.dividerDark; } catch(e){}
var bg = this.createStrokeDrawable(this.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) bg = this.createStrokeDrawable(this.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e){}
et.setBackground(bg);
et.setPadding(app.dp(8), app.dp(8), app.dp(8), app.dp(8));
if (hint) et.setHint(hint);
if (isMultiLine) {
et.setSingleLine(false);
et.setMaxLines(4);
} else {
et.setSingleLine(true);
}
// 粘贴功能
var pasteBtn = this.createFlatButton(app, "粘贴", this.colors.accent, function() {
try {
var cb = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE);
if (cb.hasPrimaryClip()) {
var item = cb.getPrimaryClip().getItemAt(0);
if (item) {
var txt = item.getText();
if (txt) {
var st = String(txt);
var old = String(et.getText());
if (old.length > 0) et.setText(old + st);
else et.setText(st);
et.setSelection(et.getText().length());
}
}
} else {
app.toast("剪贴板为空");
}
} catch (eP) {
app.toast("粘贴失败: " + eP);
}
});
// 调整粘贴按钮样式使其更紧凑
pasteBtn.setPadding(app.dp(8), app.dp(2), app.dp(8), app.dp(2));
pasteBtn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
topLine.addView(pasteBtn);
box.addView(et);
// 错误提示
var errTv = new android.widget.TextView(context);
errTv.setTextColor(this.colors.danger);
errTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 10);
errTv.setVisibility(android.view.View.GONE);
box.addView(errTv);
var self = this;
return {
view: box,
input: et,
getValue: function() { return String(et.getText()); },
setError: function(msg) {
if (msg) {
errTv.setText(msg);
errTv.setVisibility(android.view.View.VISIBLE);
et.setBackground(self.createRoundDrawable(app.withAlpha(self.colors.danger, 0.2), app.dp(4)));
} else {
errTv.setVisibility(android.view.View.GONE);
var strokeColor = self.colors.dividerLight;
try { if (app.isDarkTheme && app.isDarkTheme()) strokeColor = self.colors.dividerDark; } catch(e){}
var normalBg = self.createStrokeDrawable(self.colors.inputBgLight, strokeColor, app.dp(1), app.dp(8));
try { if (app.isDarkTheme && app.isDarkTheme()) normalBg = self.createStrokeDrawable(self.colors.inputBgDark, strokeColor, app.dp(1), app.dp(8)); } catch(e){}
et.setBackground(normalBg);
}
}
};
},
// 辅助:创建标准面板容器
createStyledPanel: function(app, paddingDp) {
var isDark = app.isDarkTheme();
var bgColor = isDark ? this.colors.bgDark : this.colors.bgLight;
var panel = new android.widget.LinearLayout(context);
panel.setOrientation(android.widget.LinearLayout.VERTICAL);
var bgDr = new android.graphics.drawable.GradientDrawable();
bgDr.setColor(bgColor);
bgDr.setCornerRadius(app.dp(16));
panel.setBackground(bgDr);
try { panel.setElevation(app.dp(8)); } catch(e){}
var p = (paddingDp !== undefined) ? app.dp(paddingDp) : app.dp(16);
panel.setPadding(p, p, p, p);
return panel;
},
// 辅助:创建标准标题栏容器
createStyledHeader: function(app, paddingBottomDp) {
var header = new android.widget.LinearLayout(context);
header.setOrientation(android.widget.LinearLayout.HORIZONTAL);
header.setGravity(android.view.Gravity.CENTER_VERTICAL);
var pb = (paddingBottomDp !== undefined) ? app.dp(paddingBottomDp) : app.dp(8);
header.setPadding(0, 0, 0, pb);
return header;
},
// 辅助:创建占位符(撑开空间)
createSpacer: function(app) {
var dummy = new android.view.View(context);
var dummyLp = new android.widget.LinearLayout.LayoutParams(0, 1);
dummyLp.weight = 1;
dummy.setLayoutParams(dummyLp);
return dummy;
}
};
// =======================【工具:主题/类莫奈颜色】======================
// # 主题调试日志工具(仅打印,不改变逻辑)
function _th_hex(c) {
try {
var Integer = Packages.java.lang.Integer;
var s = Integer.toHexString(c);
while (s.length < 8) s = "0" + s;
return "0x" + s;
} catch (e) {
try { return String(c); } catch (e2) { return "<?>"; }
}
}
function _th_argb(c) {
try {
var Color = Packages.android.graphics.Color;
var ci = Math.floor(Number(c));
if (isNaN(ci)) return "NaN";
return "a=" + Color.alpha(ci) + " r=" + Color.red(ci) + " g=" + Color.green(ci) + " b=" + Color.blue(ci);
} catch (e) {
return "argb_err";
}
}
function _th_log(L, level, msg) {
try {
if (!L) return;
if (level === "e" && L.e) { L.e(msg); return; }
if (level === "w" && L.w) { L.w(msg); return; }
if (level === "i" && L.i) { L.i(msg); return; }
if (L.d) { L.d(msg); return; }
} catch (e) {}
}
// =======================【莫奈动态取色工具】======================
var MonetColorProvider = {
_cacheLight: null,
_cacheDark: null,
_getResColor: function(resName, fallbackHex) {
try {
var res = android.content.res.Resources.getSystem();
var id = res.getIdentifier(resName, "color", "android");
if (id > 0) return res.getColor(id, null);
} catch (e) {}
try { return android.graphics.Color.parseColor(fallbackHex); } catch (e) { return 0; }
},
_getFallbackLight: function() {
return {
// 使用更接近 AOSP Monet 标准值的 fallback日间 primary 更饱和、对比度更高
primary: android.graphics.Color.parseColor("#005BC0"),
onPrimary: android.graphics.Color.parseColor("#FFFFFF"),
primaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
onPrimaryContainer: android.graphics.Color.parseColor("#041E49"),
secondary: android.graphics.Color.parseColor("#00639B"),
secondaryContainer: android.graphics.Color.parseColor("#C2E4FF"),
tertiary: android.graphics.Color.parseColor("#5C5891"),
surface: android.graphics.Color.parseColor("#F8F9FA"),
onSurface: android.graphics.Color.parseColor("#1F1F1F"),
surfaceVariant: android.graphics.Color.parseColor("#E1E3E1"),
onSurfaceVariant: android.graphics.Color.parseColor("#5F6368"),
outline: android.graphics.Color.parseColor("#747775"),
outlineVariant: android.graphics.Color.parseColor("#C4C7C5"),
error: android.graphics.Color.parseColor("#BA1A1A"),
errorContainer: android.graphics.Color.parseColor("#F9DEDC"),
onErrorContainer: android.graphics.Color.parseColor("#410E0B")
};
},
_getFallbackDark: function() {
return {
primary: android.graphics.Color.parseColor("#A8C7FA"),
onPrimary: android.graphics.Color.parseColor("#062E6F"),
primaryContainer: android.graphics.Color.parseColor("#0842A0"),
onPrimaryContainer: android.graphics.Color.parseColor("#D3E3FD"),
secondary: android.graphics.Color.parseColor("#7FCFFF"),
secondaryContainer: android.graphics.Color.parseColor("#004A77"),
tertiary: android.graphics.Color.parseColor("#C2C5DD"),
surface: android.graphics.Color.parseColor("#131314"),
onSurface: android.graphics.Color.parseColor("#E3E3E3"),
surfaceVariant: android.graphics.Color.parseColor("#49454F"),
onSurfaceVariant: android.graphics.Color.parseColor("#C4C7C5"),
outline: android.graphics.Color.parseColor("#8E918F"),
outlineVariant: android.graphics.Color.parseColor("#49454F"),
error: android.graphics.Color.parseColor("#F2B8B5"),
errorContainer: android.graphics.Color.parseColor("#8C1D18"),
onErrorContainer: android.graphics.Color.parseColor("#F9DEDC")
};
},
_loadMonet: function(isDark) {
var c = isDark ? this._getFallbackDark() : this._getFallbackLight();
try {
var res = android.content.res.Resources.getSystem();
var map = isDark ? {
primary: "system_accent1_200",
onPrimary: "system_accent1_800",
primaryContainer: "system_accent1_700",
onPrimaryContainer: "system_accent1_100",
secondary: "system_accent2_200",
secondaryContainer: "system_accent2_700",
tertiary: "system_accent3_200",
surface: "system_neutral1_900",
onSurface: "system_neutral1_100",
surfaceVariant: "system_neutral2_700",
onSurfaceVariant: "system_neutral2_200",
outline: "system_neutral2_400",
outlineVariant: "system_neutral2_700",
error: "system_accent3_200",
errorContainer: "system_accent3_800",
onErrorContainer: "system_accent3_100"
} : {
primary: "system_accent1_600",
onPrimary: "system_accent1_0",
primaryContainer: "system_accent1_100",
onPrimaryContainer: "system_accent1_900",
secondary: "system_accent2_600",
secondaryContainer: "system_accent2_100",
tertiary: "system_accent3_600",
surface: "system_neutral1_10",
onSurface: "system_neutral1_900",
surfaceVariant: "system_neutral2_100",
onSurfaceVariant: "system_neutral2_700",
outline: "system_neutral2_500",
outlineVariant: "system_neutral2_200",
error: "system_accent3_600",
errorContainer: "system_accent3_100",
onErrorContainer: "system_accent3_900"
};
for (var name in map) {
try {
var id = res.getIdentifier(map[name], "color", "android");
if (id > 0) c[name] = res.getColor(id, null);
} catch (e) {}
}
} catch (e) {}
return c;
},
getColors: function(isDark) {
if (isDark) {
if (!this._cacheDark) this._cacheDark = this._loadMonet(true);
return this._cacheDark;
} else {
if (!this._cacheLight) this._cacheLight = this._loadMonet(false);
return this._cacheLight;
}
},
invalidate: function() {
this._cacheLight = null;
this._cacheDark = null;
}
};
// =======================【兼容兜底themeTextInt/themeBgInt】======================
// 这段代码的主要内容/用途:兼容旧代码或异步回调里误引用 themeTextInt/themeBgInt 导致 ReferenceError 崩溃。
// 说明:当前版本文字色应通过 getPanelTextColorInt(bgInt) 获取;这里仅作为"兜底全局变量",避免回调炸线程。
// 声明全局变量(避免 ReferenceError
var themeBgInt = 0;
var themeTextInt = 0;
// =======================【API 兼容性辅助函数】======================
// 这段代码的主要内容/用途:处理 Android API 级别差异,避免在旧版本上崩溃
/**
* 安全创建 UserHandle兼容 API 17 以下)
* @param {number} userId - 用户 ID
* @returns {android.os.UserHandle} UserHandle 对象或 null
*/
function createUserHandle(userId) {
try {
// UserHandle.of 在 API 17+ 可用
if (android.os.Build.VERSION.SDK_INT >= 17) {
return android.os.UserHandle.of(userId);
}
// API 17 以下返回当前用户句柄
return android.os.Process.myUserHandle();
} catch (e) {
return null;
}
}
/**
* 安全启动 Activity 跨用户(兼容 API 17 以下)
* @param {Context} ctx - Context
* @param {Intent} intent - Intent
* @param {number} userId - 用户 IDAPI 17+ 有效)
*/
function startActivityAsUserSafe(ctx, intent, userId) {
try {
if (android.os.Build.VERSION.SDK_INT >= 17 && userId !== 0) {
var uh = android.os.UserHandle.of(userId);
ctx.startActivityAsUser(intent, uh);
} else {
ctx.startActivity(intent);
}
} catch (e) {
// 降级到普通启动
try {
ctx.startActivity(intent);
} catch (e2) {}
}
}
FloatBallAppWM.prototype.isDarkTheme = function() {
// 0) 优先检查用户强制设置 (0=跟随系统, 1=白天, 2=黑夜)
var mode = 0;
try { mode = Math.floor(Number(this.config.THEME_MODE || 0)); } catch(e){}
if (mode === 2) return true;
if (mode === 1) return false;
// mode === 0 (or others) -> Fallback to system detection
// 这段代码的主要内容/用途:更稳健地判断当前是否处于"夜间/暗色"模式(并打印调试日志)。
// 说明system_server 场景下 Configuration.uiMode 偶发不一致,因此再用 UiModeManager 兜底交叉验证。
var result = false;
var from = "unknown";
try {
// # 1) 优先用 Configuration最快
var uiMode = context.getResources().getConfiguration().uiMode;
var nightMask = (uiMode & android.content.res.Configuration.UI_MODE_NIGHT_MASK);
if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_YES) { result = true; from = "Configuration(UI_MODE_NIGHT_YES)"; }
else if (nightMask === android.content.res.Configuration.UI_MODE_NIGHT_NO) { result = false; from = "Configuration(UI_MODE_NIGHT_NO)"; }
} catch (e1) {}
if (from === "unknown") {
try {
// # 2) 再用 UiModeManager更"系统态"
var um = context.getSystemService(android.content.Context.UI_MODE_SERVICE);
if (um) {
var nm = um.getNightMode();
if (nm === android.app.UiModeManager.MODE_NIGHT_YES) { result = true; from = "UiModeManager(MODE_NIGHT_YES)"; }
else if (nm === android.app.UiModeManager.MODE_NIGHT_NO) { result = false; from = "UiModeManager(MODE_NIGHT_NO)"; }
else { from = "UiModeManager(mode=" + String(nm) + ")"; }
}
} catch (e2) {}
}
// # 3) 实在判断不了,就按"非暗色"处理,避免自动主题背景黑成一片
if (from === "unknown") { result = false; from = "fallback(false)"; }
// 仅在状态改变时打印日志,避免刷屏
var logKey = String(result) + "|" + from + "|" + mode;
if (this._lastDarkThemeLog !== logKey) {
this._lastDarkThemeLog = logKey;
try { _th_log(this.L, "d", "[theme] isDarkTheme=" + String(result) + " via=" + from + " mode=" + mode); } catch (e3) {}
}
// # 主题切换时刷新莫奈配色(传入 result 避免递归)
// 注:构造函数中会初始化,这里只在构造完成后的切换时触发
if (this._lastDarkResult !== undefined && this._lastDarkResult !== result) {
this._lastDarkResult = result;
try { this.refreshMonetColors(result); } catch (eM) {}
} else if (this._lastDarkResult === undefined) {
this._lastDarkResult = result;
}
return result;
};
FloatBallAppWM.prototype.refreshMonetColors = function(forceDark) {
try {
var isDark = (forceDark !== undefined) ? forceDark : this.isDarkTheme();
var m = MonetColorProvider.getColors(isDark);
var ml = MonetColorProvider.getColors(false);
var md = MonetColorProvider.getColors(true);
var c = this.ui.colors;
// 浅色配色
c.bgLight = ml.surface;
c.cardLight = ml.surfaceVariant;
c.textPriLight = ml.onSurface;
c.textSecLight = ml.onSurfaceVariant;
c.dividerLight = ml.outline;
c.inputBgLight = ml.surface;
// 深色配色
c.bgDark = md.surface;
c.cardDark = md.surfaceVariant;
c.textPriDark = md.onSurface;
c.textSecDark = md.onSurfaceVariant;
c.dividerDark = md.outline;
c.inputBgDark = md.surface;
// 当前主题配色(随主题切换)
c.primary = m.primary;
// primaryDark 修正为 onPrimaryContainer日间为深蓝(#041E49),夜间为浅蓝(#D3E3FD),符合"深色变体"语义
c.primaryDark = m.onPrimaryContainer;
c.accent = m.secondary;
c.danger = m.error;
// success/warning 优化对比度:日间更深确保可见,夜间保持适度亮度不刺眼
c.success = isDark ? android.graphics.Color.parseColor("#4ade80") : android.graphics.Color.parseColor("#15803d");
c.warning = isDark ? android.graphics.Color.parseColor("#fbbf24") : android.graphics.Color.parseColor("#b45309");
// 扩展:完整 Monet 语义字段(供面板方法直接使用)
c._monetSurface = m.surface;
c._monetOnSurface = m.onSurface;
c._monetOutline = m.outline;
c._monetOnPrimary = m.onPrimary;
c._monetPrimaryContainer = m.primaryContainer;
c._monetOnPrimaryContainer = m.onPrimaryContainer;
c._monetSecondary = m.secondary;
c._monetTertiary = m.tertiary;
try { _th_log(this.L, "d", "[monet] refreshed isDark=" + isDark + " primary=" + _th_hex(c.primary) + " primaryDark=" + _th_hex(c.primaryDark) + " accent=" + _th_hex(c.accent)); } catch (e) {}
} catch (e) {
try { _th_log(this.L, "e", "[monet] refresh err=" + String(e)); } catch (e2) {}
}
};
FloatBallAppWM.prototype.getMonetAccentForBall = function() {
// 这段代码的主要内容/用途尝试读取系统动态强调色Monet/accent失败则使用兜底颜色并打印命中信息。
// 优化点:
// 1) 日间使用 500/600 档(更深、对比度更高),夜间使用 400/300 档(柔和、不刺眼)
// 2) 移除 system_neutral1_* 中性灰色(不是强调色)
var res = context.getResources();
var dark = this.isDarkTheme();
var names = dark
? ["system_accent1_400", "system_accent1_300", "system_accent2_400"]
: ["system_accent1_500", "system_accent1_600", "system_accent2_500"];
var i;
for (i = 0; i < names.length; i++) {
try {
var id = res.getIdentifier(names[i], "color", "android");
if (id > 0) {
var c = res.getColor(id, null);
var logKey = "hit|" + names[i] + "|" + c;
if (this._lastAccentLog !== logKey) {
this._lastAccentLog = logKey;
try { _th_log(this.L, "d", "[theme] hit accent=" + names[i] + " id=" + String(id) + " c=" + _th_hex(c) + " " + _th_argb(c)); } catch (eL0) {}
}
return c;
}
} catch (e) {
try { _th_log(this.L, "w", "[theme] err accent=" + names[i] + " e=" + String(e)); } catch (eL2) {}
}
}
var fbHex = dark
? (this.config.BALL_FALLBACK_DARK || CONST_BALL_FALLBACK_DARK)
: (this.config.BALL_FALLBACK_LIGHT || CONST_BALL_FALLBACK_LIGHT);
var fb = android.graphics.Color.parseColor(fbHex);
var logKeyFb = "miss_all|" + fb;
if (this._lastAccentLog !== logKeyFb) {
this._lastAccentLog = logKeyFb;
try { _th_log(this.L, "w", "[theme] accent miss all, fallback=" + _th_hex(fb) + " " + _th_argb(fb)); } catch (eL3) {}
}
return fb;
};
FloatBallAppWM.prototype.updateBallContentBackground = function(contentView) {
try {
var ballColor = this.getMonetAccentForBall();
var dark = this.isDarkTheme();
var alpha01 = dark ? this.config.BALL_RIPPLE_ALPHA_DARK : this.config.BALL_RIPPLE_ALPHA_LIGHT;
var rippleColor = this.withAlpha(ballColor, alpha01);
// # 自定义 PNG/APP 模式下:背景透明
var fillColor = ballColor;
var _usedKind = "none";
try { _usedKind = this.state.usedIconKind || "none"; } catch(eK){}
try {
var _pngModeBg = Number(this.config.BALL_PNG_MODE || 0);
if ((_pngModeBg === 1 && _usedKind === "file") || _usedKind === "app") {
fillColor = android.graphics.Color.TRANSPARENT;
}
} catch (eBg) {}
var content = new android.graphics.drawable.GradientDrawable();
content.setShape(android.graphics.drawable.GradientDrawable.OVAL);
content.setColor(fillColor);
// # 描边:根据球体颜色亮度自动选择白/黑描边,确保任何背景下都可见
if (_usedKind !== "file" && _usedKind !== "app") {
try {
var Color = android.graphics.Color;
var lum = (Color.red(fillColor)*0.299 + Color.green(fillColor)*0.587 + Color.blue(fillColor)*0.114) / 255.0;
var strokeInt = lum > 0.55
? Color.parseColor("#33000000") // 浅球用半透明黑边
: Color.parseColor("#55FFFFFF"); // 深球用半透明白边
content.setStroke(this.dp(1), strokeInt);
} catch(eS){}
}
var mask = new android.graphics.drawable.GradientDrawable();
mask.setShape(android.graphics.drawable.GradientDrawable.OVAL);
mask.setColor(android.graphics.Color.WHITE);
contentView.setBackground(new android.graphics.drawable.RippleDrawable(
android.content.res.ColorStateList.valueOf(rippleColor),
content,
mask
));
} catch (e) {}
};
FloatBallAppWM.prototype.safeParseColor = function(hex, fallbackInt) {
// 这段代码的主要内容/用途:安全解析 #RRGGBB/#AARRGGBB解析失败直接回退避免 system_server 抛异常。
try { return android.graphics.Color.parseColor(String(hex)); } catch (e) { return fallbackInt; }
};
FloatBallAppWM.prototype.getPanelBgColorInt = function() {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的背景颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayBgHex = (this.config.THEME_DAY_BG_HEX != null) ? String(this.config.THEME_DAY_BG_HEX) : null;
var nightBgHex = (this.config.THEME_NIGHT_BG_HEX != null) ? String(this.config.THEME_NIGHT_BG_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayBgHex === "#FAF4E3") dayBgHex = null;
if (nightBgHex === "#191928") nightBgHex = null;
// # 未配置时使用莫奈 surface 色作为回退
var dayFallback = this.ui.colors.bgLight || android.graphics.Color.parseColor("#F8F9FA");
var nightFallback = this.ui.colors.bgDark || android.graphics.Color.parseColor("#131314");
var base = isDark
? (nightBgHex ? this.safeParseColor(nightBgHex, nightFallback) : nightFallback)
: (dayBgHex ? this.safeParseColor(dayBgHex, dayFallback) : dayFallback);
// # 继承原配置面板背景透明度0~1
var a = 1.0;
try { a = Number(this.config.PANEL_BG_ALPHA); } catch (e1) { a = 0.85; }
if (!(a >= 0.0 && a <= 1.0)) a = 0.85;
var out = this.withAlpha(base, a);
try { _th_log(this.L, "d", "[t]bg isDark=" + isDark + " o=" + _th_hex(out)); } catch (e2) {}
return out;
};
FloatBallAppWM.prototype.getPanelTextColorInt = function(bgInt) {
// 这段代码的主要内容/用途:配合"白天/夜晚"两档主题,返回统一的文字颜色(不再依赖自动亮度推断)。
var isDark = this.isDarkTheme();
var dayTextHex = (this.config.THEME_DAY_TEXT_HEX != null) ? String(this.config.THEME_DAY_TEXT_HEX) : null;
var nightTextHex = (this.config.THEME_NIGHT_TEXT_HEX != null) ? String(this.config.THEME_NIGHT_TEXT_HEX) : null;
// # 兼容旧版默认配色:若仍为旧默认值,自动回退到莫奈色
if (dayTextHex === "#333333") dayTextHex = null;
if (nightTextHex === "#E6E6F0") nightTextHex = null;
// # 未配置时使用莫奈 onSurface 色作为回退
var dayFallback = this.ui.colors.textPriLight || android.graphics.Color.parseColor("#1F1F1F");
var nightFallback = this.ui.colors.textPriDark || android.graphics.Color.parseColor("#E3E3E3");
if (!isDark) return dayTextHex ? this.safeParseColor(dayTextHex, dayFallback) : dayFallback;
return nightTextHex ? this.safeParseColor(nightTextHex, nightFallback) : nightFallback;
};
FloatBallAppWM.prototype.applyTextColorRecursive = function(v, colorInt) {
// 这段代码的主要内容/用途:递归设置面板内所有 TextView 的文字颜色(标题/按钮文字/描述/设置项等)。
try {
if (!v) return;
if (v instanceof android.widget.TextView) {
v.setTextColor(colorInt);
}
if (v instanceof android.view.ViewGroup) {
var i, n = v.getChildCount();
for (i = 0; i < n; i++) {
this.applyTextColorRecursive(v.getChildAt(i), colorInt);
}
}
} catch (e0) {}
};
FloatBallAppWM.prototype.updatePanelBackground = function(panelView) {
// 这段代码的主要内容/用途:统一为"主面板/设置面板/查看器面板"应用背景与文字颜色(自动/亮/暗三档),并输出调试日志(命中哪个颜色)。
try {
var bg = new android.graphics.drawable.GradientDrawable();
bg.setCornerRadius(this.dp(22));
var bgInt = this.getPanelBgColorInt();
bg.setColor(bgInt);
// 轻量描边:亮色时更明显,暗色时也保留一点边界(不提供自定义输入,避免设置页复杂化)
var sw = this.dp(1);
var isDark = this.isDarkTheme();
var outlineColor = this.ui.colors._monetOutline || (isDark ? android.graphics.Color.parseColor("#8E918F") : android.graphics.Color.parseColor("#747775"));
var stroke = this.withAlpha(outlineColor, isDark ? 0.26 : 0.20);
try { bg.setStroke(sw, stroke); } catch (eS) {}
panelView.setBackground(bg);
var tc = this.getPanelTextColorInt(bgInt);
try { themeBgInt = bgInt; themeTextInt = tc; } catch (eT) {}
this.applyTextColorRecursive(panelView, tc);
try { _th_log(this.L, "d", "[t]apply bg=" + _th_hex(bgInt) + " tx=" + _th_hex(tc)); } catch (e) {}
try {
_th_log(this.L, "d",
"[theme:apply] isDark=" + isDark +
" bg=" + _th_hex(bgInt) + " " + _th_argb(bgInt) +
" text=" + _th_hex(tc) + " " + _th_argb(tc) +
" stroke=" + _th_hex(stroke)
);
} catch (eL0) {}
} catch (e) {
try { _th_log(this.L, "e", "[theme:apply] err=" + String(e)); } catch (eL1) {}
}
};
// =======================【工具:面板位置持久化】======================
FloatBallAppWM.prototype.savePanelState = function(key, state) {
if (!key || !state) return;
try {
if (!this.config.PANEL_STATES) this.config.PANEL_STATES = {};
this.config.PANEL_STATES[key] = state;
// 节流或立即保存? 面板拖动结束通常不频繁,立即保存即可
// 但为了避免连续事件,还是可以复用 savePos 的节流逻辑,或者直接保存
ConfigManager.saveSettings(this.config);
} catch (e) {}
};
FloatBallAppWM.prototype.loadPanelState = function(key) {
if (!key || !this.config.PANEL_STATES) return null;
return this.config.PANEL_STATES[key];
};
// =======================【工具:位置持久化】======================
FloatBallAppWM.prototype.savePos = function(x, y) {
try {
this.config.BALL_INIT_X = Math.floor(x);
this.config.BALL_INIT_Y_DP = Math.floor(y / this.state.density);
// # 节流保存
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
FloatBallAppWM.prototype.loadSavedPos = function() {
// # 直接从 config 返回,因为 config 已经是持久化的
var x = Number(this.config.BALL_INIT_X || 0);
var y = this.dp(Number(this.config.BALL_INIT_Y_DP || 100));
return { x: x, y: y };
};
FloatBallAppWM.prototype.trySavePosThrottled = function(x, y) {
var t = this.now();
if (t - this.state.lastSaveTs < this.config.SAVE_THROTTLE_MS) return false;
this.state.lastSaveTs = t;
return this.savePos(x, y);
};
// =======================【工具:配置持久化】======================
FloatBallAppWM.prototype.saveConfig = function(obj) {
try {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
this.config[k] = obj[k];
}
}
if (this.L) this.L.updateConfig(this.config);
return ConfigManager.saveSettings(this.config);
} catch (e) { return false; }
};
// =======================【设置面板schema】======================
FloatBallAppWM.prototype.getConfigSchema = function() {
return ConfigManager.loadSchema();
};
// =======================【设置面板:临时编辑缓存】======================
FloatBallAppWM.prototype.beginEditConfig = function() {
try {
var schema = this.getConfigSchema();
var p = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
p[k] = this.config[k];
}
this.state.pendingUserCfg = p;
this.state.pendingDirty = false;
return true;
} catch (e0) {
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
return false;
}
};
FloatBallAppWM.prototype.getPendingValue = function(k) {
if (this.state.pendingUserCfg && this.state.pendingUserCfg.hasOwnProperty(k)) return this.state.pendingUserCfg[k];
return this.config[k];
};
FloatBallAppWM.prototype.setPendingValue = function(k, v) {
if (!this.state.pendingUserCfg) this.beginEditConfig();
this.state.pendingUserCfg[k] = v;
this.state.pendingDirty = true;
if (this.state.previewMode) {
this.refreshPreview(k);
}
};
FloatBallAppWM.prototype.getEffectiveConfig = function() {
if (!this.state.previewMode || !this.state.pendingUserCfg) return this.config;
var cfg = {};
for (var k in this.config) { cfg[k] = this.config[k]; }
for (var k in this.state.pendingUserCfg) { cfg[k] = this.state.pendingUserCfg[k]; }
return cfg;
};
FloatBallAppWM.prototype.refreshPreview = function(changedKey) {
if (this.state.closing) return;
var self = this;
// Post to next tick to avoid destroying view during event dispatch (fixes crash on switch toggle)
if (this.state.h) {
this.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() { self._refreshPreviewInternal(changedKey); }
}));
} else {
self._refreshPreviewInternal(changedKey);
}
};
FloatBallAppWM.prototype._refreshPreviewInternal = function(changedKey) {
if (this.state.closing) return;
var originalConfig = this.config;
try {
// 使用临时配置
this.config = this.getEffectiveConfig();
var needBall = false;
var needPanel = false;
if (!changedKey) {
needBall = true;
needPanel = true;
} else {
// 根据修改的 key 判断需要刷新什么,避免全量刷新导致闪烁
if (changedKey.indexOf("BALL_") === 0) needBall = true;
if (changedKey.indexOf("PANEL_") === 0) needPanel = true;
// 球大小改变会影响面板位置
if (changedKey === "BALL_SIZE_DP") needPanel = true;
}
// 1. 刷新悬浮球 (保持面板不关闭)
if (needBall) {
this.rebuildBallForNewSize(true);
}
// 2. 刷新主面板预览
if (needPanel) {
// 如果当前没有显示主面板,则创建并显示;如果已显示,则替换
var panel = this.buildPanelView("main");
// 计算位置 (使用当前球的位置)
var maxH = Math.floor(this.state.screen.h * 0.75);
panel.measure(
android.view.View.MeasureSpec.makeMeasureSpec(this.state.screen.w, android.view.View.MeasureSpec.AT_MOST),
android.view.View.MeasureSpec.makeMeasureSpec(maxH, android.view.View.MeasureSpec.AT_MOST)
);
var pw = panel.getMeasuredWidth();
var ph = panel.getMeasuredHeight();
if (ph > maxH) ph = maxH;
var bx = this.state.ballLp.x;
var by = this.state.ballLp.y;
var px = this.computePanelX(bx, pw);
var py = by;
// 尝试调整 Y
var r = this.tryAdjustPanelY(px, py, pw, ph, bx, by);
var finalX = r.ok ? r.x : px;
var finalY = r.ok ? r.y : this.clamp(py, 0, this.state.screen.h - ph);
// 优化闪烁:先添加新面板,再移除旧面板 (这样新面板会在最上层,符合预览需求)
var oldPanel = this.state.panel;
var oldAdded = this.state.addedPanel;
// 添加新面板 (addPanel 会更新 this.state.panel)
// 注意addPanel 中已为 main 添加 FLAG_NOT_FOCUSABLE所以即使在最上层也不会抢走 Settings 的输入焦点
this.addPanel(panel, finalX, finalY, "main");
// 移除旧面板
if (oldAdded && oldPanel) {
try { this.state.wm.removeView(oldPanel); } catch(e) {}
}
}
} catch(e) {
safeLog(this.L, 'e', "refreshPreview err=" + e);
} finally {
this.config = originalConfig;
}
};
FloatBallAppWM.prototype.persistUserCfgFromObject = function(obj) {
// # 这段代码的主要内容/用途:从临时编辑对象里按 schema 白名单抽取并保存(跳过 section 标题等无 key 项)
try {
var schema = this.getConfigSchema();
var out = {};
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
out[k] = obj[k];
}
return this.saveConfig(out);
} catch (e0) { return false; }
};
FloatBallAppWM.prototype.applyImmediateEffectsForKey = function(k) {
try {
if (k === "LOG_ENABLE") {
try {
if (this.L) {
this.L.enable = !!this.config.LOG_ENABLE;
this.L.cfg.LOG_ENABLE = !!this.config.LOG_ENABLE;
this.L.i("apply LOG_ENABLE=" + String(this.config.LOG_ENABLE));
}
} catch (eLE) {}
return;
}
if (k === "LOG_DEBUG") {
try {
if (this.L) {
this.L.debug = !!this.config.LOG_DEBUG;
this.L.cfg.LOG_DEBUG = !!this.config.LOG_DEBUG;
this.L.i("apply LOG_DEBUG=" + String(this.config.LOG_DEBUG));
}
} catch (eLD) {}
return;
}
if (k === "LOG_KEEP_DAYS") {
try {
var n = Math.max(1, Math.floor(Number(this.config.LOG_KEEP_DAYS || 3)));
this.config.LOG_KEEP_DAYS = n;
if (this.L) {
this.L.keepDays = n;
this.L.cfg.LOG_KEEP_DAYS = n;
this.L.i("apply LOG_KEEP_DAYS=" + String(n));
this.L.cleanupOldFiles();
}
} catch (eLK) {}
return;
}
if (k === "BALL_SIZE_DP" || k === "BALL_PNG_MODE" || k === "BALL_ICON_TYPE" || k === "BALL_ICON_PKG" || k === "BALL_ICON_FILE_PATH" || k === "BALL_ICON_RES_ID" || k === "BALL_ICON_RES_NAME" || k === "BALL_ICON_SIZE_DP" || k === "BALL_ICON_TINT_HEX" || k === "BALL_TEXT" || k === "BALL_TEXT_SIZE_SP" || k === "BALL_TEXT_COLOR_HEX" || k === "BALL_ICON_TEXT_GAP_DP") { this.rebuildBallForNewSize(); return; }
if (k === "PANEL_ROWS" || k === "PANEL_COLS" ||
k === "PANEL_ITEM_SIZE_DP" || k === "PANEL_GAP_DP" ||
k === "PANEL_PADDING_DP" || k === "PANEL_ICON_SIZE_DP" ||
k === "PANEL_LABEL_ENABLED" || k === "PANEL_LABEL_TEXT_SIZE_SP" ||
k === "PANEL_LABEL_TOP_MARGIN_DP") {
if (this.state.addedPanel) this.hideMainPanel();
if (this.state.addedSettings) this.hideSettingsPanel();
if (this.state.addedViewer) this.hideViewerPanel();
return;
}
if (k === "EDGE_VISIBLE_RATIO") {
if (this.state.addedBall && this.state.docked) {
this.state.docked = false;
this.snapToEdgeDocked(false);
}
return;
}
} catch (e0) {}
};
FloatBallAppWM.prototype.commitPendingUserCfg = function() {
try {
if (!this.state.pendingUserCfg) return { ok: false, reason: "no_pending" };
var schema = this.getConfigSchema();
var changedKeys = [];
var i;
for (i = 0; i < schema.length; i++) {
if (!schema[i] || !schema[i].key) continue;
var k = String(schema[i].key);
var oldV = this.config[k];
var newV = this.state.pendingUserCfg[k];
if (String(oldV) !== String(newV)) {
this.config[k] = newV;
changedKeys.push(k);
}
}
this.persistUserCfgFromObject(this.state.pendingUserCfg);
var j;
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] === "BALL_SIZE_DP") { this.applyImmediateEffectsForKey("BALL_SIZE_DP"); break; }
}
for (j = 0; j < changedKeys.length; j++) {
if (changedKeys[j] !== "BALL_SIZE_DP") this.applyImmediateEffectsForKey(changedKeys[j]);
}
this.state.pendingDirty = false;
safeLog(this.L, 'i', "commit settings changed=" + JSON.stringify(changedKeys));
return { ok: true, changed: changedKeys };
} catch (e0) {
safeLog(this.L, 'e', "commitPendingUserCfg err=" + String(e0));
return { ok: false, err: String(e0) };
}
};
// =======================【工具:吸边数据】======================
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) {}
}
// # 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) {}
// # 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) {}
return dr0;
}
}
} catch(eF0) {}
}
} catch(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) {}
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) {}
// # 2) LauncherApps 回退(当 shortcut service 不可用或返回空时)
if (list == null) {
var q = new android.content.pm.LauncherApps.ShortcutQuery();
try { q.setPackage(pkg2); } catch(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) {}
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; } catch(eF2) {}
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST; } catch(eF3) {}
try { qFlags = qFlags | android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED; } catch(eF4) {}
try { q.setQueryFlags(qFlags); } catch(eSF) {}
} catch(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) {}
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) {}
}
if (drSc != null) {
try { this._iconLruPut("sc|" + skey, drSc); } catch (eSc1) {}
return drSc;
}
}
}
}
// # 如果没拿到 shortcut 图标,记录一次失败时间,触发冷却
try {
if (nowTs > 0) this._shortcutIconFailTs[skey] = nowTs;
else this._shortcutIconFailTs[skey] = new Date().getTime();
} catch(eFT) {}
}
// # 回退:取应用图标,至少保证按钮有图标可见
try {
var pm2 = context.getPackageManager();
var drApp2 = pm2.getApplicationIcon(pkg2);
if (drApp2 != null) return drApp2;
} catch(eFall) {}
}
}
} catch (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) {}
// # 3) 兜底
return context.getResources().getDrawable(android.R.drawable.ic_menu_help, null);
} catch (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) {}
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) {}
}
if (ai) {
try { pushPath(ai.sourceDir); } catch (e0) {}
try { pushPath(ai.publicSourceDir); } catch (e1) {}
try {
var ss = ai.splitSourceDirs;
if (ss) {
var i;
for (i = 0; i < ss.length; i++) pushPath(ss[i]);
}
} catch (e2) {}
}
} catch (e) {}
return out;
};
FloatBallAppWM.prototype.scanShortXIconsFromApk = function() {
var out = [];
var seen = {};
var paths = this.getShortXApkPaths();
var regex = /^res\/drawable[^\/]*\/(ic_remix_[a-z0-9_]+|ic_shortx|ic_launcher|ic_menu_preferences)\.(xml|png|webp|jpg|jpeg)$/;
var lastErr = "";
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());
var m = regex.exec(name);
if (!m) continue;
var fullName = String(m[1]);
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) {}
}
}
if ((!out || out.length === 0) && lastErr) this._shortxIconCatalogError = lastErr;
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) {}
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) {}
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) {}
}
return dr;
} catch (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) {
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 !== "ic_shortx" && fname !== "ic_launcher" && fname !== "ic_menu_preferences") continue;
out.push({
name: fname,
shortName: (fname.indexOf("ic_remix_") === 0) ? fname.substring("ic_remix_".length) : fname,
id: f.getInt(null)
});
} catch (eField) {}
}
} catch (eClz) {
this._shortxIconCatalogError = String(eClz);
safeLog(this.L, 'w', "getShortXIconCatalog reflect failed: " + String(eClz));
}
}
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 = "反射与 APK 扫描均未获取到图标";
} else {
this._shortxIconCatalogError = "";
}
} catch (e) {
this._shortxIconCatalogError = String(e);
safeLog(this.L, 'w', "getShortXIconCatalog failed: " + String(e));
}
this._shortxIconCatalog = out;
return out;
};
// =======================【工具:快捷方式选择器(内置,合并 shortcuts.js】======================
// 这段代码的主要内容/用途:在 toolhub.js 内部提供与 shortcuts.js 等价的"快捷方式浏览/选择"页面(仅在点击"选择快捷方式"时触发)。
// 设计要点:
// 1) 双线程流水线UI 线程专管 WM/ViewBG 线程专管 icon 加载,避免卡顿/ANR 风险。
// 2) 稳定性:失败 TTL 熔断 + 限次重试 + icon LRU 上限;滚动触底用轮询,避免 interface listener 的 JavaAdapter 风险。
// 3) UI 风格:复用 ToolHub 主题(白天/夜晚),避免与主面板割裂;支持关闭与安全销毁。
// 4) 日志:复用 ToolHubLoggerself.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;
}
// ==================== 主 UIWM 双线程 + 轮询触底) ====================
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);
};
// =======================【Content解析 settings URI】======================
// 这段代码的主要内容/用途:识别 content://settings/(system|secure|global)/KEY 并用 Settings.* get/put 更稳
FloatBallAppWM.prototype.parseSettingsUri = function(uriStr) {
try {
var s = String(uriStr || "");
if (s.indexOf("content://settings/") !== 0) return null;
// content://settings/system/accelerometer_rotation
var rest = s.substring("content://settings/".length);
var parts = rest.split("/");
if (!parts || parts.length < 1) return null;
var table = String(parts[0] || "");
var key = "";
if (parts.length >= 2) key = String(parts[1] || "");
if (table !== "system" && table !== "secure" && table !== "global") return null;
return { table: table, key: key };
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsGetStringByTable = function(table, key) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.getString(cr, String(key));
if (table === "secure") return android.provider.Settings.Secure.getString(cr, String(key));
if (table === "global") return android.provider.Settings.Global.getString(cr, String(key));
return null;
} catch (e) { return null; }
};
FloatBallAppWM.prototype.settingsPutStringByTable = function(table, key, value) {
try {
var cr = context.getContentResolver();
if (table === "system") return android.provider.Settings.System.putString(cr, String(key), String(value));
if (table === "secure") return android.provider.Settings.Secure.putString(cr, String(key), String(value));
if (table === "global") return android.provider.Settings.Global.putString(cr, String(key), String(value));
return false;
} catch (e) { return false; }
};
// =======================【Content通用 query】======================
// 这段代码的主要内容/用途ContentResolver.query 并把 Cursor 转成文本(用于查看器面板)
FloatBallAppWM.prototype.contentQueryToText = function(uriStr, projection, selection, selectionArgs, sortOrder, maxRows) {
var out = { ok: false, uri: String(uriStr || ""), rows: 0, text: "", err: "" };
var cr = null;
var cur = null;
try {
cr = context.getContentResolver();
var uri = android.net.Uri.parse(String(uriStr));
var projArr = null;
if (projection && projection.length) {
projArr = java.lang.reflect.Array.newInstance(java.lang.String, projection.length);
var i0;
for (i0 = 0; i0 < projection.length; i0++) projArr[i0] = String(projection[i0]);
}
var sel = (selection === undefined || selection === null) ? null : String(selection);
var selArgsArr = null;
if (selectionArgs && selectionArgs.length) {
selArgsArr = java.lang.reflect.Array.newInstance(java.lang.String, selectionArgs.length);
var i1;
for (i1 = 0; i1 < selectionArgs.length; i1++) selArgsArr[i1] = String(selectionArgs[i1]);
}
var so = (sortOrder === undefined || sortOrder === null) ? null : String(sortOrder);
cur = cr.query(uri, projArr, sel, selArgsArr, so);
if (!cur) {
out.err = "query return null cursor";
return out;
}
var colCount = cur.getColumnCount();
var cols = [];
var ci;
for (ci = 0; ci < colCount; ci++) cols.push(String(cur.getColumnName(ci)));
var sb = [];
sb.push("URI: " + String(uriStr));
sb.push("Columns(" + String(colCount) + "): " + cols.join(", "));
sb.push("");
var limit = Math.max(1, Math.floor(Number(maxRows || this.config.CONTENT_MAX_ROWS || 20)));
var row = 0;
while (cur.moveToNext()) {
row++;
sb.push("#" + String(row));
var cj;
for (cj = 0; cj < colCount; cj++) {
var v = "";
try {
if (cur.isNull(cj)) v = "null";
else v = String(cur.getString(cj));
} catch (eV) {
try { v = String(cur.getLong(cj)); } catch (eV2) { v = "<??>"; }
}
sb.push(" " + cols[cj] + " = " + v);
}
sb.push("");
if (row >= limit) break;
}
out.ok = true;
out.rows = row;
out.text = sb.join("\n");
return out;
} catch (e) {
out.err = String(e);
return out;
} finally {
try { if (cur) cur.close(); } catch (eC) {}
}
};
// =======================【Content统一入口】======================
// 这段代码的主要内容/用途:处理按钮里的 type:"content"
FloatBallAppWM.prototype.execContentAction = function(btn) {
var mode = btn.mode ? String(btn.mode) : ((btn.value !== undefined && btn.value !== null) ? "put" : "get");
var uri = btn.uri ? String(btn.uri) : "";
if (!uri) return { ok: false, err: "missing uri" };
// settings uri 优先走 Settings API
var su = this.parseSettingsUri(uri);
if (mode === "get") {
if (su && su.key) {
var v = this.settingsGetStringByTable(su.table, su.key);
return { ok: true, mode: "get", kind: "settings", table: su.table, key: su.key, value: (v === null ? "null" : String(v)) };
}
// 非 settings尝试 query 一行
var q1 = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, 1);
if (!q1.ok) return { ok: false, mode: "get", kind: "query", err: q1.err };
return { ok: true, mode: "get", kind: "query", text: q1.text, rows: q1.rows };
}
if (mode === "put") {
var val = (btn.value === undefined || btn.value === null) ? "" : String(btn.value);
if (su && su.key) {
var ok = this.settingsPutStringByTable(su.table, su.key, val);
return { ok: !!ok, mode: "put", kind: "settings", table: su.table, key: su.key, value: val };
}
// 非 settings尽力走 update(ContentValues)
try {
var cr = context.getContentResolver();
var u = android.net.Uri.parse(uri);
var cv = new android.content.ContentValues();
// 支持 btn.values = {col1: "...", col2: "..."};否则写 value 列
if (btn.values) {
var k;
for (k in btn.values) {
if (!btn.values.hasOwnProperty(k)) continue;
cv.put(String(k), String(btn.values[k]));
}
} else {
cv.put("value", val);
}
var where = (btn.selection === undefined || btn.selection === null) ? null : String(btn.selection);
var whereArgs = null;
if (btn.selectionArgs && btn.selectionArgs.length) {
whereArgs = java.lang.reflect.Array.newInstance(java.lang.String, btn.selectionArgs.length);
var i2;
for (i2 = 0; i2 < btn.selectionArgs.length; i2++) whereArgs[i2] = String(btn.selectionArgs[i2]);
}
var n = cr.update(u, cv, where, whereArgs);
return { ok: true, mode: "put", kind: "update", updated: Number(n) };
} catch (eU) {
return { ok: false, mode: "put", kind: "update", err: String(eU) };
}
}
if (mode === "query") {
var maxRows = (btn.maxRows === undefined || btn.maxRows === null) ? this.config.CONTENT_MAX_ROWS : Number(btn.maxRows);
var q = this.contentQueryToText(uri, btn.projection, btn.selection, btn.selectionArgs, btn.sortOrder, maxRows);
if (!q.ok) return { ok: false, mode: "query", err: q.err };
return { ok: true, mode: "query", rows: q.rows, text: q.text };
}
if (mode === "view") {
try {
var it = new android.content.Intent(android.content.Intent.ACTION_VIEW);
it.setData(android.net.Uri.parse(uri));
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(it);
return { ok: true, mode: "view" };
} catch (eV) {
return { ok: false, mode: "view", err: String(eV) };
}
}
return { ok: false, err: "unknown mode=" + mode };
};
/* =======================
下面开始WM 动画、面板、触摸、启动、输出
======================= */
FloatBallAppWM.prototype.animateBallLayout = function(toX, toY, toW, durMs, endCb) {
var st = this.state;
if (!st.addedBall || !st.ballRoot || !st.ballLp) { if (endCb) endCb(); return; }
var fromX = st.ballLp.x;
var fromY = st.ballLp.y;
var fromW = st.ballLp.width;
try {
var va = android.animation.ValueAnimator.ofFloat(0.0, 1.0);
va.setDuration(durMs);
try {
// 使用 OvershootInterpolator 产生轻微的回弹效果,更加生动
// 0.7 的张力适中,不会过于夸张
va.setInterpolator(new android.view.animation.OvershootInterpolator(0.7));
} catch (eI) {
try { va.setInterpolator(new android.view.animation.DecelerateInterpolator()); } catch (eI2) {}
}
var self = this;
va.addUpdateListener(new android.animation.ValueAnimator.AnimatorUpdateListener({
onAnimationUpdate: function(anim) {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var f = anim.getAnimatedValue();
var nx = Math.round(fromX + (toX - fromX) * f);
var ny = Math.round(fromY + (toY - fromY) * f);
var nw = Math.round(fromW + (toW - fromW) * f);
// 性能优化:只有坐标真正变化时才请求 WindowManager 更新
if (nx !== self.state.ballLp.x || ny !== self.state.ballLp.y || nw !== self.state.ballLp.width) {
self.state.ballLp.x = nx;
self.state.ballLp.y = ny;
self.state.ballLp.width = nw;
// # 关键操作使用 safeOperation 封装
safeOperation("dockAnimation.updateViewLayout", function() {
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
}, true, self.L);
}
} catch (e) {}
}
}));
va.addListener(new android.animation.Animator.AnimatorListener({
onAnimationStart: function() {},
onAnimationRepeat: function() {},
onAnimationCancel: function() {},
onAnimationEnd: function() {
try {
if (!self.state.closing && self.state.addedBall) {
self.state.ballLp.x = toX;
self.state.ballLp.y = toY;
self.state.ballLp.width = toW;
self.state.wm.updateViewLayout(self.state.ballRoot, self.state.ballLp);
self.savePos(self.state.ballLp.x, self.state.ballLp.y);
}
} catch (e2) {}
try { if (endCb) endCb(); } catch (eCb) { try { if (self && self.L && self.L.e) self.L.e("animateBallLayout endCb err=" + String(eCb)); } catch (eLog) {} }
}
}));
va.start();
} catch (e0) {
try {
st.ballLp.x = toX;
st.ballLp.y = toY;
st.ballLp.width = toW;
st.wm.updateViewLayout(st.ballRoot, st.ballLp);
this.savePos(st.ballLp.x, st.ballLp.y);
} catch (e1) {}
try { if (endCb) endCb(); } catch (eCb2) { try { if (this && this.L && this.L.e) this.L.e("animateBallLayout endCb err=" + String(eCb2)); } catch (eLog2) {} }
}
};
FloatBallAppWM.prototype.playBounce = function(v) {
if (!this.config.ENABLE_BOUNCE) return;
if (!this.config.ENABLE_ANIMATIONS) return;
try { v.animate().cancel(); } catch (e0) {}
var self = this;
var i = 0;
function step() {
if (self.state.closing) return;
if (i >= self.config.BOUNCE_TIMES) {
try { v.setScaleX(1); v.setScaleY(1); } catch (e2) {}
return;
}
var amp = (self.config.BOUNCE_MAX_SCALE - 1) * Math.pow(self.config.BOUNCE_DECAY, i);
var s = 1 + amp;
v.animate()
.scaleX(s)
.scaleY(s)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.OvershootInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() {
v.animate()
.scaleX(1)
.scaleY(1)
.setDuration(self.config.BOUNCE_STEP_MS)
.setInterpolator(new android.view.animation.AccelerateDecelerateInterpolator())
.withEndAction(new JavaAdapter(java.lang.Runnable, {
run: function() { i++; step(); }
}))
.start();
}
}))
.start();
}
step();
};
FloatBallAppWM.prototype.safeRemoveView = function(v, whichName) {
try {
if (!v) return { ok: true, skipped: true };
this.state.wm.removeView(v);
return { ok: true };
} catch (e) {
safeLog(this.L, 'w', "removeView fail which=" + String(whichName || "") + " err=" + String(e));
return { ok: false, err: String(e), where: whichName || "" };
}
};
FloatBallAppWM.prototype.hideMask = function() {
if (!this.state.addedMask) return;
if (!this.state.mask) return;
this.safeRemoveView(this.state.mask, "mask");
this.state.mask = null;
this.state.maskLp = null;
this.state.addedMask = false;
};
FloatBallAppWM.prototype.hideMainPanel = function() {
if (!this.state.addedPanel) return;
if (!this.state.panel) return;
this.safeRemoveView(this.state.panel, "panel");
this.state.panel = null;
this.state.panelLp = null;
this.state.addedPanel = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideMainPanel");
};
FloatBallAppWM.prototype.hideSettingsPanel = function() {
if (!this.state.addedSettings) return;
if (!this.state.settingsPanel) return;
this.safeRemoveView(this.state.settingsPanel, "settingsPanel");
this.state.settingsPanel = null;
this.state.settingsPanelLp = null;
this.state.addedSettings = false;
this.state.pendingUserCfg = null;
this.state.pendingDirty = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideSettingsPanel");
};
FloatBallAppWM.prototype.hideViewerPanel = function() {
if (!this.state.addedViewer) return;
if (!this.state.viewerPanel) return;
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.addedViewer = false;
this.hideMask();
this.touchActivity();
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
};
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
// 说明:仅清理缓存引用,不强行 recycle Bitmap避免误伤仍被使用的 Drawable。
// # 防抖5秒内相同 reason 不重复清理
var now = Date.now();
var cacheKey = "_lastClear_" + (reason || "default");
var lastClear = this.state[cacheKey] || 0;
if (now - lastClear < 5000) {
return; // 5秒内已清理过跳过
}
this.state[cacheKey] = now;
try { this._iconLru = null; } catch (eLruClr) {}
try { this._shortcutIconFailTs = {}; } catch (e2) {}
// # Shortcuts 相关全局缓存(按钮编辑页/快捷方式选择器可能会创建)
try { if (typeof __scIconCache !== "undefined") __scIconCache = {}; } catch (e3) {}
try { if (typeof __scAppLabelCache !== "undefined") __scAppLabelCache = {}; } catch (e4) {}
// # 记录一次清理日志(精简:只记录关键 reason且 5秒防抖
var keyReasons = ["memory_pressure", "screen_changed", "close"];
var isKeyReason = keyReasons.indexOf(reason) >= 0;
try {
if (isKeyReason && this.L && this.L.i) {
this.L.i("clearHeavyCaches reason=" + String(reason));
}
} catch (e5) {}
};
FloatBallAppWM.prototype._clearHeavyCachesIfAllHidden = function(reason) {
// 这段代码的主要内容/用途:只在"主面板/设置/查看器"全部关闭后清理缓存,避免页面切换时反复重建导致卡顿。
try {
if (!this.state.addedPanel && !this.state.addedSettings && !this.state.addedViewer) {
this.clearHeavyCaches(reason || "all_hidden");
}
} catch (e) {}
};
FloatBallAppWM.prototype.hideAllPanels = function() {
this.hideMainPanel();
this.hideSettingsPanel();
this.hideViewerPanel();
this.hideMask();
this._clearHeavyCachesIfAllHidden("hideAllPanels");
};
FloatBallAppWM.prototype.showMask = function() {
if (this.state.addedMask) return;
if (this.state.closing) return;
var self = this;
var mask = new android.widget.FrameLayout(context);
// 遮罩层背景:轻微的黑色半透明,提升层次感
try { mask.setBackgroundColor(android.graphics.Color.parseColor("#33000000")); } catch (e0) {
mask.setBackgroundColor(0x33000000);
}
mask.setOnTouchListener(new JavaAdapter(android.view.View.OnTouchListener, {
onTouch: function(v, e) {
var a = e.getAction();
if (a === android.view.MotionEvent.ACTION_DOWN) {
self.touchActivity();
self.hideAllPanels();
return true;
}
return true;
}
}));
var lp = new android.view.WindowManager.LayoutParams(
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.MATCH_PARENT,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = 0;
lp.y = 0;
try {
this.state.wm.addView(mask, lp);
this.state.mask = mask;
this.state.maskLp = lp;
this.state.addedMask = true;
// 简单的淡入动画
try {
if (this.config.ENABLE_ANIMATIONS) {
mask.setAlpha(0);
mask.animate().alpha(1).setDuration(200).start();
} else {
mask.setAlpha(1);
}
} catch(eAnim){}
} catch (e1) {
safeLog(this.L, 'e', "add mask fail err=" + String(e1));
this.state.addedMask = false;
}
};
FloatBallAppWM.prototype.snapToEdgeDocked = function(withAnim, forceSide) {
if (this.state.closing) return;
// 移除对面板/Mask的检查允许在任何情况下强制吸边如果调用方逻辑正确
// 如果需要保护,调用方自己判断
if (this.state.dragging) return;
var di = this.getDockInfo();
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var snapLeft;
if (forceSide === "left") snapLeft = true;
else if (forceSide === "right") snapLeft = false;
else {
// 默认根据中心点判断
var centerX = this.state.ballLp.x + Math.round(ballSize / 2);
snapLeft = centerX < Math.round(this.state.screen.w / 2);
}
var targetW = visible;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
if (snapLeft) {
this.state.dockSide = "left";
this.state.docked = true;
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
if (withAnim) {
this.animateBallLayout(0, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
safeLog(this.L, 'd', "dock left x=0 y=" + String(targetY) + " w=" + String(targetW));
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) {}
return;
}
this.state.dockSide = "right";
this.state.docked = true;
try { this.state.ballContent.setX(0); } catch (eR) {}
var x2 = this.state.screen.w - visible;
if (withAnim) {
this.animateBallLayout(x2, targetY, targetW, this.config.DOCK_ANIM_MS, null);
} else {
this.state.ballLp.x = x2;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
}
// # 日志精简dock 事件添加防抖10秒内不重复记录相同边
var dockNow = Date.now();
var lastDock = this.state._lastDockLog || 0;
if (dockNow - lastDock > 10000) {
safeLog(this.L, 'i', "dock right x=" + String(x2) + " y=" + String(targetY));
this.state._lastDockLog = dockNow;
}
// 闲置变暗
try {
if (this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(this.config.BALL_IDLE_ALPHA).setDuration(300).start();
} else {
this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA);
}
} catch(eA) {}
};
FloatBallAppWM.prototype.undockToFull = function(withAnim, endCb) {
if (this.state.closing) { if (endCb) endCb(); return; }
if (!this.state.docked) { if (endCb) endCb(); return; }
if (!this.state.addedBall) { if (endCb) endCb(); return; }
var di = this.getDockInfo();
var ballSize = di.ballSize;
var targetW = ballSize;
var targetY = this.clamp(this.state.ballLp.y, 0, this.state.screen.h - ballSize);
try { this.state.ballContent.setX(0); } catch (e0) {}
if (this.state.dockSide === "left") {
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(0, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = 0;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU1) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) {}
safeLog(this.L, 'i', "undock from left");
return;
}
var x = this.state.screen.w - ballSize;
this.state.docked = false;
this.state.dockSide = null;
if (withAnim) this.animateBallLayout(x, targetY, targetW, this.config.UNDOCK_ANIM_MS, endCb);
else {
this.state.ballLp.x = x;
this.state.ballLp.y = targetY;
this.state.ballLp.width = targetW;
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU2) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
if (endCb) endCb();
}
// 恢复不透明度
try {
if (withAnim && this.config.ENABLE_ANIMATIONS) {
this.state.ballContent.animate().alpha(1.0).setDuration(150).start();
} else {
this.state.ballContent.setAlpha(1.0);
}
} catch(eA) {}
// # 日志精简undock 事件改为 INFO 级别,且记录方向
var undockSide = this.state.dockSide || "right";
safeLog(this.L, 'i', "undock from " + undockSide);
};
FloatBallAppWM.prototype.cancelDockTimer = function() {
try { if (this.state.idleDockRunnable && this.state.h) this.state.h.removeCallbacks(this.state.idleDockRunnable); } catch (e) {}
this.state.idleDockRunnable = null;
};
FloatBallAppWM.prototype.armDockTimer = function() {
if (this.state.closing) return;
if (!this.state.h) return;
if (!this.state.addedBall) return;
if (this.state.docked) return;
this.cancelDockTimer();
var hasPanel = (this.state.addedPanel || this.state.addedSettings || this.state.addedViewer || this.state.addedMask);
var targetMs = hasPanel ? this.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : this.config.DOCK_AFTER_IDLE_MS;
var self = this;
this.state.idleDockRunnable = new java.lang.Runnable({
run: function() {
try {
if (self.state.closing) return;
if (self.state.docked) return;
if (self.state.dragging) return;
var hasPanel2 = (self.state.addedPanel || self.state.addedSettings || self.state.addedViewer || self.state.addedMask);
var needMs = hasPanel2 ? self.config.PANEL_IDLE_CLOSE_AND_DOCK_MS : self.config.DOCK_AFTER_IDLE_MS;
var idle = self.now() - self.state.lastMotionTs;
if (idle < needMs) { self.armDockTimer(); return; }
// if (hasPanel2) self.hideAllPanels(); // 用户要求不再自动关闭面板
if (self.config.ENABLE_SNAP_TO_EDGE) {
self.snapToEdgeDocked(true);
}
} catch (e0) {
if (self.L) self.L.e("dockTimer run err=" + String(e0));
}
}
});
this.state.h.postDelayed(this.state.idleDockRunnable, targetMs);
};
FloatBallAppWM.prototype.touchActivity = function() {
this.state.lastMotionTs = this.now();
this.armDockTimer();
}
// # 点击防抖与安全执行
// 这段代码的主要内容/用途:防止在悬浮面板上快速/乱点导致重复 add/remove、状态机被打穿从而引发 system_server 异常重启。
FloatBallAppWM.prototype.guardClick = function(key, cooldownMs, fn) {
try {
var now = android.os.SystemClock.uptimeMillis();
if (!this.state._clickGuards) this.state._clickGuards = {};
var last = this.state._clickGuards[key] || 0;
var cd = (cooldownMs != null ? cooldownMs : INTERACTION_CONSTANTS.CLICK_COOLDOWN_MS);
if (now - last < cd) return false;
this.state._clickGuards[key] = now;
try {
fn && fn();
} catch (e1) {
safeLog(this.L, 'e', "guardClick err key=" + String(key) + " err=" + String(e1));
}
return true;
} catch (e0) {
// 兜底:绝不让点击回调异常冒泡到 system_server
try { fn && fn(); } catch (e2) {}
return true;
}
};
FloatBallAppWM.prototype.safeUiCall = function(tag, fn) {
try {
fn && fn();
} catch (e) {
safeLog(this.L, 'e', "safeUiCall err tag=" + String(tag || "") + " err=" + String(e));
}
};
;
FloatBallAppWM.prototype.onScreenChangedReflow = function() {
if (this.state.closing) return;
if (!this.state.addedBall) return;
var di = this.getDockInfo();
var oldW = this.state.screen.w;
var oldH = this.state.screen.h;
var newScreen = this.getScreenSizePx();
var newW = newScreen.w;
var newH = newScreen.h;
if (newW <= 0 || newH <= 0) return;
if (oldW <= 0) oldW = newW;
if (oldH <= 0) oldH = newH;
this.state.screen = { w: newW, h: newH };
var ballSize = di.ballSize;
var visible = di.visiblePx;
var hidden = di.hiddenPx;
var oldMaxX = Math.max(1, oldW - ballSize);
var oldMaxY = Math.max(1, oldH - ballSize);
var newMaxX = Math.max(1, newW - ballSize);
var newMaxY = Math.max(1, newH - ballSize);
var xRatio = this.state.ballLp.x / oldMaxX;
var yRatio = this.state.ballLp.y / oldMaxY;
var mappedX = Math.round(xRatio * newMaxX);
var mappedY = Math.round(yRatio * newMaxY);
mappedX = this.clamp(mappedX, 0, newMaxX);
mappedY = this.clamp(mappedY, 0, newMaxY);
if (this.state.docked) {
this.state.ballLp.y = mappedY;
this.state.ballLp.width = visible;
if (this.state.dockSide === "left") {
this.state.ballLp.x = 0;
try { this.state.ballContent.setX(-hidden); } catch (eL) {}
} else {
this.state.ballLp.x = newW - visible;
try { this.state.ballContent.setX(0); } catch (eR) {}
}
// 重新进入闲置变暗逻辑(如果需要)
try { this.state.ballContent.setAlpha(this.config.BALL_IDLE_ALPHA); } catch(eA) {}
} else {
this.state.ballLp.x = mappedX;
this.state.ballLp.y = mappedY;
this.state.ballLp.width = ballSize;
try { this.state.ballContent.setX(0); } catch (e0) {}
try { this.state.ballContent.setAlpha(1.0); } catch(eA) {}
}
try { this.state.wm.updateViewLayout(this.state.ballRoot, this.state.ballLp); } catch (eU) {}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
safeLog(this.L, 'i', "screen reflow w=" + String(newW) + " h=" + String(newH) + " x=" + String(this.state.ballLp.x) + " y=" + String(this.state.ballLp.y));
};
FloatBallAppWM.prototype.setupDisplayMonitor = function() {
if (this.state.closing) return;
try {
var dm = context.getSystemService(android.content.Context.DISPLAY_SERVICE);
if (!dm) return;
this.state.dm = dm;
this.state.lastRotation = this.getRotation();
var self = this;
var listener = new JavaAdapter(android.hardware.display.DisplayManager.DisplayListener, {
onDisplayAdded: function(displayId) {},
onDisplayRemoved: function(displayId) {},
onDisplayChanged: function(displayId) {
try {
if (self.state.closing) return;
var nowTs = self.now();
if (nowTs - self.state.lastMonitorTs < self.config.SCREEN_MONITOR_THROTTLE_MS) return;
self.state.lastMonitorTs = nowTs;
self.state.h.post(new JavaAdapter(java.lang.Runnable, {
run: function() {
try {
if (self.state.closing) return;
if (!self.state.addedBall) return;
var rot = self.getRotation();
var sz = self.getScreenSizePx();
var changed = false;
if (rot !== self.state.lastRotation) { self.state.lastRotation = rot; changed = true; }
if (sz.w !== self.state.screen.w || sz.h !== self.state.screen.h) changed = true;
if (changed) {
self.cancelDockTimer();
self.onScreenChangedReflow();
self.touchActivity();
}
} catch (e1) {
if (self.L) self.L.e("displayChanged run err=" + String(e1));
}
}
}));
} catch (e0) {}
}
});
this.state.displayListener = listener;
dm.registerDisplayListener(listener, this.state.h);
safeLog(this.L, 'i', "display monitor registered");
} catch (e2) {
safeLog(this.L, 'e', "setupDisplayMonitor err=" + String(e2));
}
};
FloatBallAppWM.prototype.stopDisplayMonitor = function() {
try { if (this.state.dm && this.state.displayListener) this.state.dm.unregisterDisplayListener(this.state.displayListener); } catch (e) {}
this.state.displayListener = null;
this.state.dm = null;
};
// =======================【Shell智能执行Action优先 + 广播桥兜底)】======================
// 这段代码的主要内容/用途:执行 Shell 按钮动作时,优先尝试 ShortX 的 ShellCommand Action如可用失败则走自定义广播桥由外部接收器实际执行
// 注意system_server 进程本身不直接执行 shell这里只负责"触发执行"。
// 这段代码的主要内容/用途:通过广播桥触发 Shell 执行(仅广播桥,不再使用 ShellCommand Action
// 注意system_server 进程本身不直接执行 shell外部接收器负责实际执行。
FloatBallAppWM.prototype.execShellSmart = function(cmdB64, needRoot) {
var ret = { ok: false, via: "", err: "" };
try {
var action = String(this.config.SHELL_BRIDGE_ACTION || CONST_SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
var it = new android.content.Intent(action);
// # 固定广播协议cmd_b64 + root + from不再发送明文 cmd避免协议漂移
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_CMD, String(cmdB64));
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_ROOT, !!needRoot);
// # from来源标记仅用于接收端识别/日志,不参与权限判断
it.putExtra(CONST_SHELL_BRIDGE_EXTRA_FROM, "ToolHub");
context.sendBroadcast(it);
ret.ok = true;
ret.via = "BroadcastBridge";
safeLog(this.L, 'i', "shell via broadcast ok action=" + action + " root=" + String(!!needRoot));
} catch (eB) {
ret.err = "Broadcast err=" + String(eB);
safeLog(this.L, 'e', "shell via broadcast fail err=" + String(eB));
}
return ret;
};
// =======================【WM 线程:按钮动作执行】======================
FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
// # 点击防抖
// 这段代码的主要内容/用途:防止在按钮面板上连续/乱点导致重复执行与 UI 状态机冲突(可能触发 system_server 异常重启)。
if (!this.guardClick("btn_exec_" + String(idx), 380, null)) return;
try {
if (!btn || !btn.type) {
this.toast("按钮#" + idx + " 未配置");
safeLog(this.L, 'w', "btn#" + String(idx) + " no type");
return;
}
var t = String(btn.type);
safeLog(this.L, 'i', "btn click idx=" + String(idx) + " type=" + t + " title=" + String(btn.title || ""));
if (t === "open_settings") {
this.showPanelAvoidBall("settings");
return;
}
if (t === "open_viewer") {
var logPath = (this.L && this.L._filePathForToday) ? this.L._filePathForToday() : "";
if (!logPath) logPath = PATH_LOG_DIR + "/ShortX_ToolHub_" + (new java.text.SimpleDateFormat("yyyyMMdd").format(new java.util.Date())) + ".log";
var content = FileIO.readText(logPath);
if (!content) content = "(日志文件不存在或为空: " + logPath + ")";
if (content.length > 30000) {
content = "[...前略...]\n" + content.substring(content.length - 30000);
}
// 简单的按行倒序,方便查看最新日志
try {
var lines = content.split("\n");
if (lines.length > 1) {
content = lines.reverse().join("\n");
}
} catch(eRev) {}
this.showViewerPanel("今日日志 (倒序)", content);
return;
}
if (t === "toast") {
var msg = "";
if (btn.text !== undefined && btn.text !== null) msg = String(btn.text);
else if (btn.title) msg = String(btn.title);
else msg = "按钮#" + idx;
this.toast(msg);
return;
}
if (t === "app") {
var pkg = btn.pkg ? String(btn.pkg) : "";
if (!pkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
var it = context.getPackageManager().getLaunchIntentForPackage(pkg);
if (!it) { this.toast("无法启动 " + pkg); return; }
it.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
// # 系统级跨用户启动Context.startActivityAsUser
// 这段代码的主要内容/用途:支持"主应用/分身应用"选择,避免弹出选择器或误启动到另一用户。
// 说明:当未配置 launchUserId 时,默认使用 0主用户失败则回退 startActivity。
var launchUid = 0;
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) launchUid = parseInt(String(btn.launchUserId), 10);
} catch(eLU0) { launchUid = 0; }
if (isNaN(launchUid)) launchUid = 0;
try {
// 运行日志:记录跨用户启动参数(便于定位分身启动失败原因)
safeLog(this.L, 'i', "startAsUser(app) idx=" + idx + " pkg=" + pkg + " launchUserId=" + launchUid);
if (launchUid !== 0) {
context.startActivityAsUser(it, android.os.UserHandle.of(launchUid));
} else {
context.startActivity(it);
}
} catch (eA) {
// # 兜底:某些 ROM/权限限制下 startActivityAsUser 可能抛异常,回退普通启动
try { context.startActivity(it); } catch (eA2) {}
this.toast("启动失败");
safeLog(this.L, 'e', "start app fail pkg=" + pkg + " uid=" + String(launchUid) + " err=" + String(eA));
}
return;
}
if (t === "shell") {
// # 这段代码的主要内容/用途:执行 shell支持 cmd 明文 与 cmd_b64最终会确保发送/执行的是"真正的 base64"
// # 修复点:历史配置里有些按钮把"明文命令"误存进 cmd_b64或 b64 被破坏),会导致广播接收端解码失败→看起来"没效果"。
var cmdB64 = (btn.cmd_b64 !== undefined && btn.cmd_b64 !== null) ? String(btn.cmd_b64) : "";
var cmdPlain = (btn.cmd !== undefined && btn.cmd !== null) ? String(btn.cmd) : "";
// # 1) 只有明文但没有 b64自动补齐 b64避免特殊字符在多层字符串传递中被破坏
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) cmdB64 = String(b64x);
} catch (eB64a) {}
}
// # 2) cmd_b64 非空但无法解码:把它当作"明文命令"重新编码(保证广播桥/Action 都能吃到正确命令)
// # 说明decodeBase64Utf8 返回空串通常意味着 b64 非法或被破坏;而真实命令不太可能是空串。
if (cmdB64 && cmdB64.length > 0) {
try {
var testPlain = decodeBase64Utf8(cmdB64);
if ((!testPlain || testPlain.length === 0) && (!cmdPlain || cmdPlain.length === 0)) {
cmdPlain = String(cmdB64);
cmdB64 = "";
}
} catch (eB64b) {}
}
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64y = encodeBase64Utf8(cmdPlain);
if (b64y && b64y.length > 0) cmdB64 = String(b64y);
} catch (eB64c) {}
}
if (!cmdB64 || cmdB64.length === 0) {
this.toast("按钮#" + idx + " 缺少 cmd/cmd_b64");
safeLog(this.L, 'e', "shell missing cmd idx=" + String(idx));
return;
}
// # 广播桥接收端默认以 root 执行,强制使用 root
var needRoot = true;
var r = this.execShellSmart(cmdB64, needRoot);
if (r && r.ok) return;
this.toast("shell 失败Action + 广播桥均失败)");
safeLog(this.L, 'e', "shell all failed cmd_b64=" + cmdB64 + " ret=" + JSON.stringify(r || {}));
return;
}
if (t === "broadcast") {
// 这段代码的主要内容/用途:发送自定义广播(兼容 btn.extra / btn.extras并对 Shell 广播桥shortx.toolhub.SHELL做额外兼容cmd/cmd_b64/root
var action = btn.action ? String(btn.action) : "";
if (!action) { this.toast("按钮#" + idx + " 缺少 action"); return; }
var it2 = new android.content.Intent(action);
// # 1) 兼容字段extra / extras两种都认
var ex = null;
try {
if (btn.extras) ex = btn.extras;
else if (btn.extra) ex = btn.extra;
} catch (eEx0) { ex = null; }
// # 2) 写入 extras支持 number / boolean / string其他类型一律转字符串
if (ex) {
try {
var k;
for (k in ex) {
if (!ex.hasOwnProperty(k)) continue;
var v = ex[k];
if (typeof v === "number") it2.putExtra(String(k), Number(v));
else if (typeof v === "boolean") it2.putExtra(String(k), !!v);
else it2.putExtra(String(k), String(v));
}
} catch (eE) {}
}
// # 3) 对"Shell 广播桥"做额外兼容:
// - 你可以在 cfg 里写 extra.cmd明文或 extra.cmd_b64Base64
// - 同时会补齐 root/from并且把 cmd 明文也塞一份,方便外部 MVEL 直接读取 cmd 进行验证
try {
var bridgeAction = String(this.config.SHELL_BRIDGE_ACTION || "shortx.toolhub.SHELL");
if (action === bridgeAction) {
var kCmdB64 = String(this.config.SHELL_BRIDGE_EXTRA_CMD || "cmd_b64");
var kFrom = String(this.config.SHELL_BRIDGE_EXTRA_FROM || "from");
var kRoot = String(this.config.SHELL_BRIDGE_EXTRA_ROOT || "root");
var cmdPlain = "";
var cmdB64 = "";
try { cmdB64 = String(it2.getStringExtra(kCmdB64) || ""); } catch (eC0) { cmdB64 = ""; }
try { cmdPlain = String(it2.getStringExtra("cmd") || ""); } catch (eC1) { cmdPlain = ""; }
// # 有明文但没 b64自动补 b64
if ((!cmdB64 || cmdB64.length === 0) && cmdPlain && cmdPlain.length > 0) {
try {
var b64x = encodeBase64Utf8(cmdPlain);
if (b64x && b64x.length > 0) {
cmdB64 = b64x;
it2.putExtra(kCmdB64, String(cmdB64));
}
} catch (eC2) {}
}
// # 有 b64 但没明文:也补一份明文(便于外部规则验证;真正执行仍建议用 cmd_b64
if ((!cmdPlain || cmdPlain.length === 0) && cmdB64 && cmdB64.length > 0) {
try {
var decoded = decodeBase64Utf8(cmdB64);
if (decoded && decoded.length > 0) {
cmdPlain = decoded;
it2.putExtra("cmd", String(cmdPlain));
}
} catch (eC3) {}
}
// # root广播桥接收端默认以 root 执行,强制传递 true
try {
if (!it2.hasExtra(kRoot)) {
it2.putExtra(kRoot, true);
}
} catch (eR0) {
try {
it2.putExtra(kRoot, true);
} catch (eR1) {}
}
// # root 类型纠正:如果外部 cfg 用了字符串 "true"/"false",这里纠正为 boolean避免外部 getBooleanExtra 读不到
try {
if (it2.hasExtra(kRoot)) {
var bdl = it2.getExtras();
if (bdl) {
var raw = bdl.get(kRoot);
if (raw != null) {
var rawStr = String(raw);
if (rawStr === "true" || rawStr === "false") {
it2.removeExtra(kRoot);
it2.putExtra(kRoot, rawStr === "true");
}
}
}
}
} catch (eRB) {}
// # from标识来源便于外部执行器做白名单/审计)
try {
if (!it2.hasExtra(kFrom)) it2.putExtra(kFrom, "ToolHub@system_server");
} catch (eF0) { try { it2.putExtra(kFrom, "ToolHub@system_server"); } catch (eF1) {} }
if (this.L) {
try {
this.L.i("broadcast(shell_bridge) action=" + action + " cmd_len=" + String(cmdPlain ? cmdPlain.length : 0) +
" cmd_b64_len=" + String(cmdB64 ? cmdB64.length : 0) + " root=" + String(it2.getBooleanExtra(kRoot, false)));
} catch (eLg) {}
}
}
} catch (eSB) {}
try { context.sendBroadcast(it2); } catch (eB) { this.toast("广播失败"); safeLog(this.L, 'e', "broadcast fail action=" + action + " err=" + String(eB)); }
return;
}
if (t === "shortcut") {
// 这段代码的主要内容/用途:仅使用 JavaScript(startActivityAsUser) 执行快捷方式,取消 Shell 与所有兜底,避免弹出主/分身选择器。
// 说明:
// 1) 运行时只执行按钮字段 shortcutJsCode由"选择快捷方式列表"点选自动生成,可手动微调)
// 2) 不再调用 am start不再回退 LauncherApps.startShortcut用户要求取消 shell、取消兜底
// 3) 目标 userIdlaunchUserId > userId用于锁定主/分身)
var spkg = btn.pkg ? String(btn.pkg) : "";
var sid = btn.shortcutId ? String(btn.shortcutId) : "";
var iu = (btn.intentUri != null) ? String(btn.intentUri) : "";
var uid = 0;
try { uid = (btn.userId != null) ? parseInt(String(btn.userId), 10) : 0; } catch(eUid0) { uid = 0; }
if (isNaN(uid)) uid = 0;
// # 启动 userId 优先级launchUserId > userId
try {
if (btn.launchUserId != null && String(btn.launchUserId).length > 0) {
var lu0 = parseInt(String(btn.launchUserId), 10);
if (!isNaN(lu0)) uid = lu0;
}
} catch(eLu0) {}
if (!spkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }
if (!sid) { this.toast("按钮#" + idx + " 缺少 shortcutId"); return; }
// # JavaScript 执行:只执行 shortcutJsCode
var jsCode = (btn.shortcutJsCode != null) ? String(btn.shortcutJsCode) : "";
if (!jsCode || jsCode.length === 0) {
this.toast("按钮#" + idx + " 未配置 JS 启动代码");
return;
}
try {
// # 提供少量上下文变量给脚本使用(可选)
// - __sc_intentUri: 当前按钮 intentUri
// - __sc_userId: 当前目标 userId已合并 launchUserId
var __sc_intentUri = iu;
var __sc_userId = uid;
var rjs = eval(jsCode);
// # 约定:返回值以 ok 开头视为成功;以 err 开头视为失败(失败也不兜底)
var sret = (rjs == null) ? "" : String(rjs);
if (sret.indexOf("ok") === 0) {
safeLog(this.L, 'i', "shortcut(js-only) ok pkg=" + spkg + " id=" + sid + " user=" + String(uid));
return;
}
safeLog(this.L, 'e', "shortcut(js-only) fail pkg=" + spkg + " id=" + sid + " user=" + String(uid) + " ret=" + sret);
this.toast("快捷方式 JS 启动失败: " + sret);
return;
} catch (eJsSc) {
safeLog(this.L, 'e', "shortcut(js-only) exception pkg=" + spkg + " id=" + sid + " err=" + eJsSc);
this.toast("快捷方式 JS 异常: " + String(eJsSc));
return;
}
}
this.toast("未知 type=" + t);
safeLog(this.L, 'w', "unknown btn type=" + t);
} catch (eBtn) {
try { this.toast("按钮执行异常"); } catch (e0) {}
safeLog(this.L, 'e', "execButtonAction crash idx=" + String(idx) + " err=" + String(eBtn));
}
};
// =======================【新增:改大小后安全重建悬浮球】======================
FloatBallAppWM.prototype.rebuildBallForNewSize = function(keepPanels) {
if (this.state.closing) return false;
if (!this.state.wm) return false;
if (!this.state.addedBall) return false;
if (!this.state.ballRoot) return false;
if (!this.state.ballLp) return false;
if (this.state.dragging) return false;
var oldSize = this.state.ballLp.height;
if (!oldSize || oldSize <= 0) oldSize = this.getDockInfo().ballSize;
var oldX = this.state.ballLp.x;
var oldY = this.state.ballLp.y;
var oldCenterX = oldX + Math.round(oldSize / 2);
var oldCenterY = oldY + Math.round(oldSize / 2);
if (!keepPanels) {
this.hideAllPanels();
}
this.cancelDockTimer();
this.state.docked = false;
this.state.dockSide = null;
this.safeRemoveView(this.state.ballRoot, "ballRoot-rebuild");
this.state.ballRoot = null;
this.state.ballContent = null;
this.state.ballLp = null;
this.state.addedBall = false;
this.createBallViews();
var di = this.getDockInfo();
var newSize = di.ballSize;
var newX = oldCenterX - Math.round(newSize / 2);
var newY = oldCenterY - Math.round(newSize / 2);
var maxX = Math.max(0, this.state.screen.w - newSize);
var maxY = Math.max(0, this.state.screen.h - newSize);
newX = this.clamp(newX, 0, maxX);
newY = this.clamp(newY, 0, maxY);
var lp = new android.view.WindowManager.LayoutParams(
newSize,
newSize,
android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
android.graphics.PixelFormat.TRANSLUCENT
);
lp.gravity = android.view.Gravity.TOP | android.view.Gravity.START;
lp.x = newX;
lp.y = newY;
try {
this.state.wm.addView(this.state.ballRoot, lp);
this.state.ballLp = lp;
this.state.addedBall = true;
} catch (eAdd) {
try { this.toast("重建悬浮球失败: " + String(eAdd)); } catch (eT) {}
safeLog(this.L, 'e', "rebuildBall add fail err=" + String(eAdd));
return false;
}
this.savePos(this.state.ballLp.x, this.state.ballLp.y);
this.touchActivity();
safeLog(this.L, 'i', "rebuildBall ok size=" + String(newSize) + " x=" + String(newX) + " y=" + String(newY));
return true;
};
// =======================【设置面板UI右上角确认】======================
FloatBallAppWM.prototype.createSectionHeader = function(item, parent) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var color = C.primary;
var h = new android.widget.TextView(context);
h.setText(String(item.name || ""));
h.setTextColor(color);
h.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 15);
h.setTypeface(null, android.graphics.Typeface.BOLD);
h.setPadding(this.dp(16), this.dp(24), this.dp(16), this.dp(8));
parent.addView(h);
};
FloatBallAppWM.prototype.createSettingItemView = function(item, parent, needDivider) {
var isDark = this.isDarkTheme();
var C = this.ui.colors;
var textColor = isDark ? C.textPriDark : C.textPriLight;
var secColor = isDark ? C.textSecDark : C.textSecLight;
var dividerColor = isDark ? C.dividerDark : C.dividerLight;
var primary = C.primary;
var switchOff = isDark ? (0xFF555555 | 0) : (0xFFCCCCCC | 0);
// 增加内边距
var padH = this.dp(16);
var padV = this.dp(16);
// 分割线 (顶部)
if (needDivider) {
var line = new android.view.View(context);
var lineLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
1 // 1px
);
lineLp.setMargins(padH, 0, padH, 0);
line.setLayoutParams(lineLp);
line.setBackgroundColor(dividerColor);
parent.addView(line);
}
// 容器
var row = new android.widget.LinearLayout(context);
row.setOrientation(android.widget.LinearLayout.VERTICAL);
// 增加点击波纹反馈
try {
var outValue = new android.util.TypedValue();
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
row.setBackgroundResource(outValue.resourceId);
} catch(e) {}
row.setPadding(padH, padV, padH, padV);
var self = this;
if (item.type === "bool") {
// === 开关类型 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
var sw = new android.widget.Switch(context);
try { sw.setTextOn(""); sw.setTextOff(""); } catch (eT) {}
// 优化开关颜色
try {
var states = [
[android.R.attr.state_checked],
[-android.R.attr.state_checked]
];
var thumbColors = [primary, switchOff];
var trackColors = [self.withAlpha(primary, 0.5), self.withAlpha(switchOff, 0.5)];
var thumbList = new android.content.res.ColorStateList(states, thumbColors);
var trackList = new android.content.res.ColorStateList(states, trackColors);
sw.setThumbTintList(thumbList);
sw.setTrackTintList(trackList);
} catch(eColor) {}
var cur = !!self.getPendingValue(item.key);
sw.setChecked(cur);
// 监听器
sw.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, !!checked);
if (self.L) self.L.d("pending " + String(item.key) + "=" + String(!!checked));
} catch (e0) {}
}
}));
// 点击行也触发开关
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
sw.setChecked(!sw.isChecked());
}
}));
row.addView(sw);
parent.addView(row);
} else if (item.type === "int" || item.type === "float") {
// === 数值类型 (SeekBar) ===
// 垂直布局:上面是 标题+数值,下面是 SeekBar
// 第一行:标题 + 数值
var line1 = new android.widget.LinearLayout(context);
line1.setOrientation(android.widget.LinearLayout.HORIZONTAL);
line1.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
line1.addView(tv);
var valTv = new android.widget.TextView(context);
valTv.setTextColor(primary);
valTv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
valTv.setTypeface(null, android.graphics.Typeface.BOLD);
line1.addView(valTv);
row.addView(line1);
// 第二行SeekBar
var sb = new android.widget.SeekBar(context);
var sbLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
sbLp.topMargin = self.dp(16); // 增加间距
sb.setLayoutParams(sbLp);
// 优化 SeekBar 颜色
try {
sb.getThumb().setTint(primary);
sb.getProgressDrawable().setTint(primary);
} catch(eColor) {}
// 配置 SeekBar
var min = (item.min !== undefined) ? Number(item.min) : 0;
var max = (item.max !== undefined) ? Number(item.max) : 100;
var step = (item.step !== undefined) ? Number(item.step) : 1;
var curV = Number(self.getPendingValue(item.key));
if (isNaN(curV)) curV = min;
curV = self.clamp(curV, min, max);
var maxP = Math.floor((max - min) / step);
if (maxP < 1) maxP = 1;
sb.setMax(maxP);
var curP = Math.floor((curV - min) / step);
if (curP < 0) curP = 0;
if (curP > maxP) curP = maxP;
sb.setProgress(curP);
function formatVal(v) {
if (item.type === "float") return String(Math.round(v * 1000) / 1000);
return String(Math.round(v));
}
function computeValByProgress(p) {
var v = min + p * step;
v = self.clamp(v, min, max);
if (item.type === "int") v = Math.round(v);
if (item.type === "float") v = Math.round(v * 1000) / 1000;
return v;
}
valTv.setText(formatVal(curV));
sb.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener({
onProgressChanged: function(seek, progress, fromUser) {
try {
self.touchActivity();
var v = computeValByProgress(progress);
valTv.setText(formatVal(v));
if (fromUser) {
self.setPendingValue(item.key, v);
}
} catch (e1) {}
},
onStartTrackingTouch: function() { try { self.touchActivity(); } catch (e2) {} },
onStopTrackingTouch: function() { try { self.touchActivity(); } catch (e3) {} }
}));
row.addView(sb);
parent.addView(row);
} else if (item.type === "action") {
// === 动作按钮 ===
row.setOrientation(android.widget.LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setClickable(true);
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
var tvLp = new android.widget.LinearLayout.LayoutParams(0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT);
tvLp.weight = 1;
tv.setLayoutParams(tvLp);
row.addView(tv);
// 样式化文本按钮
var btn = new android.widget.TextView(context);
btn.setText("打开");
btn.setTextColor(primary);
btn.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
btn.setTypeface(null, android.graphics.Typeface.BOLD);
btn.setGravity(android.view.Gravity.CENTER);
btn.setPadding(self.dp(16), self.dp(8), self.dp(16), self.dp(8));
// 透明波纹背景
btn.setBackground(self.ui.createTransparentRippleDrawable(primary, self.dp(16)));
btn.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) {}
}
}));
row.addView(btn);
// 行点击也触发
row.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
self.touchActivity();
if (item.action === "open_btn_mgr") {
self.showPanelAvoidBall("btn_editor");
}
} catch(e) {}
}
}));
parent.addView(row);
} else if (item.type === "text") {
// === 文本输入 ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var et = new android.widget.EditText(context);
var curVal = self.getPendingValue(item.key);
if (curVal === undefined || curVal === null) curVal = "";
et.setText(String(curVal));
et.setTextColor(textColor);
et.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
et.setBackground(self.ui.createRoundDrawable(isDark ? C.inputBgDark : C.inputBgLight, self.dp(6)));
et.setPadding(self.dp(8), self.dp(8), self.dp(8), self.dp(8));
et.setSingleLine(true);
var etLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
etLp.topMargin = self.dp(8);
et.setLayoutParams(etLp);
// Explicitly request keyboard on click
et.setOnClickListener(new android.view.View.OnClickListener({
onClick: function(v) {
try {
v.requestFocus();
var imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(v, 0);
} catch(e) {}
}
}));
et.addTextChangedListener(new android.text.TextWatcher({
beforeTextChanged: function(s, start, count, after) {},
onTextChanged: function(s, start, before, count) {},
afterTextChanged: function(s) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(s));
} catch (e) {}
}
}));
row.addView(et);
parent.addView(row);
} else if (item.type === "single_choice") {
// === 单选类型 (RadioGroup) ===
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(textColor);
tv.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16);
row.addView(tv);
var rg = new android.widget.RadioGroup(context);
rg.setOrientation(android.widget.RadioGroup.VERTICAL);
var rgLp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
rgLp.topMargin = self.dp(8);
rg.setLayoutParams(rgLp);
var curVal = String(self.getPendingValue(item.key) || "");
if (!curVal) curVal = "auto"; // default
var options = item.options || [];
for (var i = 0; i < options.length; i++) {
(function(opt) {
var rb = new android.widget.RadioButton(context);
rb.setText(String(opt.label));
rb.setTextColor(textColor);
rb.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 14);
// 颜色优化
try {
var states = [[android.R.attr.state_checked], [-android.R.attr.state_checked]];
var colors = [primary, secColor];
rb.setButtonTintList(new android.content.res.ColorStateList(states, colors));
} catch(eC){}
rb.setId(android.view.View.generateViewId ? android.view.View.generateViewId() : i);
// Check state
if (String(opt.value) === curVal) {
rb.setChecked(true);
}
rb.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener({
onCheckedChanged: function(btn, checked) {
if (checked) {
try {
self.touchActivity();
self.setPendingValue(item.key, String(opt.value));
} catch(e){}
}
}
}));
rg.addView(rb);
})(options[i]);
}
row.addView(rg);
parent.addView(row);
} else {
// 兜底文本
var tv = new android.widget.TextView(context);
tv.setText(String(item.name));
tv.setTextColor(secColor);
row.addView(tv);
parent.addView(row);
}
};