Files
ykxmz/_build/patch_watch_tabs.py
xmz7891 b62ac865ea ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

560 lines
23 KiB
Python
Raw Permalink 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.
# -*- coding: utf-8 -*-
"""手表端 UI 彻底重做:分页 + 37 个功能。
原来是一长条 28 个按钮的网格,要滚 274dp。
现在分成 6 页,每页最多 7 个按钮 —— **完全不用滚动**。
媒体 (7) 屏幕 (6) 文件 (6) 语音 (5) 电脑 (6) 系统 (7)
"""
from __future__ import annotations
import sys
from pathlib import Path
MAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\android\src"
r"\com\lanremote\watch\MainActivity.java")
START = " // 按钮网格\n"
END = " flushGrid(grid); // 按当前列数把上面入队的按钮切成行\n"
NEW_PANEL = ''' // ---- 分页标签 ----
HorizontalScrollView tabScroll = new HorizontalScrollView(this);
tabScroll.setHorizontalScrollBarEnabled(false);
tabBar = new LinearLayout(this);
tabBar.setOrientation(LinearLayout.HORIZONTAL);
tabBar.setPadding(0, 0, 0, 0);
tabScroll.addView(tabBar);
controlPanel.addView(tabScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(42)));
// ---- 分页内容 ----
tabContent = new LinearLayout(this);
tabContent.setOrientation(LinearLayout.VERTICAL);
tabContent.setPadding(0, dp(6), 0, dp(6));
ScrollView contentScroll = new ScrollView(this);
contentScroll.addView(tabContent, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
controlPanel.addView(contentScroll, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
'''
# ---------------------------------------------------------------- tabs 机制
TAB_METHODS_ANCHOR = ''' private Button bigButton(String text, int color, float sizeSp) {'''
TAB_METHODS = ''' // ==================================================================
// 分页
// ==================================================================
private static final class Act {
final String label;
final Runnable task;
Act(String label, Runnable task) {
this.label = label;
this.task = task;
}
}
private static final class TabDef {
final String name;
final Act[] acts;
TabDef(String name, Act[] acts) {
this.name = name;
this.acts = acts;
}
}
private TabDef[] tabDefs;
private LinearLayout tabBar;
private LinearLayout tabContent;
private int currentTab = 0;
private Act act(String label, Runnable task) {
return new Act(label, task);
}
/** 定义所有分页。每页最多 7 个按钮,一屏放得下,不用滚。 */
private void initTabs() {
tabDefs = new TabDef[]{
new TabDef("媒体", new Act[]{
act("播放/暂停", new Runnable() {
public void run() { media("play_pause"); }
}),
act("停止", new Runnable() {
public void run() { media("stop"); }
}),
act("上一曲", new Runnable() {
public void run() { media("prev"); }
}),
act("下一曲", new Runnable() {
public void run() { media("next"); }
}),
act("音量 +", new Runnable() {
public void run() { media("vol_up"); }
}),
act("音量 -", new Runnable() {
public void run() { media("vol_down"); }
}),
act("静音", new Runnable() {
public void run() { media("mute"); }
}),
}),
new TabDef("屏幕", new Act[]{
act("屏幕镜像", new Runnable() {
public void run() { openMirrorPanel(); }
}),
act("截屏", new Runnable() {
public void run() { doScreenshot(); }
}),
act("投图到电脑", new Runnable() {
public void run() { pickFile(REQ_IMAGE, "image/*"); }
}),
act("放音到电脑", new Runnable() {
public void run() { pickFile(REQ_AUDIO, "audio/*"); }
}),
act("关显示器", new Runnable() {
public void run() { confirmMonitor(); }
}),
act("设为壁纸", new Runnable() {
public void run() { confirmWallpaper(); }
}),
}),
new TabDef("文件", new Act[]{
act("发送文件", new Runnable() {
public void run() { pickFile(REQ_FILE, "*/*"); }
}),
act("接收文件", new Runnable() {
public void run() { showReceivedFiles(); }
}),
act("剪贴板→电脑", new Runnable() {
public void run() { promptClipSet(); }
}),
act("取电脑剪贴板", new Runnable() {
public void run() { fetchClip(); }
}),
act("剪贴板历史", new Runnable() {
public void run() { pickClipHistory(); }
}),
act("打开文件夹", new Runnable() {
public void run() { pickFolder(); }
}),
}),
new TabDef("语音", new Act[]{
act("语音播报", new Runnable() {
public void run() { promptSay(); }
}),
act("音色", new Runnable() {
public void run() { pickVoice(); }
}),
act("朗读剪贴板", new Runnable() {
public void run() {
sendCommand(Json.obj("cmd", "say_clip"), "朗读剪贴板");
}
}),
act("电脑音乐", new Runnable() {
public void run() { pickLocalAudio(); }
}),
act("弹通知", new Runnable() {
public void run() { promptNotify(); }
}),
}),
new TabDef("电脑", new Act[]{
act("快捷命令", new Runnable() {
public void run() { pickQuickApp(); }
}),
act("打开网址", new Runnable() {
public void run() { promptOpenUrl(); }
}),
act("进程管理", new Runnable() {
public void run() { pickProcess(); }
}),
act("性能", new Runnable() {
public void run() { showPerf(); }
}),
act("磁盘", new Runnable() {
public void run() { showDisk(); }
}),
act("电量", new Runnable() {
public void run() { showBattery(); }
}),
}),
new TabDef("系统", new Act[]{
act("锁屏", new Runnable() {
public void run() { confirmPower("lock", "锁屏"); }
}),
act("睡眠", new Runnable() {
public void run() { confirmPower("sleep", "睡眠"); }
}),
act("关机", new Runnable() {
public void run() { confirmPower("shutdown", "关机"); }
}),
act("重启", new Runnable() {
public void run() { confirmPower("reboot", "重启"); }
}),
act("取消定时关机", new Runnable() {
public void run() { cancelShutdown(); }
}),
act("定时关机", new Runnable() {
public void run() { pickShutdownTimer(); }
}),
act("设备诊断", new Runnable() {
public void run() { openDiagPanel(); }
}),
}),
};
tabBar.removeAllViews();
for (int i = 0; i < tabDefs.length; i++) {
final int index = i;
TextView tab = new TextView(this);
tab.setText(tabDefs[i].name);
tab.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
tab.setGravity(Gravity.CENTER);
tab.setPadding(dp(14), dp(8), dp(14), dp(8));
tab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
selectTab(index);
}
});
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
params.setMargins(0, 0, dp(6), 0);
tab.setLayoutParams(params);
tabBar.addView(tab);
}
selectTab(0);
}
private void selectTab(int index) {
if (tabDefs == null || index < 0 || index >= tabDefs.length) {
return;
}
currentTab = index;
for (int i = 0; i < tabBar.getChildCount(); i++) {
View child = tabBar.getChildAt(i);
boolean on = (i == index);
child.setBackground(rounded(on ? C_ACCENT : C_CARD, 8));
if (child instanceof TextView) {
((TextView) child).setTextColor(on ? 0xFFFFFFFF : C_FG);
}
}
tabContent.removeAllViews();
cellQueue.clear();
for (Act item : tabDefs[index].acts) {
addRow(tabContent, action(item.label, item.task), null);
}
flushGrid(tabContent);
}
// ==================================================================
// 新功能
// ==================================================================
private void media(String what) {
sendCommand(Json.obj("cmd", "media", "action", what), "媒体");
}
private void confirmMonitor() {
new AlertDialog.Builder(this)
.setTitle("关闭显示器")
.setMessage("要关掉电脑显示器吗?(动一下鼠标或按键就能唤醒)")
.setPositiveButton("关掉", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "monitor", "on", false), "关显示器");
}
})
.setNegativeButton("取消", null)
.show();
}
private void confirmWallpaper() {
new AlertDialog.Builder(this)
.setTitle("设为壁纸")
.setMessage("把电脑上最近收到的那张图片设为桌面壁纸?")
.setPositiveButton("设置", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "set_wallpaper"), "设置壁纸");
}
})
.setNegativeButton("取消", null)
.show();
}
private void pickShutdownTimer() {
final int[] minutes = {15, 30, 60, 120};
final String[] labels = {"15 分钟后", "30 分钟后", "1 小时后", "2 小时后"};
showChooser("定时关机", new ArrayList<String>(java.util.Arrays.asList(labels)),
new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "shutdown_timer",
"minutes", minutes[index]), "定时关机");
}
});
}
private void cancelShutdown() {
sendCommand(Json.obj("cmd", "shutdown_timer", "cancel", true), "取消定时关机");
}
private void promptOpenUrl() {
promptInput("在电脑上打开网址", "如 baidu.com", "https://", new InputHandler() {
@Override
public void onInput(String text) {
if (!text.isEmpty()) {
sendCommand(Json.obj("cmd", "open_url", "url", text), "打开网址");
}
}
});
}
private void pickQuickApp() {
final String[][] apps = {
{"任务管理器", "taskmgr"},
{"资源管理器", "explorer"},
{"命令提示符", "cmd"},
{"计算器", "calc"},
{"记事本", "notepad"},
{"画图", "mspaint"},
{"控制面板", "control"},
{"设备管理器", "devmgmt"},
};
List<String> labels = new ArrayList<String>();
for (String[] item : apps) {
labels.add(item[0]);
}
showChooser("在电脑上打开", labels, new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "quick", "app", apps[index][1]),
"打开" + apps[index][0]);
}
});
}
private void pickFolder() {
final String[][] folders = {
{"下载", "downloads"},
{"文档", "documents"},
{"桌面", "desktop"},
{"图片", "pictures"},
{"音乐", "music"},
{"接收目录", "received"},
{"程序目录", "app"},
};
List<String> labels = new ArrayList<String>();
for (String[] item : folders) {
labels.add(item[0]);
}
showChooser("打开电脑文件夹", labels, new Chooser() {
@Override
public void onPick(int index) {
sendCommand(Json.obj("cmd", "open_folder", "folder", folders[index][1]),
"打开" + folders[index][0]);
}
});
}
private void pickProcess() {
appendLog("正在获取电脑进程…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply =
client.call(Json.obj("cmd", "proc_list"), 20000);
final List<String> names = stringList(reply, "names");
final List<String> pids = stringList(reply, "pids");
ui.post(new Runnable() {
@Override
public void run() {
if (names.isEmpty()) {
toast("没读到进程,或没连上");
return;
}
showChooser("占内存最多的进程", names, new Chooser() {
@Override
public void onPick(int index) {
confirmKill(names.get(index), pids.get(index));
}
});
}
});
} catch (final IOException e) {
postError("获取进程", e);
}
}
});
}
private void confirmKill(String label, String pid) {
final int target;
try {
target = Integer.parseInt(pid);
} catch (NumberFormatException e) {
toast("进程号不对");
return;
}
new AlertDialog.Builder(this)
.setTitle("结束进程")
.setMessage("确定要结束这个进程吗?\n\n" + label)
.setPositiveButton("结束", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface d, int w) {
sendCommand(Json.obj("cmd", "proc_kill", "pid", target), "结束进程");
}
})
.setNegativeButton("取消", null)
.show();
}
private void showPerf() {
fetchAndShow("性能", Json.obj("cmd", "perf"), "note");
}
private void showDisk() {
fetchAndShow("磁盘占用", Json.obj("cmd", "disk"), "note");
}
private void showBattery() {
fetchAndShow("电脑电量", Json.obj("cmd", "battery"), "note");
}
/** 拉一条命令,把 note 用对话框显示出来。 */
private void fetchAndShow(final String title, final Map<String, Object> request,
final String field) {
appendLog("正在获取" + title + "…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply = client.call(request, 15000);
final boolean ok = Json.bool(reply, "ok", false);
final String note = ok
? Json.str(reply, field, "(无内容)")
: Json.str(reply, "error", "读取失败");
ui.post(new Runnable() {
@Override
public void run() {
appendLog(title + ":" + note);
new AlertDialog.Builder(MainActivity.this)
.setTitle(title)
.setMessage(note)
.setPositiveButton("知道了", null)
.show();
}
});
} catch (final IOException e) {
postError(title, e);
}
}
});
}
private void pickClipHistory() {
appendLog("正在获取剪贴板历史…");
io.execute(new Runnable() {
@Override
public void run() {
try {
final Map<String, Object> reply =
client.call(Json.obj("cmd", "clip_history"), 12000);
final List<String> texts = stringList(reply, "texts");
final List<String> times = stringList(reply, "times");
ui.post(new Runnable() {
@Override
public void run() {
if (texts.isEmpty()) {
toast("电脑剪贴板还没有历史");
return;
}
List<String> labels = new ArrayList<String>();
for (int i = 0; i < texts.size(); i++) {
String preview = texts.get(i).replace("\\n", " ");
if (preview.length() > 22) {
preview = preview.substring(0, 22) + "…";
}
String when = i < times.size() ? times.get(i) + " " : "";
labels.add(when + preview);
}
showChooser("剪贴板历史(点一条取到手表)", labels, new Chooser() {
@Override
public void onPick(int index) {
copyToWatchClipboard(texts.get(index));
toast("已复制到手表剪贴板");
appendLog("取回一条剪贴板历史");
}
});
}
});
} catch (final IOException e) {
postError("剪贴板历史", e);
}
}
});
}
'''
def main() -> int:
text = MAIN.read_text(encoding="utf-8")
start = text.find(START)
end = text.find(END)
if start < 0 or end < 0:
print(f" [失败] 找不到网格区块 (start={start}, end={end})")
return 1
end += len(END)
text = text[:start] + NEW_PANEL + text[end:]
print(" 分页面板: OK")
if TAB_METHODS_ANCHOR not in text:
print(" [失败] bigButton 锚点未匹配")
return 1
text = text.replace(TAB_METHODS_ANCHOR, TAB_METHODS + TAB_METHODS_ANCHOR, 1)
print(" 分页机制 + 新功能: OK")
# addRow 要能接受 null(一页里按钮数可能是奇数)
old_add = ''' private void addRow(LinearLayout parent, View left, View right) {
// 只入队,真正的行切分在 flushGrid() 里按 gridColumns 统一做。
// 这样上层那串 addRow(a, b) 不用改,列数就能自适应。
cellQueue.add(left);
cellQueue.add(right);
}'''
new_add = ''' private void addRow(LinearLayout parent, View left, View right) {
// 只入队,真正的行切分在 flushGrid() 里按 gridColumns 统一做。
// 允许传 null:分页里某一页的按钮数可能是奇数。
if (left != null) {
cellQueue.add(left);
}
if (right != null) {
cellQueue.add(right);
}
}'''
if old_add in text:
text = text.replace(old_add, new_add, 1)
print(" addRow 支持 null: OK")
else:
print(" [警告] addRow 未替换")
# 初始化分页
old_init = " showConnectPanel();"
new_init = " initTabs();\n showConnectPanel();"
if old_init in text:
text = text.replace(old_init, new_init, 1)
print(" initTabs 调用: OK")
MAIN.write_text(text, encoding="utf-8")
return 0
if __name__ == "__main__":
sys.exit(main())