mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
手表端 - 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源) - 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、 剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等 - 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘 - 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮 滚轮点按=中键、上下滑=滚动 - 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、 点画面播放暂停、上一/下一、播完自动下一首 电脑端 - 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身 - 新增 media_list / media_pull(播放器数据源) 安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过 中转站 - known_devices 从 ID 集合改成「设备ID -> 配对码」映射 - 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉 - 老格式 state 文件平滑升级
1172 lines
47 KiB
Python
1172 lines
47 KiB
Python
# -*- 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<java.util.Map<String, Object>> playerFiles =
|
||
new java.util.ArrayList<java.util.Map<String, Object>>();
|
||
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<String, Object> reply =
|
||
client.call(Json.obj("cmd", "media_list"), 25000);
|
||
final List<Object> files = Json.list(reply, "files");
|
||
ui.post(new Runnable() {
|
||
@Override
|
||
public void run() {
|
||
playerFiles.clear();
|
||
for (Object o : files) {
|
||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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())
|