feat(ch934x_serial): 添加CH934X串口设备权限管理和端口切换功能

- 新增 setActivePort 方法用于在同一USB设备内部切换活跃串口
- 新增 requestUsbPermission 方法用于主动申请指定设备的USB权限
- 实现Android侧USB权限申请广播接收器和同步等待机制
- 在示例应用中集成权限预申请和端口切换UI逻辑
- 优化设备打开后自动拉取真实串口列表的功能
- 更新端口选择下拉框在打开状态下支持实时切换串口
This commit is contained in:
Developer
2026-07-06 16:58:51 +08:00
parent 90de9121ff
commit 76377e52f2
6 changed files with 276 additions and 13 deletions
@@ -1,9 +1,15 @@
package com.xiarui.ch934x_serial;
import android.app.Activity;
import android.app.Application;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -12,6 +18,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import cn.wch.ch934xlib.CH934XManager;
import cn.wch.ch934xlib.callback.IDataCallback;
@@ -25,6 +33,8 @@ import cn.wch.ch934xlib.exception.UartLibException;
import cn.wch.ch934xlib.gpio.GPIO_DIR;
import cn.wch.ch934xlib.gpio.GPIO_VALUE;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
@@ -39,14 +49,24 @@ import io.flutter.plugin.common.MethodChannel.Result;
* {@code UsbSerial} 是 SDK 的概念性命名,真实 API 在
* {@code cn.wch.ch934xlib.CH934XManager}。
*/
public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
/** Dart ↔ Java 通信使用的 MethodChannel 名称,需与 Dart 侧保持一致。 */
private static final String CHANNEL_NAME = "ch934x_serial";
/** USB 权限申请广播的 Action,需在 AndroidManifest 中注册对应 Receiver。 */
private static final String ACTION_USB_PERMISSION = "com.xiarui.ch934x_serial.USB_PERMISSION";
private MethodChannel channel;
private Context applicationContext;
@Nullable
private Activity activity;
@Nullable
private BroadcastReceiver permissionReceiver;
@Nullable
private CompletableFuture<Boolean> pendingPermissionRequest;
/** 当前 Dart 侧选中的 (deviceId, serialNumber) 对应的 UsbDevice 引用。 */
@Nullable
private UsbDevice activeDevice;
@@ -138,6 +158,30 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
exceptionCallbackEnabled = false;
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
registerPermissionReceiver();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
unregisterPermissionReceiver();
activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
activity = binding.getActivity();
registerPermissionReceiver();
}
@Override
public void onDetachedFromActivity() {
unregisterPermissionReceiver();
activity = null;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
try {
@@ -157,9 +201,15 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
case "openPort":
result.success(handleOpenPort(call));
break;
case "requestUsbPermission":
result.success(handleRequestUsbPermission(call));
break;
case "closePort":
result.success(handleClosePort());
break;
case "setActivePort":
result.success(handleSetActivePort(call));
break;
case "read":
result.success(handleRead(call));
break;
@@ -187,8 +237,6 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
}
} catch (ChipException e) {
result.error("CHIP_ERROR", e.getMessage(), e.getClass().getName());
} catch (NoPermissionException e) {
result.error("NO_PERMISSION", e.getMessage(), e.getClass().getName());
} catch (UartLibException e) {
result.error("UART_LIB_ERROR", e.getMessage(), e.getClass().getName());
} catch (Throwable t) {
@@ -286,9 +334,13 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
// 设备打开 / 关闭(对应文档 5.x)
// ------------------------------------------------------------------------
/** 文档 5.1.1。 */
/** 文档 5.1.1。
*
* <p>若 SDK 抛 {@link NoPermissionException},自动弹出系统
* 授权对话框,等待用户选择后再重试一次。
*/
private boolean handleOpenPort(@NonNull MethodCall call)
throws ChipException, NoPermissionException, UartLibException {
throws ChipException, UartLibException {
Integer deviceId = call.argument("deviceId");
Integer serialPortIndex = call.argument("serialPortIndex");
if (deviceId == null || serialPortIndex == null) {
@@ -298,9 +350,24 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
if (device == null) {
return false;
}
boolean opened = CH934XManager.getInstance().openDevice(device);
if (!opened) {
return false;
try {
boolean opened = openDeviceInternal(device);
if (!opened) {
return false;
}
} catch (NoPermissionException npe) {
// 没有权限,自动弹出系统授权框,等用户选择。
if (!requestUsbPermissionBlocking(device)) {
return false;
}
// 重新尝试打开。
try {
if (!openDeviceInternal(device)) {
return false;
}
} catch (NoPermissionException retryFail) {
return false;
}
}
activeDevice = device;
activeSerialNumber = serialPortIndex;
@@ -309,6 +376,104 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
return true;
}
/** 内部真正调 SDK 的打开,让 [handleOpenPort] 能在外层捕获权限异常。 */
private boolean openDeviceInternal(@NonNull UsbDevice device)
throws ChipException, NoPermissionException, UartLibException {
return CH934XManager.getInstance().openDevice(device);
}
/** 处理 Dart 主动发起的"申请 USB 权限"调用。 */
private boolean handleRequestUsbPermission(@NonNull MethodCall call) {
UsbDevice device = resolveDevice(call);
if (device == null) {
return false;
}
return requestUsbPermissionBlocking(device);
}
/** 同步等待用户对 USB 权限对话框的回应,返回是否授权。 */
private boolean requestUsbPermissionBlocking(@NonNull UsbDevice device) {
if (activity == null) {
return false;
}
UsbManager usbManager = (UsbManager) applicationContext
.getSystemService(Context.USB_SERVICE);
if (usbManager == null) {
return false;
}
if (usbManager.hasPermission(device)) {
return true;
}
ensurePermissionReceiver();
CompletableFuture<Boolean> future = new CompletableFuture<>();
pendingPermissionRequest = future;
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
: PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent intent = PendingIntent.getBroadcast(
activity, 0, new Intent(ACTION_USB_PERMISSION), flags);
usbManager.requestPermission(device, intent);
try {
Boolean result = future.get(60, TimeUnit.SECONDS);
return result != null && result;
} catch (Throwable t) {
return false;
} finally {
pendingPermissionRequest = null;
}
}
/** 注册用于接收 USB 授权结果的 BroadcastReceiver(仅注册一次)。 */
private void ensurePermissionReceiver() {
if (permissionReceiver != null || activity == null) {
return;
}
permissionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!ACTION_USB_PERMISSION.equals(intent.getAction())) {
return;
}
boolean granted = intent.getBooleanExtra(
UsbManager.EXTRA_PERMISSION_GRANTED, false);
CompletableFuture<Boolean> future = pendingPermissionRequest;
if (future != null) {
future.complete(granted);
}
}
};
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// 13+ 强制要求显式 flag;USB 授权响应只来自系统,使用 NOT_EXPORTED。
activity.registerReceiver(permissionReceiver, filter,
Context.RECEIVER_NOT_EXPORTED);
} else {
activity.registerReceiver(permissionReceiver, filter);
}
}
private void registerPermissionReceiver() {
// Receiver 在真正需要时(申请权限前)才注册,避免在 manifest 中额外声明。
}
private void unregisterPermissionReceiver() {
if (permissionReceiver != null && activity != null) {
try {
activity.unregisterReceiver(permissionReceiver);
} catch (Throwable ignored) {
// Receiver 未注册时 ignore。
}
}
permissionReceiver = null;
CompletableFuture<Boolean> future = pendingPermissionRequest;
if (future != null) {
future.complete(false);
pendingPermissionRequest = null;
}
}
/** 文档 5.2.1。 */
private boolean handleClosePort() {
if (activeDevice == null) {
@@ -327,6 +492,19 @@ public class Ch934xSerialPlugin implements FlutterPlugin, MethodCallHandler {
return true;
}
/** 切换当前活跃串口索引(无需重新 open 设备)。 */
private boolean handleSetActivePort(@NonNull MethodCall call) {
if (activeDevice == null) {
return false;
}
Integer idx = call.argument("serialPortIndex");
if (idx == null || idx < 0) {
return false;
}
activeSerialNumber = idx;
return true;
}
// ------------------------------------------------------------------------
// 串口读写(对应文档 6.x)
// ------------------------------------------------------------------------