Android UsbAccessory中你需要小心的坑及解决方案

本文旨在解决Usb连接过程中的问题而不注重具体实现过程,如果有对Usb连接过程感兴趣的朋友可以查看Android官方文档

目录


现实背景

现在智能硬件发展迅速,各种运动相机、无人机不断发展,而其中大部分都通过Usb跟手机之间建立链路连接通信,所以怎么处理好Usb的连接问题就显得很重要。

存在问题

PS:下文中叙述Android系统指EMUI、MIUI、魅族、Android原生等合集,一般出问题比较多的是华为EMUI。

  • 系统不广播Usb连接事件(android.hardware.usb.action.USB_ACCESSORY_ATTACHED)
  • 系统有时不广播Usb断开事件(android.hardware.usb.action.USB_ACCESSORY_DETACHED)
  • 系统有时会异常缓存UsbAccessory,导致没连接上时App却可以获取到不可用的Accessory

问题原因

问题1:系统不广播Usb连接事件
当Usb设备连接到手机时,Android系统收到了ACTION_USB_DEVICE_ATTACHED广播,然后系统通过Manifests得知App想要在Usb设备连上的时候运行,最终系统会发android.intent.action.MAIN广播打开App。所以系统不会给App发送ACTION_USB_DEVICE_ATTACHED广播,因为系统认为App知道在这种情况该怎么处理。
更详细解析请参考 StackOverflow

问题2:系统有时不广播Usb断开事件
当Usb设备从设备拔出时,Android系统不广播USB_ACCESSORY_DETACHED,导致App无法做断开连接的逻辑。这个是系统的Bug,在华为EMUI上极容易重现。

问题3:系统有时会异常缓存UsbAccessory,导致没连接上的时候App却可以获取到Accessory
当设备拔出时,Android系统缓存UsbAccessory,导致App认为Usb设备还正在连接,导致逻辑错误。很明显这也是系统的Bug,也是在华为上极容易重现(快速插拔)。

解决方案

问题1:系统不广播Usb连接事件
这个问题解决的关键在于如何得知Usb设备已经连接上,有两种思路,一种比较可靠的是当Usb设备未连接状态通过Handler起一个Timer定时去询问Android系统有没有连接上Usb设备,另一种是在Activity的onResume去询问Android系统有没有连接上Usb设备(这种实践中我没有使用,具体存不存在问题待验证)。

方案一大致代码逻辑:

private Handler handler = new Handler(BackgroundLooper.getLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        handler.sendEmptyMessageDelayed(0, 2000);
        tryConnect();
        return false;
    }
});

private synchronized void tryConnect() {
    boolean connect = ...; // 判断是否连接上Usb设备,具体代码省略
    if(connect){
        // 连接上设备,停止Timer
        handler.removeMessages(0);
    }
}

问题2:系统有时不广播Usb断开事件
这个问题的根源在于某些Android系统上,USB_ACCESSORY_DETACHED广播不可靠。
那么解决问题的思路就在于找可靠方式来代替,幸运的是系统还给我们提供了USB_STATE(android.hardware.usb.action.USB_STATE)广播,USB_STATE广播会推送Usb设备的连接状态。
但USB_STATE也不是可靠的,有时它在Usb设备断开连接时发多个广播并且其中会混杂错误的广播(状态为Usb设备已连接),所以我们不能单纯依赖它来做Usb连接断开事件的判断。经过大部分机型的测试,可以保证USB_STATE在Usb设备断开的时候至少会发送一个Usb连接状态为断开的广播,所以我们就可以依靠USB_STATE来做Usb设备断开逻辑的检测。

USB_STATE判断Usb设备断开逻辑大致代码

public class DJIUsbAccessoryReceiver extends BroadcastReceiver {
    private long mReceivedUsbStateTime = 0;

    // 注册的广播回调函数
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        switch (action) {

            //部分代码省略

            case ACTION_USB_STATE:
                boolean connected = intent.getExtras().getBoolean("connected");
                if (!connected) {
                    // 避免一下子过来多个广播的情况
                    if(mReceivedUsbStateTime + 300 > System.currentTimeMillis()){
                        Log.e(TAG, "Disconnect broadcast(Usb State) too frequently, ignore it");
                        return;
                    }
                    mReceivedUsbStateTime = System.currentTimeMillis();

                    disconnected();
                    clearTimer();
                    handler.sendEmptyMessageDelayed(0, 3000); // 3秒后启动Timer检测Usb设备是否已经连接
                }
                break;

            default:
                break;
        }
    }
}

// App启动时注册广播监听
public static void registerUsbReceiver(Context ctx) {
    Context context = ctx.getApplicationContext();
    sUsbReceiver = new DJIUsbAccessoryReceiver();
    sUsbReceiver.start(context);
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(DJIUsbAccessoryReceiver.ACTION_USB_PERMISSION);
    intentFilter.addAction(DJIUsbAccessoryReceiver.ACTION_USB_STATE);
    context.registerReceiver(sUsbReceiver, intentFilter);
}

问题3:系统有时会异常缓存UsbAccessory,导致没连接上的时候App却可以获取到Accessory
这个问题是由部分Android系统Bug导致的,表现为Usb设备断开连接时,系统仍认为Usb设备没有断连,通过UsbManager可获取到Accessory,但是无法建立可用的AOA链路。
所以也没有比较好的解决办法,App端只能做保护,比如获取到Accessory的时候先发一个探测包(无意义),探测包发送成功才认为建立了可靠的AOA链路,这样至少可以保证Usb设备连接上即可进行数据通信。
这边项目使用的是通过UsbManager.openAccessory获取到ParcelFileDescriptor,然后将ParcelFileDescriptor传入C++获取到句柄建立连接通信的,下面就展示下C++中如何判断UsbAccessory是否可用(Android代码中实现原理也类似)。

C++部分保护代码

// 通过JNI调用到C++部分判断当前Accessory的句柄是否可用, 不可用就认为连接断开
void OnUsbConnected(JNIEnv *env, jobject obj, jobject java_fd){
    int fd = GetFDFromFileDescriptor(env, java_fd);

    // 这里写一个字节是为了避免上层ROM存在问题给了一个不可读写的fd导致错误连接
    int buffer_out_size = 1;
    unsigned char* buffer_out = new unsigned char[buffer_out_size];
    // 写失败会返回 -1
    int write_len = write(fd, buffer_out, buffer_out_size);
    delete[] buffer_out;
    if(write_len == -1)
    {
        OnUsbDisconnected(env, obj, java_fd);
        return;
    }

    // 句柄可用,建立Usb连接
    on_android_usb_added(fd);
}

小结

所以整体的思路就是:

  • Usb连接的监听:Timer定时查询
  • Usb断开的监听:USB_STATE广播
  • Usb链路可靠性确认:发送探测包

由于Android不同的ROM带来的问题有很多,这里介绍的是项目中遇到的问题及解决方案,如果你有更好的方案或者还有其他问题都可以一起讨论。
By the way, Android适配真心不是那么容易的一件事情!!!愿君安好

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值