本文旨在解决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适配真心不是那么容易的一件事情!!!愿君安好