fix: handle system gestures for ToolHub panels

This commit is contained in:
7015725
2026-05-12 10:24:30 +08:00
parent 5c95d04fab
commit 7b7fbdf9cf
8 changed files with 124 additions and 140 deletions

View File

@@ -41,6 +41,7 @@ function FloatBallAppWM(logger) {
viewerPanel: null,
viewerPanelLp: null,
viewerPanelType: null,
mask: null,
maskLp: null,

View File

@@ -185,6 +185,7 @@ FloatBallAppWM.prototype.hideViewerPanel = function() {
this.safeRemoveView(this.state.viewerPanel, "viewerPanel");
this.state.viewerPanel = null;
this.state.viewerPanelLp = null;
this.state.viewerPanelType = null;
this.state.addedViewer = false;
this.hideMask();
@@ -193,6 +194,97 @@ FloatBallAppWM.prototype.hideViewerPanel = function() {
this._clearHeavyCachesIfAllHidden("hideViewerPanel");
};
FloatBallAppWM.prototype.handlePanelBack = function(which, reason) {
// 这段代码的主要内容/用途:适配全面屏系统返回手势/返回键,让 ToolHub 设置类 UI 能按“上一级 -> 关闭”退出。
try {
if (this.state.closing) return false;
var w = which ? String(which) : "";
if (!w && this.state.addedViewer) w = String(this.state.viewerPanelType || "viewer");
if (!w && this.state.addedSettings) w = "settings";
if (!w && this.state.addedPanel) w = "main";
if (this.state.addedViewer) {
var vt = String(this.state.viewerPanelType || w || "viewer");
if (vt === "btn_editor") {
if (this.state.editingButtonIndex !== null && this.state.editingButtonIndex !== undefined) {
this.state.editingButtonIndex = null;
this.state.keepBtnEditorState = true;
this.showPanelAvoidBall("btn_editor");
return true;
}
this.hideViewerPanel();
this.showPanelAvoidBall("settings");
return true;
}
if (vt === "schema_editor") {
if (this.state.editingSchemaIndex !== null && this.state.editingSchemaIndex !== undefined) {
this.state.editingSchemaIndex = null;
this.state.keepSchemaEditorState = true;
this.showPanelAvoidBall("schema_editor");
return true;
}
this.hideViewerPanel();
this.showPanelAvoidBall("settings");
return true;
}
this.hideViewerPanel();
return true;
}
if (this.state.addedSettings) {
this.state.previewMode = false;
if (this.state.addedPanel) this.hideMainPanel();
this.hideSettingsPanel();
return true;
}
if (this.state.addedPanel) {
this.hideMainPanel();
return true;
}
} catch (e) {
safeLog(this.L, 'e', "handlePanelBack fail reason=" + String(reason || "") + " err=" + String(e));
}
return false;
};
FloatBallAppWM.prototype.handleSystemUiDismiss = function(reason) {
// 这段代码的主要内容/用途:系统 Home/最近任务手势发生时关闭 ToolHub 面板,只保留悬浮球,避免 overlay 残留在桌面/多任务上。
try {
var r = String(reason || "");
if (r === "homekey" || r === "recentapps" || r === "fs_gesture" || r === "gestureNav") {
this.hideAllPanels();
return true;
}
} catch (e) {
safeLog(this.L, 'e', "handleSystemUiDismiss fail: " + String(e));
}
return false;
};
FloatBallAppWM.prototype.attachPanelSystemKeyHandler = function(panel, which) {
try {
if (!panel) return;
var self = this;
panel.setFocusable(true);
panel.setFocusableInTouchMode(true);
panel.setOnKeyListener(new android.view.View.OnKeyListener({
onKey: function(v, keyCode, event) {
try {
if (!event) return false;
if (event.getAction() !== android.view.KeyEvent.ACTION_UP) return false;
if (keyCode === android.view.KeyEvent.KEYCODE_BACK) return self.handlePanelBack(which, "back_key");
if (keyCode === android.view.KeyEvent.KEYCODE_ESCAPE) return self.handlePanelBack(which, "escape_key");
} catch (e) { safeLog(self.L, 'e', "panel key handler fail: " + String(e)); }
return false;
}
}));
panel.post(new java.lang.Runnable({ run: function() { try { panel.requestFocus(); } catch(eFocus) {} } }));
} catch (e) {
safeLog(this.L, 'e', "attachPanelSystemKeyHandler fail which=" + String(which || "") + " err=" + String(e));
}
};
FloatBallAppWM.prototype.clearHeavyCaches = function(reason) {
// 这段代码的主要内容/用途:在所有面板都关闭后,主动清理"图标/快捷方式"等重缓存,降低 system_server 常驻内存。
// 说明:仅清理缓存引用,不强行 recycle Bitmap避免误伤仍被使用的 Drawable。

View File

@@ -1,50 +1,5 @@
// @version 1.0.1
// =======================【WM 线程:按钮动作执行】======================
FloatBallAppWM.prototype.execSystemNavigation = function(navAction) {
var ret = { ok: false, method: "", action: String(navAction || ""), err: "" };
var a = ret.action;
var keyCode = 0;
if (a === "back") keyCode = android.view.KeyEvent.KEYCODE_BACK;
else if (a === "home") keyCode = android.view.KeyEvent.KEYCODE_HOME;
else if (a === "recents") keyCode = android.view.KeyEvent.KEYCODE_APP_SWITCH;
else { ret.err = "unknown nav action: " + a; return ret; }
try {
var now = android.os.SystemClock.uptimeMillis();
var KeyEvent = android.view.KeyEvent;
var InputDevice = android.view.InputDevice;
var InputManager = android.hardware.input.InputManager;
var down = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyEvent.KEYCODE_UNKNOWN, 0, KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD);
var up = new KeyEvent(now, android.os.SystemClock.uptimeMillis(), KeyEvent.ACTION_UP, keyCode, 0, 0, KeyEvent.KEYCODE_UNKNOWN, 0, KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_KEYBOARD);
var im = InputManager.getInstance();
var mode = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
var ok1 = im.injectInputEvent(down, mode);
var ok2 = im.injectInputEvent(up, mode);
ret.ok = !!(ok1 && ok2);
ret.method = "InputManager.keyevent";
if (!ret.ok) ret.err = "injectInputEvent returned false";
return ret;
} catch (eKey) {
ret.err = String(eKey);
}
try {
var cmd = "input keyevent " + String(keyCode);
var b64 = encodeBase64Utf8(cmd);
var r = this.execShellSmart(b64, true);
if (r && r.ok) {
ret.ok = true;
ret.method = "BroadcastBridge.input_keyevent";
ret.err = "";
return ret;
}
ret.err = ret.err + "; shell fallback failed: " + JSON.stringify(r || {});
} catch (eShell) {
ret.err = ret.err + "; shell fallback exception: " + String(eShell);
}
return ret;
};
FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
// # 点击防抖
// 这段代码的主要内容/用途:防止在按钮面板上连续/乱点导致重复执行与 UI 状态机冲突(可能触发 system_server 异常重启)。
@@ -97,21 +52,6 @@ FloatBallAppWM.prototype.execButtonAction = function(btn, idx) {
return;
}
if (t === "nav") {
var navAction = btn.navAction ? String(btn.navAction) : "";
if (!navAction && btn.key) navAction = String(btn.key);
if (!navAction) { this.toast("按钮#" + idx + " 缺少导航动作"); return; }
try { this.hideAllPanels(); } catch (eHideNav) {}
var rn = this.execSystemNavigation(navAction);
if (rn && rn.ok) {
safeLog(this.L, 'i', "nav ok action=" + navAction + " via=" + String(rn.method || ""));
return;
}
this.toast("导航键执行失败: " + navAction);
safeLog(this.L, 'e', "nav fail action=" + navAction + " ret=" + JSON.stringify(rn || {}));
return;
}
if (t === "app") {
var pkg = btn.pkg ? String(btn.pkg) : "";
if (!pkg) { this.toast("按钮#" + idx + " 缺少 pkg"); return; }

View File

@@ -506,13 +506,6 @@ FloatBallAppWM.prototype.buildButtonEditorPanelView = function() {
if(btnCfg.type === "app") desc = "应用: " + (btnCfg.pkg||"");
else if(btnCfg.type === "broadcast") desc = "广播: " + (btnCfg.action||"");
else if(btnCfg.type === "shortcut") desc = "快捷方式";
else if(btnCfg.type === "nav") {
var na = String(btnCfg.navAction || btnCfg.key || "");
if (na === "back") desc = "系统导航: 返回";
else if (na === "home") desc = "系统导航: 主页";
else if (na === "recents") desc = "系统导航: 最近任务";
else desc = "系统导航";
}
else desc = "命令: " + (btnCfg.cmd || "").substring(0, 20) + "...";
detailTv.setText(desc);
detailTv.setTextColor(subTextColor);
@@ -1992,8 +1985,7 @@ FloatBallAppWM.prototype.buildButtonEditorPanelView = function() {
{ id: 1, val: "shell", txt: "Shell" },
{ id: 2, val: "app", txt: "App" },
{ id: 3, val: "broadcast", txt: "发送广播" },
{ id: 4, val: "shortcut", txt: "快捷方式" },
{ id: 5, val: "nav", txt: "系统导航" }
{ id: 4, val: "shortcut", txt: "快捷方式" }
];
// 初始化选中值
@@ -2175,51 +2167,6 @@ appWrap.addView(inputAppLaunchUser.view);
bcWrap.addView(inputExtras.view);
dynamicContainer.addView(bcWrap);
// --- System Navigation ---
// 这段代码的主要内容/用途:为全面屏手势场景提供虚拟返回/主页/最近任务按钮,不依赖屏幕底部导航栏。
var navWrap = new android.widget.LinearLayout(context);
navWrap.setOrientation(android.widget.LinearLayout.VERTICAL);
navWrap.setPadding(0, self.dp(4), 0, self.dp(8));
var navLbl = new android.widget.TextView(context);
navLbl.setText("导航键功能");
navLbl.setTextColor(subTextColor);
navLbl.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 12);
navWrap.addView(navLbl);
var navGroup = new android.widget.RadioGroup(context);
navGroup.setOrientation(android.widget.RadioGroup.VERTICAL);
var selectedNavAction = targetBtn.navAction ? String(targetBtn.navAction) : (targetBtn.key ? String(targetBtn.key) : "back");
var navItems = [
{ id: 101, val: "back", txt: "返回键Back" },
{ id: 102, val: "home", txt: "主页键Home" },
{ id: 103, val: "recents", txt: "最近任务键Recents" }
];
for (var ni = 0; ni < navItems.length; ni++) {
var nr = new android.widget.RadioButton(context);
nr.setId(navItems[ni].id);
nr.setText(navItems[ni].txt);
nr.setTextColor(textColor);
nr.setTag(navItems[ni].val);
nr.setPadding(self.dp(8), self.dp(4), self.dp(8), self.dp(4));
navGroup.addView(nr);
if (navItems[ni].val === selectedNavAction) navGroup.check(navItems[ni].id);
}
navGroup.setOnCheckedChangeListener(new android.widget.RadioGroup.OnCheckedChangeListener({
onCheckedChanged: function(group, checkedId) {
try {
var rb = group.findViewById(checkedId);
if (rb) selectedNavAction = String(rb.getTag());
} catch(eNavChg) { safeLog(null, 'e', "catch " + String(eNavChg)); }
}
}));
navWrap.addView(navGroup);
var navHint = new android.widget.TextView(context);
navHint.setText("优先用 InputManager 注入按键;失败时自动回退到 Shell 广播桥 input keyevent。");
navHint.setTextColor(subTextColor);
navHint.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 11);
navHint.setPadding(self.dp(8), self.dp(4), self.dp(8), 0);
navWrap.addView(navHint);
dynamicContainer.addView(navWrap);
// --- Shortcut ---
// 新增:启动系统/应用快捷方式Launcher Shortcuts
// 字段说明:
@@ -3227,7 +3174,6 @@ shortcutWrap.addView(scBody);
shellWrap.setVisibility(typeVal === "shell" ? android.view.View.VISIBLE : android.view.View.GONE);
appWrap.setVisibility(typeVal === "app" ? android.view.View.VISIBLE : android.view.View.GONE);
bcWrap.setVisibility(typeVal === "broadcast" ? android.view.View.VISIBLE : android.view.View.GONE);
navWrap.setVisibility(typeVal === "nav" ? android.view.View.VISIBLE : android.view.View.GONE);
shortcutWrap.setVisibility(typeVal === "shortcut" ? android.view.View.VISIBLE : android.view.View.GONE);
}
@@ -3288,7 +3234,6 @@ shortcutWrap.addView(scBody);
delete newBtn.uri;
delete newBtn.shortcutId;
delete newBtn.shortcutRunMode;
delete newBtn.navAction; delete newBtn.key;
delete newBtn.launchUserId;
var isValid = true;
@@ -3320,15 +3265,6 @@ try {
try { newBtn.extras = JSON.parse(ex); inputExtras.setError(null); }
catch(e) { inputExtras.setError("JSON 格式错误"); isValid=false; }
}
} else if (newBtn.type === "nav") {
var nv = selectedNavAction ? String(selectedNavAction) : "back";
if (nv !== "back" && nv !== "home" && nv !== "recents") nv = "back";
newBtn.navAction = nv;
if (!newBtn.title || String(newBtn.title).trim().length === 0) {
if (nv === "back") newBtn.title = "返回";
else if (nv === "home") newBtn.title = "主页";
else newBtn.title = "最近任务";
}
} else if (newBtn.type === "shortcut") {
var sp = inputScPkg.getValue();
var sid = inputScId.getValue();

View File

@@ -437,11 +437,15 @@ FloatBallAppWM.prototype.addPanel = function(panel, x, y, which) {
lp.x = x;
lp.y = y;
try { if (this.attachPanelSystemKeyHandler) this.attachPanelSystemKeyHandler(panel, which); } catch (eKeyAttach) { safeLog(this.L, 'e', "attach panel key fail which=" + String(which) + " err=" + String(eKeyAttach)); }
try { this.state.wm.addView(panel, lp); } catch (eAdd) { safeLog(this.L, 'e', "addPanel fail which=" + String(which) + " err=" + String(eAdd)); return; }
if (which === "main") { this.state.panel = panel; this.state.panelLp = lp; this.state.addedPanel = true; }
else if (which === "settings") { this.state.settingsPanel = panel; this.state.settingsPanelLp = lp; this.state.addedSettings = true; }
else { this.state.viewerPanel = panel; this.state.viewerPanelLp = lp; this.state.addedViewer = true; }
else { this.state.viewerPanel = panel; this.state.viewerPanelLp = lp; this.state.viewerPanelType = which; this.state.addedViewer = true; }
try { panel.requestFocus(); } catch (eReqFocus) {}
try {
if (this.config.ENABLE_ANIMATIONS) {

View File

@@ -245,6 +245,17 @@ FloatBallAppWM.prototype.startAsync = function(entryProcInfo, closeRule) {
);
if (cfgRcv) this.state.receivers.push(cfgRcv);
var sysDlgRcv = registerReceiverOnMain("android.intent.action.CLOSE_SYSTEM_DIALOGS", function(ctx, intent) {
try {
var reason = "";
try { reason = String(intent.getStringExtra("reason") || ""); } catch (eReason) { reason = ""; }
h.post(new JavaAdapter(java.lang.Runnable, {
run: function() { try { self.handleSystemUiDismiss(reason); } catch (eSysDlg) {} }
}));
} catch (eSysDlgOuter) {}
});
if (sysDlgRcv) this.state.receivers.push(sysDlgRcv);
var startBox = { ok: false, err: "启动确认超时", added: false };
var startLatch = new java.util.concurrent.CountDownLatch(1);
var posted = false;