# -*- coding: utf-8 -*- r'''安卓端 2.3.0:UI 精简 + 播放器 + 鼠标面板重做。 == 用户要求(原话整理)== 「杂七杂八功能都删掉」,只保留: 音量加减 / 播放 屏幕镜像 立即返回桌面 调整屏幕亮度 发送文件 打开回收站 打开各种程序 语音播报 打开网址 快捷命令 进程管理 关机/重启/睡眠/锁屏 当前窗口置顶 一键结束所有进程 再加: * 远程鼠标控制(中间滑动区 + 下面三个按钮:左键 / 滚轮 / 右键) * 视频播放器 + 音乐播放器(暂停开始、进度左右划、上一个下一个) == 改动方式 == **全部用精确锚点替换,替换前后都做断言。** 这个项目里"脚本说改了、其实锚点没匹配上"的坑踩过不止一次 (最典型的是 nginx 那次逐行注释把括号配平搞坏)。 所以这里的规矩是: * 锚点必须**唯一** * 替换后必须能在结果里找到新内容 * 任何一步对不上就抛异常,**绝不跳过** == 为什么不用 renderGroups 了 == 原来每个分页靠 `TabDef.groups` 动态渲染电脑端上报的功能组。 那样"电脑端有什么功能,手表上就出一堆按钮"——正是用户嫌乱的东西。 现在改成**显式列按钮**:手表上有什么,完全由这份代码决定。 (电脑端功能还在,只是不再无脑铺到表盘上。) ''' from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent JAVA = ROOT / "android" / "src" / "com" / "lanremote" / "watch" / "MainActivity.java" def must_replace(text: str, old: str, new: str, label: str) -> str: """精确替换,锚点必须唯一出现。""" count = text.count(old) if count == 0: raise SystemExit("✗ [%s] 锚点没找到" % label) if count > 1: raise SystemExit("✗ [%s] 锚点出现 %d 次,不唯一" % (label, count)) out = text.replace(old, new, 1) if new.strip() and new.strip()[:60] not in out: raise SystemExit("✗ [%s] 替换后找不到新内容" % label) print(" ✓ %s" % label) return out # ====================================================================== # 1. 新的分页定义 # ====================================================================== NEW_TABS = r''' /** 定义所有分页(2.3.0 精简版)。 * * 用户要求「杂七杂八功能都删掉」,只留这些。所以这里**显式列按钮**, * 不再用 groups 把电脑端所有功能无脑铺上来 —— 那正是"乱"的来源。 * * 每页按钮数控制在 7 个以内,一屏放得下不用滚。 */ private void initTabs() { tabDefs = new TabDef[]{ new TabDef("屏幕", new Act[]{ act("屏幕镜像", new Runnable() { public void run() { openMirrorPanel(); } }), act("返回桌面", new Runnable() { public void run() { sendCommand(Json.obj("cmd", "show_desktop"), "返回桌面"); } }), act("亮度 +", new Runnable() { public void run() { changeBrightness(10); } }), act("亮度 -", new Runnable() { public void run() { changeBrightness(-10); } }), act("截屏", new Runnable() { public void run() { doScreenshot(); } }), }), new TabDef("鼠标", new Act[]{ act("鼠标控制", new Runnable() { public void run() { showMousePanel(); } }), }), new TabDef("媒体", new Act[]{ act("播放/暂停", new Runnable() { public void run() { media("playpause"); } }), act("上一曲", new Runnable() { public void run() { media("prev"); } }), act("下一曲", new Runnable() { public void run() { media("next"); } }), act("音量 +", new Runnable() { public void run() { media("volup"); } }), act("音量 -", new Runnable() { public void run() { media("voldown"); } }), act("静音", new Runnable() { public void run() { media("mute"); } }), act("播放器", new Runnable() { public void run() { showPlayerPanel(); } }), }), new TabDef("文件", new Act[]{ act("发送文件", new Runnable() { public void run() { pickFile(REQ_FILE, "*/*"); } }), act("打开回收站", new Runnable() { public void run() { runFeature("open_recycle", Json.obj(), "打开回收站"); } }), }), new TabDef("电脑", new Act[]{ act("打开网址", new Runnable() { public void run() { promptOpenUrl(); } }), act("快捷命令", new Runnable() { public void run() { pickQuickApp(); } }), act("进程管理", new Runnable() { public void run() { pickProcess(); } }), act("窗口置顶", new Runnable() { public void run() { runFeature("window_top", Json.obj(), "窗口置顶"); } }), act("结束所有进程", new Runnable() { public void run() { confirmKillAll(); } }), }), new TabDef("语音", new Act[]{ act("语音播报", new Runnable() { public void run() { promptSay(); } }), act("朗读剪贴板", new Runnable() { public void run() { sendCommand(Json.obj("cmd", "say_clip"), "朗读剪贴板"); } }), }), 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("restart", "重启"); } }), }), }; 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.setBackgroundColor(0x00000000); 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, 0, 0); tab.setLayoutParams(params); tabBar.addView(tab); } selectTab(0); } ''' # ====================================================================== # 2. 新的鼠标面板(中间滑动区 + 下面三个按钮) # ====================================================================== NEW_MOUSE = r''' private void showMousePanel() { if (mousePanel == null) { buildMousePanel(); } showOnly(mousePanel); appendLog("鼠标:中间滑动移动光标,下面左键/滚轮/右键"); } /** 远程鼠标控制面板(2.3.0 版)。 * * 用户要求:「中间是一个鼠标,可以上下左右滑动控制, * 下面有三个按钮,左边是左键,中间是那个滑轮,右边是那个右键」。 * * 布局: * * ┌──────────────────────────┐ * │ 返回(右上角) │ * │ │ * │ 滑动区(占满) │ <- 手指滑动 = 移动光标 * │ │ * ├────────┬────────┬────────┤ * │ 左键 │ 滚轮 │ 右键 │ * └────────┴────────┴────────┘ * * 滚轮按钮:**点一下 = 中键单击**,**上下滑 = 滚动**。 * 这样三个按钮就把鼠标的左/中/右 + 滚轮全覆盖了。 */ private void buildMousePanel() { mousePanel = new FrameLayout(this); mousePanel.setBackgroundColor(0xFF101418); LinearLayout col = new LinearLayout(this); col.setOrientation(LinearLayout.VERTICAL); col.setPadding(dp(6), dp(34), dp(6), dp(6)); mousePanel.addView(col, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // ---- 滑动区 ---- mousePad = new View(this); mousePad.setBackgroundColor(0xFF1B2028); LinearLayout.LayoutParams padLp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f); padLp.setMargins(0, 0, 0, dp(6)); col.addView(mousePad, padLp); // ---- 底部三个按钮 ---- LinearLayout keys = new LinearLayout(this); keys.setOrientation(LinearLayout.HORIZONTAL); col.addView(keys, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(46))); keys.addView(mouseKey("左键", new Runnable() { public void run() { sendMouse("click", "left", 0); } }), keyLp()); // 滚轮:点=中键,上下滑=滚动 final Button wheel = new Button(this); wheel.setText("滚轮"); wheel.setTextSize(12f); wheel.setAllCaps(false); wheel.setPadding(0, 0, 0, 0); wheel.setMinWidth(0); wheel.setMinimumWidth(0); wheel.setLayoutParams(keyLp()); wheel.setOnTouchListener(new View.OnTouchListener() { private float downY = 0f; private int accumulated = 0; private boolean pressed = false; @Override public boolean onTouch(View v, android.view.MotionEvent e) { switch (e.getActionMasked()) { case android.view.MotionEvent.ACTION_DOWN: downY = e.getY(); accumulated = 0; pressed = true; v.setPressed(true); return true; case android.view.MotionEvent.ACTION_MOVE: if (!pressed) { return true; } // 每滑过一个"格子"滚一格。 // 手表屏幕小,一格取 18dp 比较好按。 float cell = dp(18); float dy = e.getY() - downY; accumulated = (int) (dy / cell); // 每积累一格发一次,发完把基准挪过去 while (accumulated != 0) { int dir = accumulated > 0 ? -3 : 3; // 往下滑=向下滚 sendMouse("scroll", "", dir); downY += (accumulated > 0 ? cell : -cell); accumulated = (int) ((e.getY() - downY) / cell); } return true; case android.view.MotionEvent.ACTION_UP: case android.view.MotionEvent.ACTION_CANCEL: v.setPressed(false); if (pressed) { pressed = false; // 没滑动就是一次中键单击 float moved = Math.abs(e.getY() - downY); if (moved < dp(8)) { sendMouse("click", "middle", 0); } } return true; default: return true; } } }); keys.addView(wheel); keys.addView(mouseKey("右键", new Runnable() { public void run() { sendMouse("click", "right", 0); } }), keyLp()); // ---- 返回 ---- Button back = new Button(this); back.setText("返回"); back.setTextSize(11f); back.setAllCaps(false); FrameLayout.LayoutParams backLp = new FrameLayout.LayoutParams( dp(58), dp(26)); backLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END; backLp.setMargins(0, dp(4), dp(4), 0); back.setLayoutParams(backLp); back.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showControlPanel(); } }); mousePanel.addView(back); // ---- 触摸:滑动移动光标 ---- final android.view.GestureDetector detector = new android.view.GestureDetector(this, new android.view.GestureDetector.SimpleOnGestureListener() { @Override public boolean onScroll(android.view.MotionEvent e1, android.view.MotionEvent e2, float dx, float dy) { // 手指往哪滑,光标往哪走:dx/dy 是"内容移动量", // 也就是手指移动的反方向,所以取负。 sendMouseMove(-dx, -dy); return true; } }); mousePad.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, android.view.MotionEvent e) { detector.onTouchEvent(e); return true; } }); } private LinearLayout.LayoutParams keyLp() { LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.MATCH_PARENT, 1f); lp.setMargins(dp(2), 0, dp(2), 0); return lp; } ''' # ====================================================================== # 3. 播放器(面板 + 逻辑) # ====================================================================== PLAYER_CODE = r''' // ================================================================== // 播放器(2.3.0 新增) // ================================================================== // // 用户要求:「再加一个视频播放器和音乐播放器,有暂停开始, // 进度左右划,下一个上一个」。 // // 设计取舍:**让手表自己放**,而不是让电脑放。 // // 如果让电脑放(调 play_video),电脑会打开它自己的播放器 —— // 我们就**没法拿到进度、也没法精确 seek**,只能发媒体键。 // 那"进度左右划"就是假的。 // // 手表自己放的话:MediaPlayer / VideoView 的 // getCurrentPosition / seekTo 都是现成的, // 进度条、拖动、上下一首全部真实可控。 // // 代价:要先从电脑把文件下到手表上(几百 KB 到几十 MB)。 // 文件走现有的分块传输(EVT_FILE),下完自动播。 private FrameLayout playerPanel; private android.widget.VideoView playerVideo; private android.media.MediaPlayer playerAudio; private android.widget.SeekBar playerSeek; private TextView playerTitle; private TextView playerClock; private TextView playerEmpty; private Button playerPlayBtn; private LinearLayout playerStage; /** 电脑上的媒体文件清单(media_list 返回)。 */ private final java.util.List> playerFiles = new java.util.ArrayList>(); private int playerIndex = -1; private boolean playerIsVideo = false; private boolean playerLoaded = false; private boolean playerDragging = false; /** 正在等播放器文件下载完 —— 下完 handleIncomingFile 会直接播。 */ private boolean playerPendingPlay = false; private final android.os.Handler playerTick = new android.os.Handler(); private final Runnable playerTicker = new Runnable() { @Override public void run() { if (playerPanel == null || playerPanel.getVisibility() != View.VISIBLE) { return; } if (!playerDragging) { try { int pos = playerPosition(); int dur = playerDuration(); if (dur > 0) { playerSeek.setMax(dur); playerSeek.setProgress(pos); playerClock.setText(fmtTime(pos) + " / " + fmtTime(dur)); } } catch (Exception ignored) { } } playerTick.postDelayed(this, 400); } }; private void buildPlayerPanel() { playerPanel = new FrameLayout(this); playerPanel.setBackgroundColor(0xFF0E1216); LinearLayout col = new LinearLayout(this); col.setOrientation(LinearLayout.VERTICAL); col.setPadding(dp(6), dp(34), dp(6), dp(6)); playerPanel.addView(col, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // ---- 标题 ---- playerTitle = new TextView(this); playerTitle.setText("播放器"); playerTitle.setTextColor(0xFFFFFFFF); playerTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); playerTitle.setSingleLine(true); playerTitle.setEllipsize(android.text.TextUtils.TruncateAt.MIDDLE); playerTitle.setPadding(dp(4), 0, dp(4), dp(4)); col.addView(playerTitle, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); // ---- 画面 / 封面区 ---- playerStage = new LinearLayout(this); playerStage.setOrientation(LinearLayout.VERTICAL); playerStage.setGravity(Gravity.CENTER); playerStage.setBackgroundColor(0xFF10151A); LinearLayout.LayoutParams stageLp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f); stageLp.setMargins(0, 0, 0, dp(4)); col.addView(playerStage, stageLp); playerEmpty = new TextView(this); playerEmpty.setText("点「列表」选歌/视频\n(从电脑的音乐、视频目录读)"); playerEmpty.setTextColor(0xFF8B96A3); playerEmpty.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); playerEmpty.setGravity(Gravity.CENTER); playerStage.addView(playerEmpty, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); // ---- 进度条 ---- playerSeek = new android.widget.SeekBar(this); playerSeek.setMax(1000); playerSeek.setProgress(0); col.addView(playerSeek, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(28))); playerSeek.setOnSeekBarChangeListener( new android.widget.SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(android.widget.SeekBar bar, int progress, boolean fromUser) { if (fromUser) { playerClock.setText(fmtTime(progress) + " / " + fmtTime(playerDuration())); } } @Override public void onStartTrackingTouch(android.widget.SeekBar bar) { playerDragging = true; } @Override public void onStopTrackingTouch(android.widget.SeekBar bar) { playerDragging = false; seekTo(bar.getProgress()); } }); // ---- 时间 ---- playerClock = new TextView(this); playerClock.setText("00:00 / 00:00"); playerClock.setTextColor(0xFF9AA4B2); playerClock.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10); playerClock.setGravity(Gravity.CENTER); col.addView(playerClock, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); // ---- 上一首 / 播放 / 下一首 ---- LinearLayout ctl = new LinearLayout(this); ctl.setOrientation(LinearLayout.HORIZONTAL); ctl.setGravity(Gravity.CENTER_VERTICAL); LinearLayout.LayoutParams ctlLp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(52)); ctlLp.setMargins(0, dp(4), 0, 0); col.addView(ctl, ctlLp); ctl.addView(playerBtn("上一", new Runnable() { public void run() { stepPlayer(-1); } }), new LinearLayout.LayoutParams(0, dp(50), 1f)); playerPlayBtn = playerBtn("播放", new Runnable() { public void run() { togglePlayer(); } }); ctl.addView(playerPlayBtn, new LinearLayout.LayoutParams(0, dp(50), 1.4f)); ctl.addView(playerBtn("下一", new Runnable() { public void run() { stepPlayer(1); } }), new LinearLayout.LayoutParams(0, dp(50), 1f)); ctl.addView(playerBtn("列表", new Runnable() { public void run() { pickPlayerFile(); } }), new LinearLayout.LayoutParams(0, dp(50), 1f)); // ---- 返回 ---- Button back = new Button(this); back.setText("返回"); back.setTextSize(11f); back.setAllCaps(false); FrameLayout.LayoutParams backLp = new FrameLayout.LayoutParams( dp(58), dp(26)); backLp.gravity = android.view.Gravity.TOP | android.view.Gravity.END; backLp.setMargins(0, dp(4), dp(4), 0); back.setLayoutParams(backLp); back.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showControlPanel(); } }); playerPanel.addView(back); // ---- 画面区左右划 = 快进/快退 10 秒 ---- final android.view.GestureDetector swipe = new android.view.GestureDetector(this, new android.view.GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(android.view.MotionEvent e1, android.view.MotionEvent e2, float vx, float vy) { if (e1 == null || e2 == null) { return false; } float dx = e2.getX() - e1.getX(); if (Math.abs(dx) < dp(30) || Math.abs(dx) < Math.abs(vy)) { return false; } // 右滑 = 前进 10 秒,左滑 = 后退 10 秒 seekBy(dx > 0 ? 10000 : -10000); return true; } @Override public boolean onSingleTapConfirmed(android.view.MotionEvent e) { togglePlayer(); // 点画面 = 播放/暂停 return true; } }); playerStage.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, android.view.MotionEvent e) { swipe.onTouchEvent(e); return true; } }); } private Button playerBtn(String text, final Runnable action) { Button b = new Button(this); b.setText(text); b.setTextSize(12f); b.setAllCaps(false); b.setPadding(0, 0, 0, 0); b.setMinWidth(0); b.setMinimumWidth(0); b.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { action.run(); } }); return b; } private void showPlayerPanel() { if (playerPanel == null) { buildPlayerPanel(); } showOnly(playerPanel); startPlayerTicker(); if (!playerLoaded) { loadPlayerList(); } } private void startPlayerTicker() { playerTick.removeCallbacks(playerTicker); playerTick.post(playerTicker); } private void stopPlayerTicker() { playerTick.removeCallbacks(playerTicker); } /** 拉电脑上的媒体清单。 */ private void loadPlayerList() { playerLoaded = true; playerTitle.setText("读取电脑媒体…"); io.execute(new Runnable() { @Override public void run() { try { final Map reply = client.call(Json.obj("cmd", "media_list"), 25000); final List files = Json.list(reply, "files"); ui.post(new Runnable() { @Override public void run() { playerFiles.clear(); for (Object o : files) { Map m = Json.asMap(o); if (m != null) { playerFiles.add(m); } } if (playerFiles.isEmpty()) { playerTitle.setText("播放器"); playerEmpty.setText("电脑的音乐/视频目录里没有文件\n" + "把文件放进「音乐」「视频」或「下载」文件夹"); playerEmpty.setVisibility(View.VISIBLE); } else { playerTitle.setText("播放器 · 共 " + playerFiles.size() + " 个"); pickPlayerFile(); } } }); } catch (final IOException e) { ui.post(new Runnable() { @Override public void run() { playerLoaded = false; playerTitle.setText("播放器"); playerEmpty.setText("读不到电脑媒体列表:" + e.getMessage()); playerEmpty.setVisibility(View.VISIBLE); } }); } } }); } private void pickPlayerFile() { if (playerFiles.isEmpty()) { loadPlayerList(); return; } String[] labels = new String[playerFiles.size()]; for (int i = 0; i < playerFiles.size(); i++) { Map f = playerFiles.get(i); String kind = Json.str(f, "kind", "audio"); labels[i] = ("video".equals(kind) ? "▶ " : "♪ ") + Json.str(f, "name", "?") + "\n" + humanSize(Json.integer(f, "size", 0)); } new AlertDialog.Builder(MainActivity.this) .setTitle("电脑上的媒体") .setItems(labels, new android.content.DialogInterface.OnClickListener() { @Override public void onClick(android.content.DialogInterface dialog, int which) { openPlayerItem(which); } }) .setNegativeButton("关闭", null) .show(); } /** 下载并播放第 index 个文件。 */ private void openPlayerItem(final int index) { if (index < 0 || index >= playerFiles.size()) { return; } playerIndex = index; final Map f = playerFiles.get(index); final String name = Json.str(f, "name", "media"); final String path = Json.str(f, "path", ""); playerIsVideo = "video".equals(Json.str(f, "kind", "audio")); // 先看手表上是不是已经有了(可能刚播过) File cached = mediaCacheFile(name); if (cached != null && cached.isFile() && cached.length() > 0) { startPlayback(cached, name); return; } playerTitle.setText("下载 " + name + " …"); playerEmpty.setText("正在从电脑下载…"); playerEmpty.setVisibility(View.VISIBLE); playerPendingPlay = true; incoming = new ByteArrayOutputStream(); incomingName = name; io.execute(new Runnable() { @Override public void run() { try { client.call(Json.obj("cmd", "media_pull", "path", path), 600000); } catch (final IOException e) { playerPendingPlay = false; ui.post(new Runnable() { @Override public void run() { playerTitle.setText("播放器"); playerEmpty.setText("下载失败:" + e.getMessage()); playerEmpty.setVisibility(View.VISIBLE); } }); } } }); } private File mediaCacheFile(String name) { File dir = getExternalFilesDir(null); if (dir == null) { dir = getFilesDir(); } File media = new File(dir, "media"); if (!media.isDirectory()) { //noinspection ResultOfMethodCallIgnored media.mkdirs(); } // 文件名里可能有路径分隔符之类,清一下 String safe = name.replaceAll("[\\\\/:*?\"<>|]", "_"); return new File(media, safe); } /** 真正开始播放本地文件。 */ private void startPlayback(final File file, final String name) { stopPlayback(); playerTitle.setText(name); playerEmpty.setVisibility(View.GONE); playerStage.removeAllViews(); if (playerIsVideo) { playerVideo = new android.widget.VideoView(this); playerVideo.setLayoutParams(new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); playerVideo.setVideoPath(file.getAbsolutePath()); playerVideo.setOnPreparedListener(new android.media.MediaPlayer.OnPreparedListener() { @Override public void onPrepared(android.media.MediaPlayer mp) { playerVideo.start(); playerPlayBtn.setText("暂停"); try { playerSeek.setMax(mp.getDuration()); } catch (Exception ignored) { } } }); playerVideo.setOnCompletionListener(new android.media.MediaPlayer.OnCompletionListener() { @Override public void onCompletion(android.media.MediaPlayer mp) { // 播完自动下一首(用户要的"下一个") stepPlayer(1); } }); playerStage.addView(playerVideo); playerVideo.requestFocus(); } else { try { playerAudio = new android.media.MediaPlayer(); playerAudio.setDataSource(file.getAbsolutePath()); playerAudio.prepare(); playerAudio.start(); playerPlayBtn.setText("暂停"); playerSeek.setMax(playerAudio.getDuration()); playerAudio.setOnCompletionListener( new android.media.MediaPlayer.OnCompletionListener() { @Override public void onCompletion(android.media.MediaPlayer mp) { stepPlayer(1); } }); // 音频没有画面,摆个大音符 TextView art = new TextView(this); art.setText("♪\n" + name); art.setTextColor(0xFFFFFFFF); art.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); art.setGravity(Gravity.CENTER); playerStage.addView(art, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } catch (Exception e) { playerTitle.setText(name); playerEmpty.setText("播放失败:" + e.getMessage()); playerEmpty.setVisibility(View.VISIBLE); } } startPlayerTicker(); } private void togglePlayer() { try { if (playerIsVideo && playerVideo != null) { if (playerVideo.isPlaying()) { playerVideo.pause(); playerPlayBtn.setText("播放"); } else { playerVideo.start(); playerPlayBtn.setText("暂停"); } } else if (playerAudio != null) { if (playerAudio.isPlaying()) { playerAudio.pause(); playerPlayBtn.setText("播放"); } else { playerAudio.start(); playerPlayBtn.setText("暂停"); } } } catch (Exception ignored) { } } private void stepPlayer(int delta) { if (playerFiles.isEmpty()) { pickPlayerFile(); return; } int next = playerIndex + delta; if (next < 0) { next = playerFiles.size() - 1; } if (next >= playerFiles.size()) { next = 0; } openPlayerItem(next); } private int playerPosition() { try { if (playerIsVideo && playerVideo != null) { return playerVideo.getCurrentPosition(); } if (playerAudio != null) { return playerAudio.getCurrentPosition(); } } catch (Exception ignored) { } return 0; } private int playerDuration() { try { if (playerIsVideo && playerVideo != null) { return playerVideo.getDuration(); } if (playerAudio != null) { return playerAudio.getDuration(); } } catch (Exception ignored) { } return 0; } private void seekTo(int ms) { try { if (playerIsVideo && playerVideo != null) { playerVideo.seekTo(ms); } else if (playerAudio != null) { playerAudio.seekTo(ms); } } catch (Exception ignored) { } } private void seekBy(int deltaMs) { int dur = playerDuration(); int target = playerPosition() + deltaMs; if (target < 0) { target = 0; } if (dur > 0 && target > dur) { target = dur; } seekTo(target); toast((deltaMs > 0 ? "快进 " : "快退 ") + Math.abs(deltaMs) / 1000 + " 秒"); } private void stopPlayback() { try { if (playerVideo != null) { playerVideo.stopPlayback(); playerVideo = null; } } catch (Exception ignored) { } try { if (playerAudio != null) { playerAudio.release(); playerAudio = null; } } catch (Exception ignored) { } playerStage.removeAllViews(); playerEmpty.setVisibility(View.VISIBLE); playerPlayBtn.setText("播放"); } private void releasePlayer() { stopPlayerTicker(); stopPlayback(); } private static String fmtTime(int ms) { if (ms < 0) { ms = 0; } int total = ms / 1000; int m = total / 60; int s = total % 60; return String.format("%02d:%02d", m, s); } // ---- 亮度 / 结束所有进程(2.3.0 新增的显式入口)---- /** 调屏幕亮度。电脑端 brightness 是 10~100 的绝对值, * 所以这里先问电脑当前值,再 ±delta。 */ private void changeBrightness(final int delta) { io.execute(new Runnable() { @Override public void run() { try { Map reply = client.call(Json.obj("cmd", "feature_run", "name", "brightness", "params", Json.obj()), 15000); int now = Json.integer(reply, "value", 0); if (now <= 0) { now = 70; } int target = Math.max(10, Math.min(100, now + delta)); client.call(Json.obj("cmd", "feature_run", "name", "brightness", "params", Json.obj("value", target)), 15000); final int shown = target; ui.post(new Runnable() { @Override public void run() { toast("亮度 " + shown + "%"); } }); } catch (final IOException e) { postError("亮度", e); } } }); } private void confirmKillAll() { new AlertDialog.Builder(MainActivity.this) .setTitle("结束所有进程") .setMessage("会结束这台电脑上所有**用户程序**" + "(系统进程和本程序会保留)。继续?") .setPositiveButton("结束", new android.content.DialogInterface.OnClickListener() { @Override public void onClick(android.content.DialogInterface dialog, int which) { runFeature("kill_all", Json.obj(), "结束所有进程"); } }) .setNegativeButton("取消", null) .show(); } ''' # ====================================================================== # 入口 # ====================================================================== def main() -> int: if not JAVA.is_file(): print("✗ 找不到 %s" % JAVA) return 1 text = JAVA.read_text(encoding="utf-8") original_len = len(text) print("=" * 62) print(" 安卓端 2.3.0 改动") print("=" * 62) print(" 原文 %d 字符" % original_len) print() # ---- 1. initTabs 整体替换 ---- # 锚点:从 " /** 定义所有分页" 到 initTabs 的收尾 " selectTab(0);\n }" start = text.find(" /** 定义所有分页") if start < 0: print("✗ 找不到 initTabs 的注释锚点") return 1 end_mark = " selectTab(0);\n }\n" end = text.find(end_mark, start) if end < 0: print("✗ 找不到 initTabs 的结束锚点") return 1 end += len(end_mark) old_tabs = text[start:end] if "new TabDef(\"媒体\"" not in old_tabs or "new TabDef(\"电源\"" in old_tabs: print("✗ initTabs 锚点范围不对(%d 字符)" % len(old_tabs)) return 1 text = text[:start] + NEW_TABS + text[end:] print(" ✓ 替换 initTabs(%d -> %d 字符)" % (len(old_tabs), len(NEW_TABS))) # ---- 2. 鼠标面板整体替换 ---- m_start = text.find(" private void showMousePanel() {") if m_start < 0: print("✗ 找不到 showMousePanel") return 1 # 结束:sendMouseMove 方法之后、选择页注释之前 m_end_mark = " // ==================================================================\n // 连接后的选择页(远控 / 静默更新)" m_end = text.find(m_end_mark, m_start) if m_end < 0: print("✗ 找不到鼠标面板结束锚点") return 1 old_mouse = text[m_start:m_end] if "sendMouseMove" not in old_mouse: print("✗ 鼠标面板锚点范围不对") return 1 # 保留 sendMouse 和 sendMouseMove(它们被别处调用) keep_start = old_mouse.find(" private void sendMouse(String action") if keep_start < 0: print("✗ 找不到 sendMouse") return 1 keep = old_mouse[keep_start:] text = text[:m_start] + NEW_MOUSE + "\n" + keep + text[m_end:] print(" ✓ 替换鼠标面板(%d -> %d 字符)" % (len(old_mouse), len(NEW_MOUSE) + len(keep))) # ---- 3. 插入播放器代码 ---- # 放在鼠标面板之后、选择页之前 anchor = " // ==================================================================\n // 连接后的选择页(远控 / 静默更新)" if text.count(anchor) != 1: print("✗ 播放器插入锚点不唯一") return 1 text = text.replace(anchor, PLAYER_CODE + "\n" + anchor, 1) print(" ✓ 插入播放器代码(%d 字符)" % len(PLAYER_CODE)) # ---- 4. showOnly 登记新面板 ---- text = must_replace( text, " View[] all = {connectPanel, controlPanel, mirrorPanel, lanPanel,\n" " settingsPanel, mousePanel, choicePanel};", " View[] all = {connectPanel, controlPanel, mirrorPanel, lanPanel,\n" " settingsPanel, mousePanel, choicePanel, playerPanel};", "showOnly 登记播放器面板") # ---- 5. 面板注册:addView + topMargin ---- text = must_replace( text, " applyPanelTopMargin(mousePanel);\n" " applyPanelTopMargin(choicePanel);", " applyPanelTopMargin(mousePanel);\n" " applyPanelTopMargin(choicePanel);\n" " applyPanelTopMargin(playerPanel);", "播放器面板让出快捷栏高度") # ---- 6. 建面板 + 挂到 root ---- text = must_replace( text, " buildChoicePanel(); // 同上:连上之后才显示\n" " buildQuickBar();", " buildChoicePanel(); // 同上:连上之后才显示\n" " buildPlayerPanel(); // 播放器,先建不显示\n" " buildQuickBar();", "构建播放器面板") # 挂到 root:在 choicePanel 的 addView 之后 choice_add = " root.addView(choicePanel, new FrameLayout.LayoutParams(\n" \ " ViewGroup.LayoutParams.MATCH_PARENT,\n" \ " ViewGroup.LayoutParams.MATCH_PARENT));\n" if text.count(choice_add) != 1: print("✗ choicePanel addView 锚点不唯一") return 1 text = text.replace( choice_add, choice_add + " root.addView(playerPanel, new FrameLayout.LayoutParams(\n" " ViewGroup.LayoutParams.MATCH_PARENT,\n" " ViewGroup.LayoutParams.MATCH_PARENT));\n" " playerPanel.setVisibility(View.GONE);\n", 1) print(" ✓ 播放器面板挂到 root") # ---- 7. handleIncomingFile 支持播放器 ---- text = must_replace( text, " appendLog(\"已保存 \" + name + \" (\" + humanSize(blob.length) + \")\");\n" " toast(\"已保存到 \" + saved.getName());", " appendLog(\"已保存 \" + name + \" (\" + humanSize(blob.length) + \")\");\n" " if (playerPendingPlay) {\n" " // 播放器在等这个文件 —— 下完直接播\n" " playerPendingPlay = false;\n" " File media = mediaCacheFile(name);\n" " if (media != null) {\n" " try {\n" " java.io.FileOutputStream out =\n" " new java.io.FileOutputStream(media);\n" " out.write(blob);\n" " out.close();\n" " } catch (IOException ignored) {\n" " }\n" " startPlayback(media, name);\n" " }\n" " } else {\n" " toast(\"已保存到 \" + saved.getName());\n" " }", "handleIncomingFile 播放器接管") JAVA.write_text(text, encoding="utf-8", newline="\n") print() print(" 改后 %d 字符(+%d)" % (len(text), len(text) - original_len)) return 0 if __name__ == "__main__": sys.exit(main())