mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
用户这次提了一整套升级,这是最后一批。
== 一、手表端顶部快捷栏 ==
用户原话:"手表端不管干什么,上面都有一栏,栏里的按钮可以在设置页
自定义,最多可以加 4 个,其中有一个按钮不能改,功能就是一键返回桌面"。
("返回桌面"经确认 = **让被控电脑显示桌面**,即 Win+D。)
* 一条常驻的栏,贴在屏幕顶部,切页面时不动
* **4 个格子**:3 个可自定义 + **1 个固定的「显示桌面」(改不了)**
* 7 个可选动作:传文件 / 屏幕镜像 / 鼠标控制 / 音效 / 关机 /
语音播报 / 投图到电脑
* 设置页里点一下换;也能把某一格留空
* 没连上电脑时按钮置灰(不是藏起来 —— "一直在但暂时不能用"
比"时有时无"更好理解)
实现上的几个要点:
* 快捷栏 addView 在 5 个面板**之后** —— FrameLayout 里后加的在上层,
加在前面会被面板(MATCH_PARENT)整个盖住
* 面板加了 topMargin 给栏让位,集中在 applyPanelTopMargin 里改
(每个面板各写一遍很容易漏一个,表现是"就某一页顶部被切掉")
* 第 4 格在代码里**写死** desktop,而且 QUICK_ACTIONS 里**没有**它 ——
这样它既不会出现在可选列表里,也不可能被改成别的
* 存储用 SharedPreferences。读取时**过滤不认识的 id + 补齐到 3 个**:
以后删了某个动作,老配置也不会让栏变空或下标越界
== 二、远程鼠标控制(手表上的触摸板)==
* 拖动 = 移动光标,单击 = 左键,双击 = 右键
* 下面还有专门的 左键 / 右键 / 滚轮上 / 滚轮下 按钮
* 电脑端用 SendInput 注入(见下)
== 三、弹文字窗口的位置和大小可配 ==
用户原话:"弹文字窗口 …… 位置和大小都能自定义"。
* 位置:右下(默认,保持老习惯)/ 右上 / 左上 / 左下 /
屏幕正中间 / 自定义坐标
* 字号:8~32(夹住,免得设成 200 一个通知占满屏幕)
* 宽度:可指定,0 = 按内容自适应
* 三处窗口定位统一到一个 _notice_geometry() —— 以前三处各写死一遍
右下角,改一次要改三个地方,漏一个用户就看得见
== 四、屏幕被观看时的提示条改成默认不显示 ==
用户原话:"以后不需要弹,啥窗户都没有,就可以像 UU 远程一样"。
* 默认**连窗口都不建**
* **但保留成开关**,设置里可以打开
关于这条:那条提示原来是**故意加**的(有人在看屏幕时,电脑前面的人
应该知道)。用户要求去掉,这是他自己的电脑、自己的教室,
属于正当的远程管理需求。所以能力保留、默认关闭 ——
这软件应该始终有一个"让本机用户看得见正在被控制"的选项可查。
== 五、中转后台能看到版本并远程点更新 ==
* 设备表新增「版本」列
* 每行一个「更新」按钮:点一下让那台电脑自己静默升级
* 离线设备的按钮**置灰并说明原因**(命令只会挂在队列里等它上线,
那时用户早忘了自己点过,突然重启很莫名其妙)
* 和手表那边走**同一条命令**(cmd=update, action=start),
不给"后台触发"单独写一套逻辑 —— 少一套就少一处不一致
== 六、手表端可开关「允许新设备连接」 ==
* 关掉后只有配对过的电脑能连,教室里防别人乱连
* 电脑端新增 _do_allow_new_pairing 这个 feature
* 只影响**配对新设备**,不踢现有会话
(否则老师一点"禁止",自己正连着的那台也被踢掉,成了"自杀开关")
== 修掉的几个自己造的坑 ==
1. **管理页 JS 裸换行** —— 最严重的一个。
ADMIN_JS 是 Python 三引号字符串,我在里面写 `\n` 拼多行文案,
结果是**真的换行**插进 JS,字符串字面量跨行 -> 语法非法 ->
**整个 script 块解析失败 -> 管理页所有按钮全死**(改码/删除/踢下线/全选)。
改成 String.fromCharCode(10),不依赖任何转义层。
2. **路由落进了 do_GET** —— 我拿 "没有这个管理接口" 当锚点,
但那个字符串在文件里有**两处**(do_GET 和 _route_post 各一个),
于是路由加到了 GET 块里,管理页的 POST 打不到它。
表现是"点了更新没反应"。
3. **isConnected() 根本不存在** —— 写快捷栏时凭直觉调了个方法名,
编译才发现。顺手包了一层并加上说明。
4. **两个不存在的方法名** —— onCreate 里写了
buildMousePanelIfNeeded / mousePanelHolder,编译报 cannot find symbol。
5. **未调用方法扫描的假阳性** —— GestureDetector 的
onSingleTapConfirmed / onDoubleTap / onScroll 是框架回调,
扫描把"名字只出现 1 次"当成死代码。改成**跳过 @Override 的方法**。
这种假阳性最坏的结果是有人为了让它变绿把回调删了,手势就失灵了。
6. **测试断言太严导致误报** —— "不能有写死的 240",
但 helper 里 `auto = max(240, ...)` 是自适应的兜底,故意的。
7. **屏幕共享提示那个测试断言的是旧行为** —— 没有简单删掉,
改成验两个方向:开关打开时提示条正常(位置/尺寸/出现/消失),
关闭时默认值必须是 False 且代码路径上不建窗口。
== 验证 ==
全量回归(19 关)全部 PASS,零失败
新增关卡:3a10 静默更新 / 3a11 鼠标控制 / 3a12 后台更新+弹窗 /
3a13 手表快捷栏
Java 编译通过(android-34, --release 8)
APK / 安装包 / 服务端包 全部重建,Win7 兼容校验通过
623 个 Python 文件语法通过
200 lines
8.5 KiB
Python
200 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
'''防回归:抓"定义了但从没被调用"的方法。
|
||
|
||
为什么值得单独一个测试:
|
||
1.7.0 交付后用户装上发现**一片白屏**。根因是
|
||
private void initTabs() { ... } // 定义在第 937 行
|
||
而 onCreate 里只调了 buildControlPanel(),**从头到尾没人调 initTabs()**。
|
||
结果 tabDefs 为 null -> selectTab() 直接 return -> 内容区空白。
|
||
|
||
这种错误编译能过、别的测试也能过,只有真机跑起来才看得见,
|
||
所以必须静态查出来。
|
||
|
||
判定方法:
|
||
统计方法名在文件里出现的次数(定义那一处也算)。
|
||
只出现 1 次 = 只有定义没调用。
|
||
|
||
分两档:
|
||
* **硬性失败**:关键路径上的方法(initTabs / 三个面板 / selectTab)。
|
||
这些漏了就是白屏或功能全无,必须拦住。
|
||
* **仅报告**:其它可疑方法。有些确实是历史遗留的死代码
|
||
(下面的 KNOWN_DEAD 记录了已确认的),有些是接口回调
|
||
(由框架调用,本文件里当然只有定义)。
|
||
报告出来供清理,但不拦构建 —— 免得噪音太大没人看。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
MAIN = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
|
||
/ "MainActivity.java")
|
||
|
||
# 框架/接口回调:由 Android 或 RemoteClient 调用,本文件里只有定义是正常的
|
||
WHITELIST = {
|
||
"onCreate", "onDestroy", "onResume", "onPause", "onStart", "onStop",
|
||
"onRestart", "onNewIntent", "onSaveInstanceState", "onRestoreInstanceState",
|
||
"onBackPressed", "onKeyDown", "onKeyUp", "onWindowFocusChanged",
|
||
"onRequestPermissionsResult", "onActivityResult", "onOptionsItemSelected",
|
||
"onCreateOptionsMenu", "onPrepareOptionsMenu", "onConfigurationChanged",
|
||
"onTouchEvent", "onClick", "onLongClick", "onTouch", "onAnimationUpdate",
|
||
"onAnimationStart", "onAnimationEnd", "onAnimationCancel", "onAnimationRepeat",
|
||
"onPreDraw", "onGlobalLayout", "onScrollChanged", "onItemClick",
|
||
"onItemSelected", "onNothingSelected", "onFocusChange", "onEditorAction",
|
||
"run", "call", "get", "set", "equals", "hashCode", "toString",
|
||
"uncaughtException", "afterTextChanged", "beforeTextChanged",
|
||
"onTextChanged", "onProgressChanged", "onStartTrackingTouch",
|
||
"onStopTrackingTouch", "onDateSet", "onTimeSet", "onDismiss",
|
||
"onCancel", "onShow", "onKey", "onCreateDialog", "onPrepareDialog",
|
||
"onReceive", "onServiceConnected", "onServiceDisconnected",
|
||
"onBind", "onCreateView", "onViewCreated", "onDestroyView",
|
||
"onAttach", "onDetach", "onActivityCreated", "onOptionsMenuClosed",
|
||
"onContextItemSelected", "onCreateContextMenu", "handleMessage",
|
||
"onChildViewAdded", "onChildViewRemoved", "onGlobalFocusChanged",
|
||
"onLayoutChange", "onScrollStateChanged", "onScrolled",
|
||
"onPageStarted", "onPageFinished", "shouldOverrideUrlLoading",
|
||
"onReceivedError", "onDownloadStart", "onChildDraw",
|
||
# RemoteClient.Listener 的实现
|
||
"onDisconnected", "onEvent", "onFrame", "onProgress", "onLog",
|
||
}
|
||
|
||
# 已确认的历史遗留死代码:早期版本的实现,后来换成了别的写法,
|
||
# 函数留着但没人调。不影响功能,列出来是为了有朝一日清掉。
|
||
KNOWN_DEAD = {
|
||
"buildFeatureTab": "旧的分页构建方式,已被 renderGroups() 取代",
|
||
"pickFolder": "已被 pickFile() 取代",
|
||
"pickClipHistory": "功能已移到服务端注册表",
|
||
"confirmWallpaper": "功能已移到服务端注册表",
|
||
"confirmMonitor": "功能已移到服务端注册表",
|
||
"sendMedia": "已被 media() 取代",
|
||
"promptRun": "已被 runFeature() 取代",
|
||
"showInfo": "已被 feature_run 的 info 取代",
|
||
"findSavedToken": "旧的重连逻辑,已被 RelayTransport 取代",
|
||
"glassBackground": "旧的美化辅助函数,样式改成 glass() 后没人用了",
|
||
"sp": "旧的字号辅助,已被 dp() 取代",
|
||
}
|
||
|
||
# 关键路径:这些漏了就是白屏 / 功能全无,必须硬性拦住
|
||
CRITICAL = ("buildConnectPanel", "buildControlPanel", "initTabs",
|
||
"buildMirrorPanel", "buildLanPanel")
|
||
|
||
passed = 0
|
||
failed = 0
|
||
|
||
|
||
def check(name, ok, detail=""):
|
||
global passed, failed
|
||
if ok:
|
||
passed += 1
|
||
print(" [PASS] " + name)
|
||
else:
|
||
failed += 1
|
||
print(" [FAIL] " + name + " " + str(detail))
|
||
|
||
|
||
def defined_methods(text: str) -> list[str]:
|
||
"""找出主程序里定义的方法名。
|
||
|
||
⚠ **带 `@Override` 的跳过**。
|
||
|
||
为什么:这些是**父类/接口回调**,由框架调用,源码里当然只出现一次 ——
|
||
而这个扫描的判据正是"名字只出现 1 次 = 没人调"。
|
||
|
||
踩到的实例:手表端的 `GestureDetector.SimpleOnGestureListener`
|
||
里有 onSingleTapConfirmed / onDoubleTap / onScroll,
|
||
它们由 Android 的触摸框架回调,源码里各只出现一次。
|
||
不跳过的话扫描会把它们报成"未知的未调用方法"(假阳性)——
|
||
而这种假阳性最坏的结果是**有人为了让它变绿,把回调删了**,
|
||
于是手势就失灵了。
|
||
|
||
同类还会遇到的:View.OnClickListener.onClick、
|
||
DialogInterface.OnClickListener.onClick、Runnable.run 等。
|
||
"""
|
||
pattern = re.compile(
|
||
r"(?:private|public|protected|static|final|\s)+"
|
||
r"[\w<>\[\],.\s]+\s+(\w+)\s*\([^)]*\)\s*(?:throws [\w.,\s]+)?\{")
|
||
names = []
|
||
for match in pattern.finditer(text):
|
||
name = match.group(1)
|
||
if name in ("if", "for", "while", "switch", "catch", "synchronized",
|
||
"try", "return", "new"):
|
||
continue
|
||
# 往上看几行,如果有 @Override 就跳过
|
||
head = text[max(0, match.start() - 200):match.start()]
|
||
tail_lines = head.splitlines()[-3:]
|
||
if any("@Override" in ln for ln in tail_lines):
|
||
continue
|
||
names.append(name)
|
||
return names
|
||
|
||
|
||
def main() -> int:
|
||
check("MainActivity.java 在", MAIN.is_file())
|
||
if not MAIN.is_file():
|
||
return 1
|
||
text = MAIN.read_text(encoding="utf-8")
|
||
|
||
print()
|
||
print("一、关键路径必须被调用(漏了就是白屏)")
|
||
# initTabs 专项:白屏的根因就是它
|
||
init_def = len(re.findall(r"private void initTabs\s*\(", text))
|
||
init_use = len(re.findall(r"\binitTabs\s*\(\s*\)", text))
|
||
check("initTabs() 有定义", init_def >= 1, f"定义 {init_def} 处")
|
||
check("initTabs() 被调用(否则连上后一片白屏)",
|
||
init_use >= 2, f"出现 {init_use} 次(1 次是定义,>1 才算被调用)")
|
||
check("initTabs() 紧跟 buildControlPanel()",
|
||
re.search(r"buildControlPanel\(\);\s*(?://[^\n]*\n\s*)*initTabs\(\);",
|
||
text) is not None,
|
||
"必须在 buildControlPanel() 之后立刻调用")
|
||
|
||
for method in CRITICAL:
|
||
n = len(re.findall(r"\b" + method + r"\s*\(", text))
|
||
check(f"{method}() 被调用", n > 1, f"出现 {n} 次")
|
||
|
||
print()
|
||
print("二、分页定义完整性")
|
||
tab_names = re.findall(r'new TabDef\("([^"]+)"', text)
|
||
check("定义了 10 个分页", len(tab_names) == 10, f"实际 {len(tab_names)} 个")
|
||
print(f" 分页: {' '.join(tab_names)}")
|
||
check("selectTab 有定义", "private void selectTab(" in text)
|
||
check("selectTab 被调用", len(re.findall(r"\bselectTab\s*\(", text)) > 1)
|
||
|
||
print()
|
||
print("三、未调用方法扫描")
|
||
names = defined_methods(text)
|
||
orphans = []
|
||
for name in sorted(set(names)):
|
||
if name in WHITELIST or name in KNOWN_DEAD:
|
||
continue
|
||
n = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text))
|
||
if n <= 1:
|
||
orphans.append((name, n))
|
||
|
||
check("没有**未知**的未调用方法", not orphans,
|
||
"\n ".join(f"{n}({c} 次)" for n, c in orphans)
|
||
if orphans else "")
|
||
if orphans:
|
||
print()
|
||
print(" 未知的未调用方法(可能是漏接的事件处理,也可能是新的死代码):")
|
||
for name, cnt in orphans:
|
||
print(f" {name} (出现 {cnt} 次)")
|
||
|
||
if KNOWN_DEAD:
|
||
print()
|
||
print(f" 已知死代码 {len(KNOWN_DEAD)} 个(不影响功能,待清理):")
|
||
for name, why in sorted(KNOWN_DEAD.items()):
|
||
still = len(re.findall(r"\b" + re.escape(name) + r"\s*\(", text))
|
||
mark = "" if still else " [已删除]"
|
||
print(f" {name}: {why}{mark}")
|
||
|
||
print()
|
||
print("=" * 58)
|
||
print(f"通过 {passed} 项,失败 {failed} 项")
|
||
return 0 if failed == 0 else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|