Android从3.1版本可开始引进了对AOA的支持,这是一种允许外部USB硬件与Android设备进行交互的特殊Accessory模式。当一个Android设备处于Accessory模式的时候,已连接的配件则扮演Host的角色(负责给总线供电并且枚举设备),而Android设备则扮演USB device的角色。Android USB配件是一个专门设计的附加到Android 设备上,并实现了一个简单的协议(AOA),使他们能够检测支持Accessory模式的Android设备。配件还必须提供500ma的5V供电电源。许多以前发布的Android设备只能充当USB设备,不能用来与外部USB设备的连接通讯。AOA的支持克服了这个限制。accessory模式最终依赖于硬件设备,并且不是所有的设备都支持Accessory模式。如果设备支持Accessory模式的话,可以在应用的AndroidManifest中使用 uses-feature 元素来标记。
一个USB配件必须遵循AOA协议,这个协议定义了一个配件如何发现并且与设备建立通讯,通常情况下,一个配件应该按照以下步骤:
当设备连接时,他们应该在以下三种状态中的一种:
1.已Attached的设备支持Accessory模式,并且已经处于此模式
2.已Attached的设备支持Accessory模式,但不处于此模式
3.设备不支持Accessory模式
当建立连接之后,配件应该检查连接的设备的VID和PID。google固定了AOA设备的VID与PID,如果PID跟VID匹配的话,那么就可以使用USB的块传输的endpoints建立配件与设备之间的通讯。
AOA 设备VID = 0x18D1, PID有多种,其中包括AOA 2.0中新增的,分别对应的关系如下:
PID | 模式 |
---|---|
0x2D00 | accessory |
0x2D01 | accessory + adb |
0x2D02 | audio |
0x2D03 | audio + adb |
0x2D04 | accessory + audio |
0x2D05 | accessory + audio + adb |
如果VID跟PID不匹配,那么我们就需要请求设备切换为Accessory模式,这时候需要发送51号命令控制请求来确定设备是否支持Android附件协议。如果支持协议,则返回非零数,表示设备支持的协议版本。此请求是端点0上的控制请求,其特征如下:
参数 | 值 |
---|---|
requestType | USB_DIR_IN 或者 USB_TYPE_VENDOR |
request | 51 |
value | 0 |
index | 0 |
data | 协议版本号 |
如果设备返回支持的协议版本号,则向设备发送标识字符串信息。该信息允许设备为该附件找出合适的应用程序,并且如果不存在适当的应用程序,则向用户指示下载的URL地址。这些请求是端点0(对于每个字符串ID)的52号命令控制请求,具有以下特征:
参数 | 值 |
---|---|
requestType | USB_DIR_OUT 或者 USB_TYPE_VENDOR |
request | 52 |
value | 0 |
index | stringID |
data | UTF-8字符 |
stringID定义如下(每个字符串最大长度为256,以/0结尾):
参数 | 描述 | 值 |
---|---|---|
manufacturer name | 制造商 | 0 |
model name | 型号 | 1 |
description | 描述 | 2 |
version | 版本 | 3 |
URI | 下载路径 | 4 |
serial number | 序列号 | 5 |
发送string ID后,使用53号控制命令请求设备在accessory模式下启动,使用如下命令:
参数 | 值 |
---|---|
requestType | USB_DIR_OUT 或者 USB_TYPE_VENDOR |
request | 53 |
value | 0 |
index | 0 |
data | none |
在AOA 2.0协议中,增加了对Audio的支持,将设备设置为音频模式(此命令必须在发送53号命令之前发送),控制请求描述如下:
参数 | 值 |
---|---|
requestType | USB_DIR_OUT 或者 USB_TYPE_VENDOR |
request | 58 |
value | 0 for no audio (default), 1 for 2 channel, 16-bit PCM at 44100 KHz |
index | 0 |
data | none |
package com.zdragon.videoio;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.io.UnsupportedEncodingException;
/**
* Created by GaddiZou on 2018/8/5 0005.
*/
public class UsbTest {
private final static String MANUFACTURER_NAME = "AoA Test MA";
private final static String MODEL_NAME = "AOA Test Model";
private final static String DESCRIPTION = "Description";
private final static String VERSION = "ver1.0";
private final static String URI = "http://www.downloadapk.com";
private final static String SERIAL = "111111111111111111";
private final static String REQUEST_PERMISSION_ACTION = "com.test.usb";
private UsbManager mUsbManager;
private PendingIntent mPendingIntent;
private HandlerThread mHandlerThread = new HandlerThread("usb-handler");
private UsbHandler mUsbHandler;
private class UsbHandler extends Handler {
public final static int MSG_REQUEST_PERMISSION = 0;
public final static int MSG_USB_ATTACHED = 1;
public final static int MSG_USB_DETACHED = 2;
public UsbHandler(Looper looper){
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int what = msg.what;
UsbDevice device = (UsbDevice) msg.obj;
switch(what){
case MSG_REQUEST_PERMISSION:
if (mUsbManager.hasPermission(device)) {
switchToAoAMode(device);
} else {
//No permission
}
break;
case MSG_USB_ATTACHED:
//TODO
break;
case MSG_USB_DETACHED:
//TODO
break;
}
}
}
private BroadcastReceiver mUsbDeviceEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
Message msg = mUsbHandler.obtainMessage();
msg.obj = device;
if(action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)){
msg.what = UsbHandler.MSG_USB_ATTACHED;
} else if(action.equals(UsbManager.ACTION_USB_ACCESSORY_DETACHED)){
msg.what = UsbHandler.MSG_USB_DETACHED;
} else if(action.equals(REQUEST_PERMISSION_ACTION)){
msg.what = UsbHandler.MSG_REQUEST_PERMISSION;
}
mUsbHandler.sendMessage(msg);
}
};
public UsbTest(Context context){
mHandlerThread.start();
mUsbHandler = new UsbHandler(mHandlerThread.getLooper());
mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
intentFilter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
intentFilter.addAction(REQUEST_PERMISSION_ACTION);
context.registerReceiver(mUsbDeviceEventReceiver, intentFilter);
mPendingIntent = PendingIntent.getBroadcast(context,0, new Intent(REQUEST_PERMISSION_ACTION), 0);
}
public void usbRequestPermission(UsbDevice usbDevice){
if(usbDevice == null){
return;
}
mUsbManager.requestPermission(usbDevice, mPendingIntent);
}
public void switchToAoAMode(UsbDevice usbDevice){
if(usbDevice.getVendorId() == 0x18D1 && (usbDevice.getProductId() == 0x2D00 ||
usbDevice.getProductId() == 0x2D01 ||
usbDevice.getProductId() == 0x2D02 ||
usbDevice.getProductId() == 0x2D03 ||
usbDevice.getProductId() == 0x2D04 ||
usbDevice.getProductId() == 0x2D05 )){
//UsbDevice in AOA mode.
sendAccessoryInfo(usbDevice);
}else{
UsbDeviceConnection udc = mUsbManager.openDevice(usbDevice);
byte [] datas = new byte[2];
int ret = udc.controlTransfer(UsbConstants.USB_DIR_IN, 51, 0,0, datas, 1, 0);
if(ret > 0 && datas[0] == 1){
//device has been switch to support AOA
sendAccessoryInfo(usbDevice);
}else{
//device not support AOA
}
}
}
public void sendAccessoryInfo(UsbDevice usbDevice){
UsbDeviceConnection udc = mUsbManager.openDevice(usbDevice);
int ret = -1;
try {
byte [] datas = MANUFACTURER_NAME.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,0, datas, datas.length, 0);
if(ret < 0){
return;
}
datas = MODEL_NAME.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,1, datas, datas.length, 0);
if(ret < 0){
return;
}
datas = DESCRIPTION.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,2, datas, datas.length, 0);
if(ret < 0){
return;
}
datas = VERSION.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,3, datas, datas.length, 0);
if(ret < 0){
return;
}
datas = URI.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,4, datas, datas.length, 0);
if(ret < 0){
return;
}
datas = SERIAL.getBytes("utf-8");
ret = udc.controlTransfer(UsbConstants.USB_DIR_OUT, 52, 0,5, datas, datas.length, 0);
if(ret < 0){
return;
}
//After send info ok, we start up accessory.
udc.controlTransfer(UsbConstants.USB_DIR_OUT, 53, 0,0, null, 0, 0);
} catch (UnsupportedEncodingException e) {
Log.e("ERROR", Log.getStackTraceString(e));
}
}
}