# -*- coding: utf-8 -*- """安卓端新增:局域网连接页面 + 服务器延迟检测。 用户要求: "在首页没连之前可以加一个进入局域网连接页面, 就可以用局域网进行连接,不经过中转服务器" "加个东西就是可以检查延迟检查连接服务器" 设计: 首页(未连接时)三个按钮: [ 局域网连接(同一个 WiFi) ] <- 新增,走局域网直连 [ 输入配对码连接 ] <- 原来的,经中转 [ 中转设置(跨网络用) ] 外加一个延迟状态行: [ 检查服务器 ] 显示"延迟 83 毫秒 / 连不上" 局域网页面: * 自动搜索(UDP 广播,被控端监听 8766) * 搜到的电脑列出来,点一下就连 * 也能手动填 IP:端口 * 配对码 * 全部走 RemoteClient.connect()(直连),不碰中转 两条路并存:局域网友好、中转兜底。 """ from __future__ import annotations import ast import sys from pathlib import Path MAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote\android\src" r"\com\lanremote\watch\MainActivity.java") # ===================== 1. 首页加按钮 ===================== HOME_ANCHOR = ''' Button codeButton = bigButton("输入配对码连接", C_OK, 15);''' HOME_NEW = ''' // 局域网连接:手表和电脑在同一个 WiFi 时走这条,不经过中转。 // 用户要求:"在首页没连之前可以加一个进入局域网连接页面, // 就可以用局域网进行连接,不经过中转服务器" Button lanButton = bigButton("局域网连接(同一个 WiFi)", C_ACCENT, 15); LinearLayout.LayoutParams lanParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(52)); lanParams.setMargins(0, 0, 0, dp(8)); lanButton.setLayoutParams(lanParams); lanButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showLanPanel(); } }); box.addView(lanButton); Button codeButton = bigButton("输入配对码连接", C_OK, 15);''' HOME_LATENCY_ANCHOR = ''' statusView = new TextView(this); statusView.setTextColor(C_DIM); statusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); statusView.setPadding(0, 0, 0, dp(6)); box.addView(statusView);''' HOME_LATENCY_NEW = ''' // 服务器延迟:连不上中转的时候,这一条最有用 LinearLayout serverRow = new LinearLayout(this); serverRow.setOrientation(LinearLayout.HORIZONTAL); serverRow.setGravity(Gravity.CENTER_VERTICAL); serverStatus = new TextView(this); serverStatus.setTextColor(C_DIM); serverStatus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); serverStatus.setText("服务器:还没测"); LinearLayout.LayoutParams ssParams = new LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); serverStatus.setLayoutParams(ssParams); serverRow.addView(serverStatus); Button pingButton = smallButton("检查服务器", C_CARD, new Runnable() { @Override public void run() { checkServer(); } }); serverRow.addView(pingButton); box.addView(serverRow); statusView = new TextView(this); statusView.setTextColor(C_DIM); statusView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); statusView.setPadding(0, dp(6), 0, dp(6)); box.addView(statusView);''' # ===================== 2. 字段 ===================== FIELD_ANCHOR = ''' private TextView statusView;''' FIELD_NEW = ''' // ---- 局域网连接页面 ---- private FrameLayout lanPanel; private LinearLayout lanResultBox; private EditText lanIpInput; private EditText lanPortInput; private EditText lanCodeInput; private TextView lanStatus; private volatile boolean lanSearching; // ---- 服务器延迟 ---- private TextView serverStatus; private TextView statusView;''' # ===================== 3. onCreate 里建这个面板 ===================== CREATE_ANCHOR = ''' buildConnectPanel(); buildControlPanel(); buildMirrorPanel();''' CREATE_NEW = ''' buildConnectPanel(); buildControlPanel(); buildMirrorPanel(); buildLanPanel();''' CREATE_ADD_ANCHOR = ''' root.addView(connectPanel, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));''' CREATE_ADD_NEW = ''' root.addView(connectPanel, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); root.addView(lanPanel, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));''' CREATE_HIDE_ANCHOR = ''' controlPanel.setVisibility(View.GONE); mirrorPanel.setVisibility(View.GONE); connectPanel.setVisibility(View.VISIBLE);''' CREATE_HIDE_NEW = ''' controlPanel.setVisibility(View.GONE); mirrorPanel.setVisibility(View.GONE); lanPanel.setVisibility(View.GONE); connectPanel.setVisibility(View.VISIBLE);''' # ===================== 4. 新方法(插在 showConnectPanel 前) ===================== METHOD_ANCHOR = ''' private void showConnectPanel() {''' METHOD_NEW = ''' // ================================================================== // 局域网连接(不经过中转) // ================================================================== private void buildLanPanel() { lanPanel = new FrameLayout(this); lanPanel.setBackgroundColor(C_BG); ScrollView scroll = new ScrollView(this); scroll.setBackgroundColor(C_BG); LinearLayout box = new LinearLayout(this); box.setOrientation(LinearLayout.VERTICAL); box.setPadding(dp(10), dp(12), dp(10), dp(12)); scroll.addView(box, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); lanPanel.addView(scroll, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // ---- 标题 + 返回 ---- LinearLayout top = new LinearLayout(this); top.setOrientation(LinearLayout.HORIZONTAL); top.setGravity(Gravity.CENTER_VERTICAL); TextView title = new TextView(this); title.setText("局域网连接"); title.setTextColor(C_FG); title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 17); LinearLayout.LayoutParams tParams = new LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); title.setLayoutParams(tParams); top.addView(title); top.addView(smallButton("返回", C_CARD, new Runnable() { @Override public void run() { hideLanPanel(); } })); box.addView(top); TextView hint = new TextView(this); hint.setText("手表和电脑连同一个 WiFi 时用这个,速度更快、不占流量。\\n" + "电脑上要开着 ykxmz。"); hint.setTextColor(C_DIM); hint.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); hint.setPadding(0, dp(6), 0, dp(10)); box.addView(hint); // ---- 自动搜索 ---- Button search = bigButton("自动搜索电脑", C_ACCENT, 14); LinearLayout.LayoutParams sParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(50)); sParams.setMargins(0, 0, 0, dp(8)); search.setLayoutParams(sParams); search.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { startLanSearch(); } }); box.addView(search); lanStatus = new TextView(this); lanStatus.setTextColor(C_DIM); lanStatus.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); lanStatus.setPadding(0, 0, 0, dp(6)); box.addView(lanStatus); lanResultBox = new LinearLayout(this); lanResultBox.setOrientation(LinearLayout.VERTICAL); box.addView(lanResultBox); // ---- 手动填地址 ---- TextView manualTip = new TextView(this); manualTip.setText("搜不到就自己填(电脑上 ykxmz 会显示 IP)"); manualTip.setTextColor(C_DIM); manualTip.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); manualTip.setPadding(0, dp(10), 0, dp(4)); box.addView(manualTip); lanIpInput = makeInput("电脑 IP,如 192.168.10.105", InputType.TYPE_CLASS_TEXT); box.addView(labeled("电脑 IP", lanIpInput)); lanPortInput = makeInput("端口(默认 8765)", InputType.TYPE_CLASS_NUMBER); box.addView(labeled("端口", lanPortInput)); lanCodeInput = makeInput("6 位配对码", InputType.TYPE_CLASS_NUMBER); box.addView(labeled("配对码", lanCodeInput)); Button connect = bigButton("连上这台电脑", C_OK, 15); LinearLayout.LayoutParams cParams = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(52)); cParams.setMargins(0, dp(10), 0, 0); connect.setLayoutParams(cParams); connect.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { connectLanTyped(); } }); box.addView(connect); } private void showLanPanel() { connectPanel.setVisibility(View.GONE); controlPanel.setVisibility(View.GONE); if (mirrorPanel != null) { mirrorPanel.setVisibility(View.GONE); } lanPanel.setVisibility(View.VISIBLE); getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); // 进来先自动搜一次,省得用户还要自己点 String savedCode = savedPairingCode(); if (lanCodeInput != null && savedCode != null && !savedCode.isEmpty() && lanCodeInput.getText().length() == 0) { lanCodeInput.setText(savedCode); } startLanSearch(); } private void hideLanPanel() { lanPanel.setVisibility(View.GONE); showConnectPanel(); } /** 广播搜一圈,找到的电脑列出来。 */ private void startLanSearch() { if (lanSearching) { return; } lanSearching = true; if (lanResultBox != null) { lanResultBox.removeAllViews(); } setLanStatus("正在搜索…(最多 6 秒)", C_DIM); appendLog("局域网搜索开始"); io.execute(new Runnable() { @Override public void run() { java.util.List found; try { found = Discovery.search(MainActivity.this, 4000); } catch (Throwable error) { found = new java.util.ArrayList(); appendLog("搜索失败:" + error); } final java.util.List result = found; runOnUiThread(new Runnable() { @Override public void run() { lanSearching = false; if (lanResultBox == null) { return; } lanResultBox.removeAllViews(); if (result.isEmpty()) { setLanStatus("没搜到电脑。确认:① 电脑开着 ykxmz ② " + "同一个 WiFi ③ 路由器没开 AP 隔离。" + "\\n也可以直接填下面的 IP。", C_WARN); appendLog("局域网没搜到电脑"); return; } setLanStatus("搜到 " + result.size() + " 台,点一下就连", C_OK); appendLog("搜到 " + result.size() + " 台电脑"); for (final DiscoveryCore.Found item : result) { Button b = bigButton( item.name + " · " + item.host, C_CARD, 14); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(50)); lp.setMargins(0, 0, 0, dp(6)); b.setLayoutParams(lp); b.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (lanIpInput != null) { lanIpInput.setText(item.host); } if (lanPortInput != null) { lanPortInput.setText( String.valueOf(item.port)); } connectLanTyped(); } }); lanResultBox.addView(b); } } }); } }); } private void setLanStatus(String text, int color) { if (lanStatus != null) { lanStatus.setText(text); lanStatus.setTextColor(color); } } /** 用手填/点选的地址直连,不走中转。 */ private void connectLanTyped() { String host = lanIpInput == null ? "" : lanIpInput.getText().toString().trim(); String portText = lanPortInput == null ? "" : lanPortInput.getText().toString().trim(); String code = lanCodeInput == null ? "" : lanCodeInput.getText().toString().trim(); if (host.isEmpty()) { setLanStatus("先填电脑 IP,或者上面搜一下", C_WARN); toast("还没填电脑 IP"); return; } int port = RemoteClient.DEFAULT_PORT; if (!portText.isEmpty()) { try { port = Integer.parseInt(portText); } catch (NumberFormatException error) { setLanStatus("端口要是数字", C_WARN); return; } } if (code.isEmpty()) { setLanStatus("要填 6 位配对码", C_WARN); toast("还没填配对码"); return; } rememberPairingCode(code); lastHost = host; lastPort = port; lastCode = code; lanDirect = true; // 断了自动切回中转 appendLog("局域网直连 " + host + ":" + port); connect(host, port, code, null, DeviceStore.deviceKey(host, port, store.deviceId())); } // ================================================================== // 服务器延迟检测 // ================================================================== private void checkServer() { final String relay = relayUrl(); if (relay.isEmpty()) { if (serverStatus != null) { serverStatus.setText("服务器:还没设中转地址"); serverStatus.setTextColor(C_WARN); } return; } if (serverStatus != null) { serverStatus.setText("服务器:正在测…"); serverStatus.setTextColor(C_DIM); } appendLog("开始检查服务器 " + relay); io.execute(new Runnable() { @Override public void run() { final long started = System.currentTimeMillis(); String text; int color = C_DIM; try { java.net.URL url = new java.net.URL(relay + "/v1/health"); java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); conn.setRequestProperty("User-Agent", "LanRemote/1.4.0"); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); conn.setRequestMethod("GET"); int code = conn.getResponseCode(); long ms = System.currentTimeMillis() - started; if (code == 200) { text = "服务器:延迟 " + ms + " 毫秒"; color = ms < 300 ? C_OK : (ms < 900 ? C_WARN : C_DANGER); } else { text = "服务器:HTTP " + code; color = C_DANGER; } try { conn.disconnect(); } catch (Throwable ignored) { // 关不掉也无所谓 } } catch (Throwable error) { text = "服务器:连不上(" + shortReason(error) + ")"; color = C_DANGER; } final String shown = text; final int tint = color; runOnUiThread(new Runnable() { @Override public void run() { if (serverStatus != null) { serverStatus.setText(shown); serverStatus.setTextColor(tint); } appendLog(shown); } }); } }); } /** 异常名压短,免得界面上一长串。 */ private static String shortReason(Throwable error) { String name = error.getClass().getSimpleName(); if (name.isEmpty()) { name = "错误"; } String message = String.valueOf(error.getMessage()); if (message.length() > 40) { message = message.substring(0, 40); } return message.isEmpty() || "null".equals(message) ? name : (name + " " + message); } private void showConnectPanel() {''' # ===================== 5. 返回键处理 ===================== BACK_ANCHOR = ''' protected void onDestroy() {''' BACK_NEW = ''' private boolean closeLanIfOpen() { if (lanPanel != null && lanPanel.getVisibility() == View.VISIBLE) { hideLanPanel(); return true; } return false; } @Override public void onBackPressed() { // 局域网页面开着的话,先退回首页 if (closeLanIfOpen()) { return; } super.onBackPressed(); } @Override protected void onDestroy() {''' def main() -> int: text = MAIN.read_text(encoding="utf-8") if "buildLanPanel" in text: print(" 已经加过") return 0 ok = True for old, new, label in ( (HOME_ANCHOR, HOME_NEW, "首页加局域网按钮"), (HOME_LATENCY_ANCHOR, HOME_LATENCY_NEW, "首页加延迟状态"), (FIELD_ANCHOR, FIELD_NEW, "加字段"), (CREATE_ANCHOR, CREATE_NEW, "onCreate 建面板"), (CREATE_ADD_ANCHOR, CREATE_ADD_NEW, "onCreate 挂面板"), (CREATE_HIDE_ANCHOR, CREATE_HIDE_NEW, "onCreate 默认隐藏"), (METHOD_ANCHOR, METHOD_NEW, "加局域网页面和延迟检测"), (BACK_ANCHOR, BACK_NEW, "加返回键处理")): if old not in text: print(f" [失败] {label} —— 锚点没找到") ok = False continue if text.count(old) != 1: print(f" [警告] {label} 锚点出现 {text.count(old)} 次") text = text.replace(old, new, 1) print(f" {label}") MAIN.write_text(text, encoding="utf-8", newline="\n") try: ast.parse(text) except SyntaxError: pass # Java 不是 Python,这里只占位 print(" 已写入") return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())