14.1 问题
应用程序需要与USB设备进行通信来控制或传输数据。
14.2 解决方案
(API Level 12)
对于拥有USB主机电路的设备,Android以及内置了对它的支持,可以与已经连接的USB设备进行模拟和通信。USBManager是一项系统服务,可以让应用程序访问任何通过USB连接的外部设备,接下来我们将看一下在应用程序中如何使用这个服务来建立连接。
设备上的USB主机电路已经越来越普及,但还是很普及,但还是很稀少。刚开始,只有平板电脑设备拥有这种能力,但随着科技的快速发展,在商用Android手机上它也可能很快成为一个通用的接口。正因为如此,无疑需要在应用程序的清单中中包含以下元素:
这样只有真正拥有相应硬件的设备,才可以使用你的应用程序。
Android提供的API和USB规范几乎一样,并没有更多更深入的知识。这就意味着如果想要使用这些API,你至少需要了解一些USB的基础知识以及设备间是如何通信的。
USB概述
在查看Android是如何与USB设备进行交互的示例之前,让我们花点时间定义一些USB术语。
- 端点:USB设备的最小构件。应用程序最终就是通过连接这些端点发送和接收数据的。端点主要分为4种类型:
控件传输:用于配置和状态命名。每台设备至少有一个控制端点,即“端点0”,它不会关联任何接口。
中断传输:用于小量的、高优先级的控制命令。
批量传输:用于传输大数据。通常都是双向成对出现的(1IN和1OUT)。
同步传输:用于实时数据传输,如音频。撰写本书时,最新的Android SDK还不支持这个功能。 - 接口:端点的集合,用来表示一台“逻辑”设备。
多台设备USB设备对于主机来说可以呈现为多台逻辑设备,即通过暴露多个接口来标识。 - 配置:一个或多个接口的集合。USB协议强制规定一台设备在某个特定时间只能有一个配置是激活的。事实上,多数设备也就只有一个配置,并把它作为设备的操作模式。
14.3 实现机制
以下两段清单代码演示了使用UsbManager来检查通过USB连接的设备以及使用控制传输来进一步查询配置的示例。
res/layout/main.xml
USB主机上查询设备的Activity
public class USBActivity extends Activity {
private static final String TAG = "UsbHost";
TextView mDeviceText, mDisplayText;
Button mConnectButton;
UsbManager mUsbManager;
UsbDevice mDevice;
PendingIntent mPermissionIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mDeviceText = (TextView) findViewById(R.id.text_status);
mDisplayText = (TextView) findViewById(R.id.text_data);
mConnectButton = (Button) findViewById(R.id.button_connect);
mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
}
@Override
protected void onResume() {
super.onResume();
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);
//检查当前连接的设备
updateDeviceList();
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mUsbReceiver);
}
public void onConnectClick(View v) {
if (mDevice == null) {
return;
}
mDisplayText.setText("---");
//这里如果用户已经授权,就会立即发送ACTION_USB_PERMISSION
// 否则会向用户显示授权对话框
mUsbManager.requestPermission(mDevice, mPermissionIntent);
}
/*
* 捕捉用户权限响应的接收器,在和已经连接的设备进行真正的交互时是需要这些权限的
*/
private static final String ACTION_USB_PERMISSION = "com.android.recipes.USB_PERMISSION";
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
&& device != null) {
//查询设备的描述符
getDeviceStatus(device);
} else {
Log.d(TAG, "permission denied for device " + device);
}
}
}
};
//类型: 表示读写还是写入
// 与USB_ENDPOINT_DIR_MASK 进行匹配,判断IN还是OUT
private static final int REQUEST_TYPE = 0x80;
//请求: GET_CONFIGURATION_DESCRIPTOR = 0x06
private static final int REQUEST = 0x06;
//值: 描述符类型 (高) 和索引值 (低)
// Configuration Descriptor = 0x2
// Index = 0x0 (第一次配置)
private static final int REQ_VALUE = 0x200;
private static final int REQ_INDEX = 0x00;
private static final int LENGTH = 64;
/**
*初始化控制传输来请求设备的第一个配置描述符
*/
private void getDeviceStatus(UsbDevice device) {
UsbDeviceConnection connection = mUsbManager.openDevice(device);
//为传入的数据创建一个足够大的缓冲区
byte[] buffer = new byte[LENGTH];
connection.controlTransfer(REQUEST_TYPE, REQUEST, REQ_VALUE, REQ_INDEX,
buffer, LENGTH, 2000);
//将接收到的数据解析为描述符
String description = parseConfigDescriptor(buffer);
mDisplayText.setText(description);
connection.close();
}
/*
* 按照 USB 规范解析USB 配置描述符响应信息。返回可打印的连接设备的信息
*/
private static final int DESC_SIZE_CONFIG = 9;
private String parseConfigDescriptor(byte[] buffer) {
StringBuilder sb = new StringBuilder();
//解析配置描述符的头信息
int totalLength = (buffer[3] &0xFF) << 8;
totalLength += (buffer[2] & 0xFF);
//接口数量
int numInterfaces = (buffer[5] & 0xFF);
//配置的属性
int attributes = (buffer[7] & 0xFF);
//电量递增2mA
int maxPower = (buffer[8] & 0xFF) * 2;
sb.append("Configuration Descriptor:\n");
sb.append("Length: " + totalLength + " bytes\n");
sb.append(numInterfaces + " Interfaces\n");
sb.append(String.format("Attributes:%s%s%s\n",
(attributes & 0x80) == 0x80 ? " BusPowered" : "",
(attributes & 0x40) == 0x40 ? " SelfPowered" : "",
(attributes & 0x20) == 0x20 ? " RemoteWakeup" : ""));
sb.append("Max Power: " + maxPower + "mA\n");
//描述符的剩余部分为接口和端口信息
int index = DESC_SIZE_CONFIG;
while (index < totalLength) {
//读取长度和类型
int len = (buffer[index] & 0xFF);
int type = (buffer[index+1] & 0xFF);
switch (type) {
case 0x04: //接口描述符
int intfNumber = (buffer[index+2] & 0xFF);
int numEndpoints = (buffer[index+4] & 0xFF);
int intfClass = (buffer[index+5] & 0xFF);
sb.append(String.format("- Interface %d, %s, %d Endpoints\n",
intfNumber, nameForClass(intfClass), numEndpoints));
break;
case 0x05: //端点描述符
int endpointAddr = ((buffer[index+2] & 0xFF));
//端口号为 4 位
int endpointNum = (endpointAddr & 0x0F);
//方向为空位
int direction = (endpointAddr & 0x80);
int endpointAttrs = (buffer[index+3] & 0xFF);
//类型为低两位
int endpointType = (endpointAttrs & 0x3);
sb.append(String.format("-- Endpoint %d, %s %s\n",
endpointNum,
nameForEndpointType(endpointType),
nameForDirection(direction) ));
break;
}
//继续下一个描述符
index += len;
}
return sb.toString();
}
private void updateDeviceList() {
HashMap connectedDevices = mUsbManager
.getDeviceList();
if (connectedDevices.isEmpty()) {
mDevice = null;
mDeviceText.setText("No Devices Currently Connected");
mConnectButton.setEnabled(false);
} else {
StringBuilder builder = new StringBuilder();
for (UsbDevice device : connectedDevices.values()) {
//打开最后一台 (如果有多台的话) 检测到的设备
mDevice = device;
builder.append(readDevice(device));
builder.append("\n\n");
}
mDeviceText.setText(builder.toString());
mConnectButton.setEnabled(true);
}
}
/*
* 遍历所有已经连接的设备的端口和接口
* 这里不涉及权限,在尝试连接真实设备之前这些都是“公开可用”的
*/
private String readDevice(UsbDevice device) {
StringBuilder sb = new StringBuilder();
sb.append("Device Name: " + device.getDeviceName() + "\n");
sb.append(String.format(
"Device Class: %s -> Subclass: 0x%02x -> Protocol: 0x%02x\n",
nameForClass(device.getDeviceClass()),
device.getDeviceSubclass(), device.getDeviceProtocol()));
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface intf = device.getInterface(i);
sb.append(String
.format("+--Interface %d Class: %s -> Subclass: 0x%02x -> Protocol: 0x%02x\n",
intf.getId(),
nameForClass(intf.getInterfaceClass()),
intf.getInterfaceSubclass(),
intf.getInterfaceProtocol()));
for (int j = 0; j < intf.getEndpointCount(); j++) {
UsbEndpoint endpoint = intf.getEndpoint(j);
sb.append(String.format(" +---Endpoint %d: %s %s\n",
endpoint.getEndpointNumber(),
nameForEndpointType(endpoint.getType()),
nameForDirection(endpoint.getDirection())));
}
}
return sb.toString();
}
/* 辅助方法,用来为 USB 常量提供可读性更强的名称 */
private String nameForClass(int classType) {
switch (classType) {
case UsbConstants.USB_CLASS_APP_SPEC:
return String.format("Application Specific 0x%02x", classType);
case UsbConstants.USB_CLASS_AUDIO:
return "Audio";
case UsbConstants.USB_CLASS_CDC_DATA:
return "CDC Control";
case UsbConstants.USB_CLASS_COMM:
return "Communications";
case UsbConstants.USB_CLASS_CONTENT_SEC:
return "Content Security";
case UsbConstants.USB_CLASS_CSCID:
return "Content Smart Card";
case UsbConstants.USB_CLASS_HID:
return "Human Interface Device";
case UsbConstants.USB_CLASS_HUB:
return "Hub";
case UsbConstants.USB_CLASS_MASS_STORAGE:
return "Mass Storage";
case UsbConstants.USB_CLASS_MISC:
return "Wireless Miscellaneous";
case UsbConstants.USB_CLASS_PER_INTERFACE:
return "(Defined Per Interface)";
case UsbConstants.USB_CLASS_PHYSICA:
return "Physical";
case UsbConstants.USB_CLASS_PRINTER:
return "Printer";
case UsbConstants.USB_CLASS_STILL_IMAGE:
return "Still Image";
case UsbConstants.USB_CLASS_VENDOR_SPEC:
return String.format("Vendor Specific 0x%02x", classType);
case UsbConstants.USB_CLASS_VIDEO:
return "Video";
case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER:
return "Wireless Controller";
default:
return String.format("0x%02x", classType);
}
}
private String nameForEndpointType(int type) {
switch (type) {
case UsbConstants.USB_ENDPOINT_XFER_BULK:
return "Bulk";
case UsbConstants.USB_ENDPOINT_XFER_CONTROL:
return "Control";
case UsbConstants.USB_ENDPOINT_XFER_INT:
return "Interrupt";
case UsbConstants.USB_ENDPOINT_XFER_ISOC:
return "Isochronous";
default:
return "Unknown Type";
}
}
private String nameForDirection(int direction) {
switch (direction) {
case UsbConstants.USB_DIR_IN:
return "IN";
case UsbConstants.USB_DIR_OUT:
return "OUT";
default:
return "Unknown Direction";
}
}
}
当Activtiy首次进入前台时,它注册一个自定义动作的BroadcastReceiver,并且通过UsbManager.getDeviceList()方法来查询当前已连接舍不得列表,该方法会返回一个UsbDevice项的HashMap,然后就可以遍历和查询这个HashMap。对于每台连接的设备,我们会查询它的接口和端口,并且会构建 需要显示给用户的每台设备的描述信息。然后,我们会在用户界面上打印所有这些信息。
注意:
就目前来说,这个应用程序不需要在清单中声明任何权限。对于只是简单地查询连接到主机的设备的信息,并不需要声明权限。
如你所见,对于你想与之通信的连接设备,UsbManager提供的API可以获得你想要的所有信息。所有标准的定义,如设备种类、端点类型和传输方向也都在UsbManager中做了定义,所以不需要自己定义就可以匹配想要的类型。
那么为什么要注册BroadcastReceiver呢?在用户按下屏幕上的Connect按钮后,这个示例的剩余部分做了相应的响应。这时候我们想要与连接的设备进行真正的交互,这时候就需要用户权限。在此,当用户单击按钮时,会调用UsbManager.requestPermission()来询问用户是否可以连接。如果还没有授权相应的权限,用户会看到询问授权连接的对话框。
如果选择确认授权,传入方法的PendingIntent就会被触发。在示例中,这个Intent是通过自定义动作字符串做广播的,此时会触发BroadcastReceiver的onReceiver()方法;接下来任何的requestPermission()调用都会立即触发这个接收器。在接收器内部,我们会检查以确保结果是授权响应并通过UsbManager.openDeceive()打开与设备的连接,如果连接成功,则会返回一个UsbManagerConnection实例。
对于有效的连接,我们会通过控制传输来请求设备的配置描述符,从而得到设备更加详细的信息。控制传输一般都是通过设备的“端口0”来请求的。我们则分配一个合适大小的缓冲区来保证可以得到所有的信息。
controlTransfer()返回后,缓冲区中已经填好了响应数据。接下来应用程序会处理这些数据,得到设备的一些详细信息,例如设备的最大能耗以及设备是使用USB供电(总线供电)还是其他方式外部供电(自供电)。这个示例只是从这些标识符中解析出一小部分有用的信息。同样,所有解析出来的数据就会被放到一个字符串报告中并显示在用户界面上。
第一节中从框架API读取的信息和第二节中直接从设备读取的信息是一样的,并且按照1:1的比例通过两个文本报告显示在用户屏幕上。需要注意的一点就是,只有在设备连接上时应用程序才会工作:对于应用程序在前台运行时才连接的设备,应用程序并不会得到通知。
获取设备连接时的通知
要想在Android在设备连接时可以通知你的应用程序,需要在清单中通过
AndroidManifest.xml中的部分代码
res/xml/device_filter.xml
能够处理设备连接的Activity添加一个名为USB_DEVICE_ATTACHED动作字符串的过滤器和描述想要处理设备的一些XML元数据信息。可以
- vendor-id
- product-id
- class
- subclass
- protocol
必要时,可以定义以上很多属性来适应你的应用程序。例如,如果只想和某一台特定设备进行通信,或许可以想示例代码一样同时定义vendor-id和product-id。如果相匹配某一类型的设备(例如,所有的大数据存储设备),或许只需要定义class属性即可。甚至可以不定义任何属性,这样应用程序就可以匹配所有连接的设备。