USB 摄像头+远程遥控+中英的适配+topic的集成和状态的使用到相应页面
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package com.maibu.usb_camera_android;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.platform.PlatformViewRegistry;
|
||||
|
||||
public class UsbCameraAndroidPlugin implements FlutterPlugin, MethodChannel.MethodCallHandler {
|
||||
private static final String METHOD_CHANNEL = "com.maibu.usb_camera_android/methods";
|
||||
private static final String EVENT_CHANNEL = "com.maibu.usb_camera_android/events";
|
||||
private static final String VIEW_TYPE = "com.maibu.usb_camera_android/preview";
|
||||
|
||||
private MethodChannel methodChannel;
|
||||
private EventChannel eventChannel;
|
||||
private UsbCameraEngine engine;
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||
final Context context = binding.getApplicationContext();
|
||||
final BinaryMessenger messenger = binding.getBinaryMessenger();
|
||||
|
||||
engine = new UsbCameraEngine(context);
|
||||
methodChannel = new MethodChannel(messenger, METHOD_CHANNEL);
|
||||
methodChannel.setMethodCallHandler(this);
|
||||
|
||||
eventChannel = new EventChannel(messenger, EVENT_CHANNEL);
|
||||
eventChannel.setStreamHandler(engine);
|
||||
|
||||
final PlatformViewRegistry registry = binding.getPlatformViewRegistry();
|
||||
registry.registerViewFactory(VIEW_TYPE, new UsbCameraViewFactory(engine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "getDeviceList":
|
||||
result.success(engine.getDeviceList());
|
||||
break;
|
||||
case "getSupportedResolutions":
|
||||
result.success(engine.getSupportedResolutions());
|
||||
break;
|
||||
case "getPreviewSize":
|
||||
result.success(engine.getPreviewSize());
|
||||
break;
|
||||
case "open":
|
||||
final String deviceId = call.argument("deviceId");
|
||||
final String resolution = call.argument("resolution");
|
||||
engine.open(deviceId, resolution, result);
|
||||
break;
|
||||
case "close":
|
||||
engine.close(result);
|
||||
break;
|
||||
case "setResolution":
|
||||
final String res = call.argument("resolution");
|
||||
engine.setResolution(res, result);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
if (methodChannel != null) {
|
||||
methodChannel.setMethodCallHandler(null);
|
||||
methodChannel = null;
|
||||
}
|
||||
if (eventChannel != null) {
|
||||
eventChannel.setStreamHandler(null);
|
||||
eventChannel = null;
|
||||
}
|
||||
if (engine != null) {
|
||||
engine.dispose();
|
||||
engine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package com.maibu.usb_camera_android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.herohan.uvcapp.CameraHelper;
|
||||
import com.herohan.uvcapp.ICameraHelper;
|
||||
import com.serenegiant.usb.Size;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.EventChannel;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
/**
|
||||
* UVC 摄像头引擎:封装 CameraHelper,管理设备状态和预览。
|
||||
*
|
||||
* 权限流程:
|
||||
* 1. open() 先查 hasPermission() → 有则直接 selectDevice()
|
||||
* 2. 无权限 → 通过静态 UsbPermissionReceiver 申请(Android 14+ 兼容)
|
||||
* 3. 用户点"允许"→ Receiver 回调 → selectDevice()
|
||||
* 用户点"拒绝"→ Receiver 回调 → completeOpen(拒绝)
|
||||
* 4. 若异常(PendingIntent 创建失败等),立即 completeOpen 避免 Flutter
|
||||
* MethodChannel 双重回复崩溃。
|
||||
*
|
||||
* 为什么必须用静态 Receiver(详见 UsbPermissionReceiver 类注释):
|
||||
* Android 14+ 禁止 FLAG_MUTABLE + 隐式 Intent,动态 BroadcastReceiver
|
||||
* 无法构造显式 Intent → 只能用 manifest 静态注册。
|
||||
*/
|
||||
public class UsbCameraEngine implements EventChannel.StreamHandler {
|
||||
private static final String TAG = "UsbCameraEngine";
|
||||
|
||||
private final Context appContext;
|
||||
private final UsbManager usbManager;
|
||||
private final Handler mainHandler;
|
||||
private final ICameraHelper cameraHelper;
|
||||
|
||||
private Surface currentSurface;
|
||||
private boolean isPreviewing = false;
|
||||
private UsbDevice currentDevice;
|
||||
private List<Size> supportedSizes = new ArrayList<>();
|
||||
|
||||
private EventChannel.EventSink eventSink;
|
||||
private MethodChannel.Result pendingOpenResult;
|
||||
|
||||
public UsbCameraEngine(@NonNull Context context) {
|
||||
this.appContext = context.getApplicationContext();
|
||||
this.usbManager = (UsbManager) appContext.getSystemService(Context.USB_SERVICE);
|
||||
this.mainHandler = new Handler(Looper.getMainLooper());
|
||||
this.cameraHelper = new CameraHelper();
|
||||
this.cameraHelper.setStateCallback(stateCallback);
|
||||
}
|
||||
|
||||
// ---------- 设备枚举 ----------
|
||||
|
||||
public List<Map<String, Object>> getDeviceList() {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (UsbDevice device : usbManager.getDeviceList().values()) {
|
||||
if (isUvcDevice(device)) {
|
||||
result.add(deviceToMap(device));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否 UVC 设备。
|
||||
* 1. 接口级 class==14 (USB Video Class)
|
||||
* 2. 设备级 class==239/0xEF(复合设备 Miscellaneous,UVC采集卡常用)
|
||||
*/
|
||||
private boolean isUvcDevice(@NonNull UsbDevice device) {
|
||||
if (device.getDeviceClass() == 239
|
||||
&& device.getDeviceSubclass() == 2
|
||||
&& device.getDeviceProtocol() == 1) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
if (device.getInterface(i).getInterfaceClass() == 14) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Map<String, Object> deviceToMap(@NonNull UsbDevice device) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("deviceId", String.valueOf(device.getDeviceId()));
|
||||
map.put("vendorId", device.getVendorId());
|
||||
map.put("productId", device.getProductId());
|
||||
map.put("productName", device.getProductName());
|
||||
map.put("hasPermission", usbManager.hasPermission(device) ? 1 : 0);
|
||||
return map;
|
||||
}
|
||||
|
||||
// ---------- 打开 / 关闭 ----------
|
||||
|
||||
/**
|
||||
* 打开指定设备:
|
||||
* 1. 找设备
|
||||
* 2. hasPermission() → selectDevice / requestUsbPermission
|
||||
* 3. try-catch 保护:PendingIntent 异常时立即报错给 Dart,避免 result 残留
|
||||
* 导致后续 close() 二次回复崩溃 "Reply already submitted"。
|
||||
*/
|
||||
public void open(
|
||||
@NonNull String deviceId,
|
||||
@Nullable String resolution,
|
||||
@NonNull MethodChannel.Result result) {
|
||||
UsbDevice target = null;
|
||||
for (UsbDevice device : usbManager.getDeviceList().values()) {
|
||||
if (String.valueOf(device.getDeviceId()).equals(deviceId)) {
|
||||
target = device;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
result.error("DEVICE_NOT_FOUND", "未找到指定 USB 设备: " + deviceId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
cancelPendingOpen();
|
||||
pendingOpenResult = result;
|
||||
currentDevice = target;
|
||||
|
||||
try {
|
||||
if (usbManager.hasPermission(target)) {
|
||||
Log.d(TAG, "已有 USB 权限,直接 selectDevice");
|
||||
cameraHelper.selectDevice(target);
|
||||
} else {
|
||||
Log.d(TAG, "无 USB 权限,通过 UsbPermissionReceiver 申请: " + deviceId);
|
||||
requestUsbPermission(target);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// 例如 Android 14+ PendingIntent 创建异常、UsbManager 服务不可用等
|
||||
Log.e(TAG, "requestPermission 异常: " + e.getMessage(), e);
|
||||
completeOpen("申请 USB 权限失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过静态 UsbPermissionReceiver + 显式 PendingIntent 申请 USB 权限。
|
||||
*
|
||||
* 关键点:
|
||||
* - 显式 Intent(new Intent(context, UsbPermissionReceiver.class))
|
||||
* 使 FLAG_MUTABLE 在 Android 14+ 合法。
|
||||
* - FLAG_MUTABLE 允许系统注入 EXTRA_DEVICE / EXTRA_PERMISSION_GRANTED。
|
||||
* - 一次性静态回调 sCallback 在 Receiver 内部触发后立即清空。
|
||||
*/
|
||||
private void requestUsbPermission(@NonNull UsbDevice device) {
|
||||
UsbPermissionReceiver.sCallback = (recvDevice, granted) -> mainHandler.post(() -> {
|
||||
if (granted) {
|
||||
Log.d(TAG, "用户已授予 USB 权限 → selectDevice");
|
||||
cameraHelper.selectDevice(recvDevice);
|
||||
} else {
|
||||
Log.d(TAG, "用户拒绝了 USB 权限");
|
||||
completeOpen("用户拒绝了 USB 权限,请点击「重试」并在弹窗中选择「允许」");
|
||||
postEvent("cancelled", deviceToMap(recvDevice));
|
||||
}
|
||||
});
|
||||
|
||||
android.app.PendingIntent pi =
|
||||
UsbPermissionReceiver.createPermissionIntent(appContext, device);
|
||||
usbManager.requestPermission(device, pi);
|
||||
}
|
||||
|
||||
public void close(@NonNull MethodChannel.Result result) {
|
||||
cancelPendingOpen();
|
||||
UsbPermissionReceiver.sCallback = null; // 取消等待中的权限回调
|
||||
if (cameraHelper.isCameraOpened()) {
|
||||
cameraHelper.closeCamera();
|
||||
}
|
||||
currentDevice = null;
|
||||
isPreviewing = false;
|
||||
if (currentSurface != null) {
|
||||
cameraHelper.removeSurface(currentSurface);
|
||||
}
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消等待中的 open MethodChannel 回调。
|
||||
* 用 try-catch 保护:如果 Flutter 框架已经回复过(例如异常时自动 error),
|
||||
* 重复调用 result.error() 会抛 IllegalStateException("Reply already submitted")
|
||||
* 并导致进程崩溃。忽略即可。
|
||||
*/
|
||||
private void cancelPendingOpen() {
|
||||
if (pendingOpenResult != null) {
|
||||
final MethodChannel.Result result = pendingOpenResult;
|
||||
pendingOpenResult = null;
|
||||
mainHandler.post(() -> {
|
||||
try {
|
||||
result.error("CANCELLED", "操作被取消", null);
|
||||
} catch (IllegalStateException ignored) {
|
||||
// 框架已回复,重复调用无效,忽略
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 分辨率 ----------
|
||||
|
||||
/**
|
||||
* 获取当前预览尺寸(width x height),用于 Flutter 侧计算 AspectRatio。
|
||||
* 摄像头未打开时返回 null。
|
||||
*/
|
||||
@Nullable
|
||||
public Map<String, Object> getPreviewSize() {
|
||||
if (!isPreviewing) return null;
|
||||
Size size = cameraHelper.getPreviewSize();
|
||||
if (size == null) return null;
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("width", size.width);
|
||||
map.put("height", size.height);
|
||||
return map;
|
||||
}
|
||||
|
||||
public List<String> getSupportedResolutions() {
|
||||
List<String> list = new ArrayList<>();
|
||||
for (Size size : supportedSizes) {
|
||||
list.add(size.width + "x" + size.height);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setResolution(@NonNull String resolution, @NonNull MethodChannel.Result result) {
|
||||
Size target = findResolution(resolution);
|
||||
if (target == null) {
|
||||
result.error("INVALID_RESOLUTION", "无法解析分辨率: " + resolution, null);
|
||||
return;
|
||||
}
|
||||
if (isPreviewing) {
|
||||
cameraHelper.stopPreview();
|
||||
cameraHelper.setPreviewSize(target);
|
||||
cameraHelper.startPreview();
|
||||
if (currentSurface != null && currentSurface.isValid()) {
|
||||
cameraHelper.addSurface(currentSurface, false);
|
||||
}
|
||||
}
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Size findResolution(@NonNull String resolution) {
|
||||
String[] parts = resolution.split("x");
|
||||
if (parts.length != 2) return null;
|
||||
try {
|
||||
int width = Integer.parseInt(parts[0].trim());
|
||||
int height = Integer.parseInt(parts[1].trim());
|
||||
for (Size size : supportedSizes) {
|
||||
if (size.width == width && size.height == height) {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Surface 管理 ----------
|
||||
|
||||
public void addSurface(@NonNull Surface surface) {
|
||||
currentSurface = surface;
|
||||
if (isPreviewing && surface.isValid()) {
|
||||
cameraHelper.addSurface(surface, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSurface(@NonNull Surface surface) {
|
||||
if (currentSurface == surface) {
|
||||
if (isPreviewing) {
|
||||
cameraHelper.removeSurface(surface);
|
||||
}
|
||||
currentSurface = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 生命周期 ----------
|
||||
|
||||
public void dispose() {
|
||||
UsbPermissionReceiver.sCallback = null;
|
||||
cameraHelper.release();
|
||||
}
|
||||
|
||||
// ---------- CameraHelper 状态回调 ----------
|
||||
|
||||
private final ICameraHelper.StateCallback stateCallback = new ICameraHelper.StateCallback() {
|
||||
@Override
|
||||
public void onAttach(@NonNull UsbDevice device) {
|
||||
postEvent("attached", deviceToMap(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceOpen(@NonNull UsbDevice device, boolean isFirstOpen) {
|
||||
cameraHelper.openCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraOpen(@NonNull UsbDevice device) {
|
||||
supportedSizes = cameraHelper.getSupportedSizeList();
|
||||
startPreview();
|
||||
completeOpen(null);
|
||||
postEvent("opened", deviceToMap(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraClose(@NonNull UsbDevice device) {
|
||||
isPreviewing = false;
|
||||
if (currentSurface != null) {
|
||||
cameraHelper.removeSurface(currentSurface);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceClose(@NonNull UsbDevice device) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetach(@NonNull UsbDevice device) {
|
||||
if (currentDevice != null && currentDevice.getDeviceId() == device.getDeviceId()) {
|
||||
currentDevice = null;
|
||||
isPreviewing = false;
|
||||
postEvent("detached", deviceToMap(device));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull UsbDevice device) {
|
||||
// UVCAndroid 内部权限被拒(通常被 UsbPermissionReceiver 拦截,此处兜底)
|
||||
completeOpen("用户拒绝了 USB 权限,请点击「重试」并在弹窗中选择「允许」");
|
||||
postEvent("cancelled", deviceToMap(device));
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- 预览启动 ----------
|
||||
|
||||
private void startPreview() {
|
||||
if (isPreviewing) return;
|
||||
cameraHelper.startPreview();
|
||||
isPreviewing = true;
|
||||
if (currentSurface != null && currentSurface.isValid()) {
|
||||
cameraHelper.addSurface(currentSurface, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- MethodChannel 异步回调 ----------
|
||||
|
||||
private void completeOpen(@Nullable String error) {
|
||||
if (pendingOpenResult == null) return;
|
||||
final MethodChannel.Result result = pendingOpenResult;
|
||||
pendingOpenResult = null;
|
||||
mainHandler.post(() -> {
|
||||
try {
|
||||
if (error == null) {
|
||||
result.success(null);
|
||||
} else {
|
||||
result.error("OPEN_FAILED", error, null);
|
||||
}
|
||||
} catch (IllegalStateException ignored) {
|
||||
// Flutter 框架已回复,重复调用无效,忽略
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- EventChannel.StreamHandler ----------
|
||||
|
||||
@Override
|
||||
public void onListen(Object arguments, EventChannel.EventSink events) {
|
||||
eventSink = events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(Object arguments) {
|
||||
eventSink = null;
|
||||
}
|
||||
|
||||
private void postEvent(@NonNull String type, @NonNull Map<String, Object> data) {
|
||||
mainHandler.post(() -> {
|
||||
if (eventSink == null) return;
|
||||
Map<String, Object> event = new HashMap<>();
|
||||
event.put("type", type);
|
||||
event.put("device", data);
|
||||
eventSink.success(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.maibu.usb_camera_android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.flutter.plugin.platform.PlatformView;
|
||||
|
||||
public class UsbCameraView implements PlatformView {
|
||||
private final SurfaceView surfaceView;
|
||||
|
||||
public UsbCameraView(Context context, UsbCameraEngine engine) {
|
||||
surfaceView = new SurfaceView(context);
|
||||
surfaceView.setZOrderMediaOverlay(true);
|
||||
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
|
||||
@Override
|
||||
public void surfaceCreated(@NonNull SurfaceHolder holder) {
|
||||
engine.addSurface(holder.getSurface());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(
|
||||
@NonNull SurfaceHolder holder, int format, int width, int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
|
||||
engine.removeSurface(holder.getSurface());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return surfaceView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
Surface surface = surfaceView.getHolder().getSurface();
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.maibu.usb_camera_android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.flutter.plugin.common.StandardMessageCodec;
|
||||
import io.flutter.plugin.platform.PlatformView;
|
||||
import io.flutter.plugin.platform.PlatformViewFactory;
|
||||
|
||||
public class UsbCameraViewFactory extends PlatformViewFactory {
|
||||
private final UsbCameraEngine engine;
|
||||
|
||||
public UsbCameraViewFactory(@NonNull UsbCameraEngine engine) {
|
||||
super(StandardMessageCodec.INSTANCE);
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PlatformView create(
|
||||
@NonNull Context context, int id, @Nullable Object args) {
|
||||
return new UsbCameraView(context, engine);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.maibu.usb_camera_android;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.os.Build;
|
||||
|
||||
/**
|
||||
* USB 权限结果广播接收器(manifest 静态注册)。
|
||||
*
|
||||
* 为什么必须用静态 Receiver 而不能用动态注册的 BroadcastReceiver:
|
||||
* - Android 14 (API 34+) 禁止 FLAG_MUTABLE + 隐式 Intent 组合,直接抛
|
||||
* IllegalArgumentException("Targeting U+ disallows creating a PendingIntent
|
||||
* with FLAG_MUTABLE, an implicit Intent...")。
|
||||
* - 动态 BroadcastReceiver 无法在 Intent 里指定目标 class,只能是隐式 →
|
||||
* FLAG_MUTABLE 在 Android 14+ 崩溃;FLAG_IMMUTABLE 又会导致系统无法注入
|
||||
* EXTRA_DEVICE / EXTRA_PERMISSION_GRANTED,权限结果永远读不到。
|
||||
* - 静态 Receiver 在 manifest 声明,可用 new Intent(context, 本类.class)
|
||||
* 构造显式 Intent → FLAG_MUTABLE 合法 → 系统正常注入 extras。
|
||||
*
|
||||
* 权限结果通过一次性静态回调 sCallback 传给 UsbCameraEngine,收到后立刻清空。
|
||||
*/
|
||||
public class UsbPermissionReceiver extends BroadcastReceiver {
|
||||
|
||||
static final String ACTION_USB_PERMISSION =
|
||||
"com.maibu.usb_camera_android.USB_PERMISSION";
|
||||
|
||||
interface Callback {
|
||||
void onResult(UsbDevice device, boolean granted);
|
||||
}
|
||||
|
||||
/** 同一时刻只有一个等待中的权限请求 */
|
||||
static volatile Callback sCallback;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!ACTION_USB_PERMISSION.equals(intent.getAction())) return;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
boolean granted =
|
||||
intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false);
|
||||
|
||||
Callback cb = sCallback;
|
||||
sCallback = null; // 一次性,防止旧回调串场
|
||||
if (cb != null && device != null) {
|
||||
cb.onResult(device, granted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 Android 14+ 合法的 USB 权限 PendingIntent。
|
||||
*
|
||||
* 显式 Intent(指定 UsbPermissionReceiver.class)
|
||||
* → 允许搭配 FLAG_MUTABLE → 系统可注入权限结果 extras。
|
||||
* API 30 及以下 PendingIntent 默认可变,不需要显式 FLAG_MUTABLE。
|
||||
*/
|
||||
static PendingIntent createPermissionIntent(Context context, UsbDevice device) {
|
||||
Intent intent = new Intent(context, UsbPermissionReceiver.class);
|
||||
intent.setAction(ACTION_USB_PERMISSION);
|
||||
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // API 31+
|
||||
flags |= PendingIntent.FLAG_MUTABLE;
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context, device.getDeviceId(), intent, flags);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user