package com.lanremote.watch;
import android.Manifest;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.OpenableColumns;
import android.text.InputType;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 手表控制端主界面。
*
* 界面按 490x602 的小屏手表定制:两列大按钮、最小 58dp 点击高度、
* 15sp 以上字号,整页可滚动,保证在极小屏幕上也能准确点按。
*/
public class MainActivity extends Activity implements RemoteClient.Listener {
// ---------------- 配色 ----------------
// ---- 浅色配色 ----
private static final int C_BG = 0xFFF5F7FA; // 页面底色(浅灰)
private static final int C_CARD = 0xFFFFFFFF; // 卡片 / 按钮底色
private static final int C_FG = 0xFF1F2328; // 主文字(近黑)
private static final int C_ACCENT = 0xFF185CE8; // 与图标一致的蓝
private static final int C_OK = 0xFF1A7F37;
private static final int C_WARN = 0xFF9A6700;
private static final int C_DANGER = 0xFFCF222E;
private static final int C_DIM = 0xFF656D76;
private static final int C_BORDER = 0xFFD0D7DE; // 描边
// ---- 液态玻璃 ----
private static final int GLASS_BG_TOP = 0xFFE8F0FF; // 背景渐变·上
private static final int GLASS_BG_MID = 0xFFF1EBFF; // 背景渐变·中
private static final int GLASS_BG_BOT = 0xFFFFF0F6; // 背景渐变·下
private static final int GLASS_FILL = 0xB3FFFFFF; // 玻璃面(70% 白)
private static final int GLASS_FILL_SOFT = 0x8CFFFFFF; // 更透的玻璃
private static final int GLASS_FILL_SOLID = 0xE6FFFFFF; // 实一点(输入框/日志)
private static final int GLASS_EDGE = 0x99FFFFFF; // 玻璃亮边
private static final int GLASS_EDGE_DIM = 0x40FFFFFF; // 弱一点的边
/** 中转服务器地址(跨网络用)。空 = 只走局域网。 */
/**
* 内置的默认中转地址 —— **默认留空**,由用户自己在「中转设置」里填。
*
*
为什么不再内置一个:原来写死了一个公网隧道域名,结果
* 用户的部署/域名一变,写死的地址就成了错的,App 装完就一直在
* 连一个不存在的主机。留空反而干净。
*
*
留空**不影响局域网**:广播发现、填 IP 直连、首页已配对电脑
* 一键连,全都不依赖这个值。
*/
private static final String DEFAULT_RELAY_URL = "";
/**
* 老版本硬塞进来的中转地址,要清掉。
*
*
1.5.0 那版把作者的域名写死成默认值:
*
* DEFAULT_RELAY_URL 这个常量写的是 https://relay.xxmxxs.top
*
* 而且第一次运行就存进了 SharedPreferences。装新 APK 不会清偏好,
* 所以用户会看到「默认还是那个中转站」—— 其实他从来没选过。
*
* 现在默认值是空串(跨网络要自己填)。这里做一次性迁移:
* 存的值如果正好是当时内置的那个域名,就当成没设过,清掉。
*
*
⚠ 只在**完全等于**内置值时清 —— 用户自己手填的地址不能动。
*/
private static final String[] LEGACY_BAKED_IN_RELAYS = {
"https://relay.xxmxxs.top",
"http://relay.xxmxxs.top",
};
private void migrateLegacyRelay() {
String saved = getSharedPreferences("lanremote", MODE_PRIVATE)
.getString("relay_url", "");
if (saved == null) {
return;
}
String trimmed = saved.trim();
for (String legacy : LEGACY_BAKED_IN_RELAYS) {
if (legacy.equalsIgnoreCase(trimmed)) {
// 这是老版本硬塞的,不是用户选的 —— 清掉,让他自己填
getSharedPreferences("lanremote", MODE_PRIVATE).edit()
.remove("relay_url").apply();
appendLog("已清掉旧版本内置的中转地址(不是你自己填的)");
return;
}
}
}
private String relayUrl() {
String saved = getSharedPreferences("lanremote", MODE_PRIVATE)
.getString("relay_url", DEFAULT_RELAY_URL);
// 用户没填过就返回内置默认值(现在是空字符串 = 只走局域网)。
// 注意:这里返回空是**正常状态**,不是错误 ——
// 调用方都用 isEmpty() 判断要不要走中转,空了就老老实实走局域网。
return (saved == null || saved.trim().isEmpty())
? DEFAULT_RELAY_URL : saved.trim();
}
private void setRelayUrl(String url) {
getSharedPreferences("lanremote", MODE_PRIVATE).edit()
.putString("relay_url", url == null ? "" : url.trim())
.apply();
}
/** 让用户填中转地址。填了就能跨网络控制。 */
private void promptRelayUrl() {
final EditText input = new EditText(this);
input.setHint("https://relay.你的域名");
input.setText(relayUrl());
input.setTextColor(C_FG);
input.setHintTextColor(C_DIM);
input.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
input.setSingleLine(true);
input.setBackground(rounded(C_CARD, 7));
input.setPadding(dp(10), dp(10), dp(10), dp(10));
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setPadding(dp(14), dp(8), dp(14), dp(4));
TextView hint = new TextView(this);
hint.setText("填了就走中转服务器,能在任意网络下控制电脑。\n"
+ "留空则只在同一个局域网内可用。");
hint.setTextColor(C_DIM);
hint.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);
hint.setPadding(0, 0, 0, dp(8));
box.addView(hint);
box.addView(input, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
new AlertDialog.Builder(this)
.setTitle("中转服务器地址")
.setView(box)
.setPositiveButton("保存", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
String url = input.getText().toString().trim();
setRelayUrl(url);
if (url.isEmpty()) {
toast("已清空:以后只走局域网");
} else {
toast("已保存中转地址");
appendLog("中转地址:" + url);
}
}
})
.setNeutralButton("清空", new android.content.DialogInterface.OnClickListener() {
@Override
public void onClick(android.content.DialogInterface dialog, int which) {
setRelayUrl("");
toast("已清空");
}
})
.setNegativeButton("取消", null)
.show();
}
private static final int REQ_IMAGE = 1001;
private static final int REQ_AUDIO = 1002;
private static final int REQ_FILE = 1003;
private static final int REQ_PERMS = 2001;
private RemoteClient client;
private DeviceStore store;
private Handler ui;
private final ExecutorService io = Executors.newSingleThreadExecutor();
private FrameLayout root;
private View connectPanel;
private LinearLayout controlPanel;
private FrameLayout mirrorPanel;
private ImageView mirrorImage;
private TextView mirrorStatus;
private LinearLayout deviceBox;
// 横屏/竖屏自适应用的网格参数(adaptGridToScreen 里算)
private int gridColumns = 3;
private int cellHeightDp = 58;
private float cellTextSp = 13f;
// 待排版的按钮队列,flushGrid() 里按列数切成行
private final java.util.List cellQueue =
new java.util.ArrayList();
// 正在连接中(避免重复点)
private volatile boolean busy;
// 屏幕镜像是否在推流
private volatile boolean mirrorStreaming;
// 正在接收的文件缓冲 + 文件名
private ByteArrayOutputStream incoming;
private String incomingName = "";
// 屏幕镜像的帧率统计
private int frameCount;
private float currentFps;
private long fpsWindowStart;
// 界面日志缓冲(限制长度,免得越滚越长)
private final StringBuilder logBuffer = new StringBuilder();
// ---- 局域网连接页面 ----
private FrameLayout lanPanel;
// ---- 设置页(用户要求:各种设置收在一个页面里调)----
private FrameLayout settingsPanel;
private TextView settingsRelayValue;
private TextView settingsCodeValue;
private TextView settingsDevicesValue;
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;
private TextView logView;
private TextView serverLabel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
store = new DeviceStore(this);
// 老版本硬塞的中转地址要清掉(见 migrateLegacyRelay)
migrateLegacyRelay();
ui = new Handler(Looper.getMainLooper());
client = new RemoteClient();
client.setDevice(store.deviceId(), store.deviceName());
root = new FrameLayout(this);
root.setBackgroundColor(C_BG);
// 三块面板都先建好,之后按需要显示哪一块。
// 注意顺序:控制页在最上面,镜像页居中,连接页压在最上层。
buildConnectPanel();
buildControlPanel();
// ⚠ initTabs() 必须在这里调 —— 它负责填 tabDefs 并把 10 个标签
// 塞进 tabBar。漏了这一步的后果是**连上后一片白屏**:
// tabDefs == null -> selectTab() 直接 return -> 内容区空白,
// 标签栏里只剩下那个滑动指示器(一个孤立的小蓝块)。
// 之前某次批量改代码把这一行删掉了,白屏就是这么来的。
initTabs();
buildMirrorPanel();
buildLanPanel();
buildSettingsPanel();
root.addView(controlPanel, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
root.addView(mirrorPanel, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
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));
controlPanel.setVisibility(View.GONE);
mirrorPanel.setVisibility(View.GONE);
lanPanel.setVisibility(View.GONE);
connectPanel.setVisibility(View.VISIBLE);
setContentView(root);
// 崩溃兜底要放在界面建好之后(它内部要用 ui)
installCrashGuard();
requestStartupPermissions();
refreshDeviceList();
appendLog("ykxmz 手表端已启动");
}
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() {
super.onDestroy();
destroyed = true;
try {
ui.removeCallbacksAndMessages(null);
} catch (Exception ignored) {
// 主线程 Handler 拿不到也不致命
}
try {
client.close();
} catch (Exception ignored) {
}
io.shutdownNow();
}
// ==================================================================
// 尺寸辅助
// ==================================================================
private int dp(float value) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
getResources().getDisplayMetrics());
}
private int sp(float value) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value,
getResources().getDisplayMetrics());
}
// ==================================================================
// 液态玻璃 设计系统
// ==================================================================
/** 背景:柔和三段渐变,像玻璃后面透出来的壁纸。 */
private GradientDrawable glassBackground() {
GradientDrawable drawable = new GradientDrawable(
GradientDrawable.Orientation.TL_BR,
new int[]{GLASS_BG_TOP, GLASS_BG_MID, GLASS_BG_BOT});
return drawable;
}
/** 一块玻璃:半透明白 + 亮边。 */
private GradientDrawable glass(float radiusDp) {
return glass(GLASS_FILL, radiusDp, GLASS_EDGE);
}
private GradientDrawable glass(int fill, float radiusDp, int edge) {
GradientDrawable drawable = new GradientDrawable();
drawable.setShape(GradientDrawable.RECTANGLE);
drawable.setColor(fill);
drawable.setCornerRadius(dp(radiusDp));
// 亮边是"玻璃厚度"的关键,没有它看着就是一块白纸
drawable.setStroke(Math.max(1, dp(1)), edge);
return drawable;
}
/** 按钮的玻璃底:顶部略亮,模拟反光。 */
private GradientDrawable glassButton(int tint, float radiusDp) {
GradientDrawable drawable = new GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
new int[]{blend(tint, 0xFFFFFFFF, 0.10f), tint});
drawable.setCornerRadius(dp(radiusDp));
drawable.setStroke(Math.max(1, dp(1)), GLASS_EDGE);
return drawable;
}
/** 把颜色往另一个颜色混一点。amount=0 原色,1 完全是另一个色。 */
private static int blend(int base, int other, float amount) {
float keep = 1f - amount;
int a = (int) (((base >> 24) & 0xFF) * keep + ((other >> 24) & 0xFF) * amount);
int r = (int) (((base >> 16) & 0xFF) * keep + ((other >> 16) & 0xFF) * amount);
int g = (int) (((base >> 8) & 0xFF) * keep + ((other >> 8) & 0xFF) * amount);
int b = (int) ((base & 0xFF) * keep + (other & 0xFF) * amount);
return (a << 24) | (r << 16) | (g << 8) | b;
}
/**
* 按压动画:缩到 0.94,松手带一点回弹。
* 返回 false 不吞事件,OnClickListener 照常触发。
*/
private void attachPress(final View view) {
if (view == null) {
return;
}
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
v.animate().cancel();
v.animate().scaleX(0.94f).scaleY(0.94f)
.setDuration(90)
.setInterpolator(new android.view.animation.DecelerateInterpolator())
.start();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
v.animate().cancel();
v.animate().scaleX(1f).scaleY(1f)
.setDuration(280)
.setInterpolator(
new android.view.animation.OvershootInterpolator(2.2f))
.start();
break;
}
return false;
}
});
}
/** 淡入 + 轻微上移(面板/内容出现时用)。 */
private void animateIn(View view, float fromDp, long duration) {
if (view == null) {
return;
}
view.setAlpha(0f);
view.setTranslationY(dp(fromDp));
view.animate().alpha(1f).translationY(0f)
.setDuration(duration)
.setInterpolator(new android.view.animation.DecelerateInterpolator())
.start();
}
/** 淡出 + 轻微下移(面板关闭时用)。 */
private void animateOut(final View view, float toDp, long duration,
final Runnable after) {
if (view == null) {
if (after != null) {
after.run();
}
return;
}
view.animate().alpha(0f).translationY(dp(toDp))
.setDuration(duration)
.setInterpolator(new android.view.animation.AccelerateInterpolator())
.withEndAction(new Runnable() {
@Override
public void run() {
if (after != null) {
after.run();
}
}
})
.start();
}
private GradientDrawable rounded(int fill, float radiusDp) {
GradientDrawable drawable = new GradientDrawable();
drawable.setColor(fill);
drawable.setCornerRadius(dp(radiusDp));
// 浅色主题下白按钮和浅灰底几乎同色,加一圈淡描边区分开
drawable.setStroke(Math.max(1, dp(1)), C_BORDER);
return drawable;
}
/** 按底色亮度自动选白字还是黑字,避免蓝底配黑字看不清。 */
private static int readableOn(int background) {
double r = (background >> 16) & 0xFF;
double g = (background >> 8) & 0xFF;
double b = background & 0xFF;
double luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
return luminance < 0.62 ? 0xFFFFFFFF : C_FG;
}
// ==================================================================
// 连接面板
// ==================================================================
private void buildConnectPanel() {
ScrollView scroll = new ScrollView(this);
scroll.setBackgroundColor(0x00000000);
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
box.setBackgroundColor(0x00000000);
box.setPadding(dp(10), dp(12), dp(10), dp(12));
scroll.addView(box, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
connectPanel = scroll;
TextView title = new TextView(this);
title.setText("遥控手表");
title.setTextColor(C_FG);
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
title.setGravity(Gravity.CENTER);
box.addView(title);
TextView hint = new TextView(this);
hint.setText("和电脑连同一个 WiFi,不用输 IP");
hint.setTextColor(C_DIM);
hint.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
hint.setGravity(Gravity.CENTER);
hint.setPadding(0, dp(2), 0, dp(10));
box.addView(hint);
// 连不上?—— 直接在这里做设备诊断。
// 用户原话:"我就是连不上才开设备诊断,连的上要设备诊断干嘛",
// 所以它属于首页,不属于功能标签页。
// 中转地址:跨网络用。放首页是因为这是"连不上"的常见原因之一。
Button relayButton = bigButton("设置", C_ACCENT, 14);
LinearLayout.LayoutParams relayParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52));
relayParams.setMargins(0, 0, 0, dp(8));
relayButton.setLayoutParams(relayParams);
relayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showSettingsPanel();
}
});
box.addView(relayButton);
// 输入配对码直接连对应电脑。
// 不再需要"先搜局域网 / 手动填 IP"那一套 ——
// 现在统一经中转,配对码就是找电脑的唯一凭据。
// 局域网连接:手表和电脑在同一个 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);
LinearLayout.LayoutParams codeParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(52));
codeParams.setMargins(0, 0, 0, dp(8));
codeButton.setLayoutParams(codeParams);
codeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
promptPairingCode();
}
});
box.addView(codeButton);
// 服务器延迟:连不上中转的时候,这一条最有用
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);
TextView savedTitle = new TextView(this);
savedTitle.setText("已配对的电脑(点一下直连)");
savedTitle.setTextColor(C_FG);
savedTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
savedTitle.setPadding(0, dp(12), 0, dp(6));
box.addView(savedTitle);
deviceBox = new LinearLayout(this);
deviceBox.setOrientation(LinearLayout.VERTICAL);
box.addView(deviceBox);
}
// ==================================================================
/** 连不上时:广播请求,让电脑把网络自检窗口打开。 */
// 自动搜索电脑(不用输 IP)
// ==================================================================
/** 找出这台电脑是否已经配对过。 */
private String findSavedToken(String host, int port) {
for (Map device : store.devices()) {
if (host.equals(Json.str(device, "host", ""))
&& port == Json.integer(device, "port", 0)) {
String token = Json.str(device, "token", "");
if (!token.isEmpty()) {
return token;
}
}
}
return null;
}
private LinearLayout labeled(String label, View field) {
LinearLayout box = new LinearLayout(this);
box.setOrientation(LinearLayout.VERTICAL);
TextView text = new TextView(this);
text.setText(label);
text.setTextColor(C_DIM);
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
box.addView(text);
box.addView(field);
box.setPadding(0, dp(4), 0, dp(4));
return box;
}
private EditText makeInput(String hint, int inputType) {
EditText edit = new EditText(this);
edit.setHint(hint);
edit.setHintTextColor(C_DIM);
edit.setTextColor(C_FG);
edit.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
edit.setInputType(inputType);
edit.setSingleLine(true);
edit.setBackground(rounded(C_CARD, 8));
edit.setPadding(dp(10), dp(10), dp(10), dp(10));
edit.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, dp(48)));
return edit;
}
/**
* 兜底:任何线程抛出未捕获异常都不让它把整个 App 干掉。
*
* 用户的要求是「不管干嘛,程序都要开着」——
* 后台线程一个 NPE 就闪退,看起来就像"程序自己关了"。
*/
private void installCrashGuard() {
Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, final Throwable error) {
try {
final String detail = String.valueOf(error);
ui.post(new Runnable() {
@Override
public void run() {
try {
appendLog("已拦截内部错误:" + detail);
toast("出了点小问题,程序继续运行");
} catch (Throwable ignored) {
// 兜底里再出错就彻底放弃提示
}
}
});
} catch (Throwable ignored) {
// 忽略
}
// 故意不交给默认处理器:交给它系统就按崩溃处理了
}
});
}
private void refreshDeviceList() {
deviceBox.removeAllViews();
List