14.1 問題
應(yīng)用程序需要與USB設(shè)備進(jìn)行通信來控制或傳輸數(shù)據(jù)。
14.2 解決方案
(API Level 12)
對(duì)于擁有USB主機(jī)電路的設(shè)備,Android以及內(nèi)置了對(duì)它的支持,可以與已經(jīng)連接的USB設(shè)備進(jìn)行模擬和通信。USBManager是一項(xiàng)系統(tǒng)服務(wù),可以讓應(yīng)用程序訪問任何通過USB連接的外部設(shè)備,接下來我們將看一下在應(yīng)用程序中如何使用這個(gè)服務(wù)來建立連接。
設(shè)備上的USB主機(jī)電路已經(jīng)越來越普及,但還是很普及,但還是很稀少。剛開始,只有平板電腦設(shè)備擁有這種能力,但隨著科技的快速發(fā)展,在商用Android手機(jī)上它也可能很快成為一個(gè)通用的接口。正因?yàn)槿绱耍瑹o疑需要在應(yīng)用程序的清單中中包含以下元素:
<uses-feature android:name="android.hareware.usb.host"/>
這樣只有真正擁有相應(yīng)硬件的設(shè)備,才可以使用你的應(yīng)用程序。
Android提供的API和USB規(guī)范幾乎一樣,并沒有更多更深入的知識(shí)。這就意味著如果想要使用這些API,你至少需要了解一些USB的基礎(chǔ)知識(shí)以及設(shè)備間是如何通信的。
USB概述
在查看Android是如何與USB設(shè)備進(jìn)行交互的示例之前,讓我們花點(diǎn)時(shí)間定義一些USB術(shù)語。
- 端點(diǎn):USB設(shè)備的最小構(gòu)件。應(yīng)用程序最終就是通過連接這些端點(diǎn)發(fā)送和接收數(shù)據(jù)的。端點(diǎn)主要分為4種類型:
控件傳輸:用于配置和狀態(tài)命名。每臺(tái)設(shè)備至少有一個(gè)控制端點(diǎn),即“端點(diǎn)0”,它不會(huì)關(guān)聯(lián)任何接口。
中斷傳輸:用于小量的、高優(yōu)先級(jí)的控制命令。
批量傳輸:用于傳輸大數(shù)據(jù)。通常都是雙向成對(duì)出現(xiàn)的(1IN和1OUT)。
同步傳輸:用于實(shí)時(shí)數(shù)據(jù)傳輸,如音頻。撰寫本書時(shí),最新的Android SDK還不支持這個(gè)功能。 - 接口:端點(diǎn)的集合,用來表示一臺(tái)“邏輯”設(shè)備。
多臺(tái)設(shè)備USB設(shè)備對(duì)于主機(jī)來說可以呈現(xiàn)為多臺(tái)邏輯設(shè)備,即通過暴露多個(gè)接口來標(biāo)識(shí)。 - 配置:一個(gè)或多個(gè)接口的集合。USB協(xié)議強(qiáng)制規(guī)定一臺(tái)設(shè)備在某個(gè)特定時(shí)間只能有一個(gè)配置是激活的。事實(shí)上,多數(shù)設(shè)備也就只有一個(gè)配置,并把它作為設(shè)備的操作模式。
14.3 實(shí)現(xiàn)機(jī)制
以下兩段清單代碼演示了使用UsbManager來檢查通過USB連接的設(shè)備以及使用控制傳輸來進(jìn)一步查詢配置的示例。
res/layout/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Connect"
android:onClick="onConnectClick" />
<TextView
android:id="@+id/text_status"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text_data"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
USB主機(jī)上查詢?cè)O(shè)備的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);
//檢查當(dāng)前連接的設(shè)備
updateDeviceList();
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mUsbReceiver);
}
public void onConnectClick(View v) {
if (mDevice == null) {
return;
}
mDisplayText.setText("---");
//這里如果用戶已經(jīng)授權(quán),就會(huì)立即發(fā)送ACTION_USB_PERMISSION
// 否則會(huì)向用戶顯示授權(quán)對(duì)話框
mUsbManager.requestPermission(mDevice, mPermissionIntent);
}
/*
* 捕捉用戶權(quán)限響應(yīng)的接收器,在和已經(jīng)連接的設(shè)備進(jìn)行真正的交互時(shí)是需要這些權(quán)限的
*/
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) {
//查詢?cè)O(shè)備的描述符
getDeviceStatus(device);
} else {
Log.d(TAG, "permission denied for device " + device);
}
}
}
};
//類型: 表示讀寫還是寫入
// 與USB_ENDPOINT_DIR_MASK 進(jìn)行匹配,判斷IN還是OUT
private static final int REQUEST_TYPE = 0x80;
//請(qǐng)求: 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;
/**
*初始化控制傳輸來請(qǐng)求設(shè)備的第一個(gè)配置描述符
*/
private void getDeviceStatus(UsbDevice device) {
UsbDeviceConnection connection = mUsbManager.openDevice(device);
//為傳入的數(shù)據(jù)創(chuàng)建一個(gè)足夠大的緩沖區(qū)
byte[] buffer = new byte[LENGTH];
connection.controlTransfer(REQUEST_TYPE, REQUEST, REQ_VALUE, REQ_INDEX,
buffer, LENGTH, 2000);
//將接收到的數(shù)據(jù)解析為描述符
String description = parseConfigDescriptor(buffer);
mDisplayText.setText(description);
connection.close();
}
/*
* 按照 USB 規(guī)范解析USB 配置描述符響應(yīng)信息。返回可打印的連接設(shè)備的信息
*/
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);
//接口數(shù)量
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) {
//讀取長(zhǎng)度和類型
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: //端點(diǎn)描述符
int endpointAddr = ((buffer[index+2] & 0xFF));
//端口號(hào)為 4 位
int endpointNum = (endpointAddr & 0x0F);
//方向?yàn)榭瘴? 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;
}
//繼續(xù)下一個(gè)描述符
index += len;
}
return sb.toString();
}
private void updateDeviceList() {
HashMap<String, UsbDevice> 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()) {
//打開最后一臺(tái) (如果有多臺(tái)的話) 檢測(cè)到的設(shè)備
mDevice = device;
builder.append(readDevice(device));
builder.append("\n\n");
}
mDeviceText.setText(builder.toString());
mConnectButton.setEnabled(true);
}
}
/*
* 遍歷所有已經(jīng)連接的設(shè)備的端口和接口
* 這里不涉及權(quán)限,在嘗試連接真實(shí)設(shè)備之前這些都是“公開可用”的
*/
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 常量提供可讀性更強(qiáng)的名稱 */
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";
}
}
}
當(dāng)Activtiy首次進(jìn)入前臺(tái)時(shí),它注冊(cè)一個(gè)自定義動(dòng)作的BroadcastReceiver,并且通過UsbManager.getDeviceList()方法來查詢當(dāng)前已連接舍不得列表,該方法會(huì)返回一個(gè)UsbDevice項(xiàng)的HashMap,然后就可以遍歷和查詢這個(gè)HashMap。對(duì)于每臺(tái)連接的設(shè)備,我們會(huì)查詢它的接口和端口,并且會(huì)構(gòu)建 需要顯示給用戶的每臺(tái)設(shè)備的描述信息。然后,我們會(huì)在用戶界面上打印所有這些信息。
注意:
就目前來說,這個(gè)應(yīng)用程序不需要在清單中聲明任何權(quán)限。對(duì)于只是簡(jiǎn)單地查詢連接到主機(jī)的設(shè)備的信息,并不需要聲明權(quán)限。
如你所見,對(duì)于你想與之通信的連接設(shè)備,UsbManager提供的API可以獲得你想要的所有信息。所有標(biāo)準(zhǔn)的定義,如設(shè)備種類、端點(diǎn)類型和傳輸方向也都在UsbManager中做了定義,所以不需要自己定義就可以匹配想要的類型。
那么為什么要注冊(cè)BroadcastReceiver呢?在用戶按下屏幕上的Connect按鈕后,這個(gè)示例的剩余部分做了相應(yīng)的響應(yīng)。這時(shí)候我們想要與連接的設(shè)備進(jìn)行真正的交互,這時(shí)候就需要用戶權(quán)限。在此,當(dāng)用戶單擊按鈕時(shí),會(huì)調(diào)用UsbManager.requestPermission()來詢問用戶是否可以連接。如果還沒有授權(quán)相應(yīng)的權(quán)限,用戶會(huì)看到詢問授權(quán)連接的對(duì)話框。
如果選擇確認(rèn)授權(quán),傳入方法的PendingIntent就會(huì)被觸發(fā)。在示例中,這個(gè)Intent是通過自定義動(dòng)作字符串做廣播的,此時(shí)會(huì)觸發(fā)BroadcastReceiver的onReceiver()方法;接下來任何的requestPermission()調(diào)用都會(huì)立即觸發(fā)這個(gè)接收器。在接收器內(nèi)部,我們會(huì)檢查以確保結(jié)果是授權(quán)響應(yīng)并通過UsbManager.openDeceive()打開與設(shè)備的連接,如果連接成功,則會(huì)返回一個(gè)UsbManagerConnection實(shí)例。
對(duì)于有效的連接,我們會(huì)通過控制傳輸來請(qǐng)求設(shè)備的配置描述符,從而得到設(shè)備更加詳細(xì)的信息??刂苽鬏斠话愣际峭ㄟ^設(shè)備的“端口0”來請(qǐng)求的。我們則分配一個(gè)合適大小的緩沖區(qū)來保證可以得到所有的信息。
controlTransfer()返回后,緩沖區(qū)中已經(jīng)填好了響應(yīng)數(shù)據(jù)。接下來應(yīng)用程序會(huì)處理這些數(shù)據(jù),得到設(shè)備的一些詳細(xì)信息,例如設(shè)備的最大能耗以及設(shè)備是使用USB供電(總線供電)還是其他方式外部供電(自供電)。這個(gè)示例只是從這些標(biāo)識(shí)符中解析出一小部分有用的信息。同樣,所有解析出來的數(shù)據(jù)就會(huì)被放到一個(gè)字符串報(bào)告中并顯示在用戶界面上。
第一節(jié)中從框架API讀取的信息和第二節(jié)中直接從設(shè)備讀取的信息是一樣的,并且按照1:1的比例通過兩個(gè)文本報(bào)告顯示在用戶屏幕上。需要注意的一點(diǎn)就是,只有在設(shè)備連接上時(shí)應(yīng)用程序才會(huì)工作:對(duì)于應(yīng)用程序在前臺(tái)運(yùn)行時(shí)才連接的設(shè)備,應(yīng)用程序并不會(huì)得到通知。
獲取設(shè)備連接時(shí)的通知
要想在Android在設(shè)備連接時(shí)可以通知你的應(yīng)用程序,需要在清單中通過<intent-filter>注冊(cè)要匹配的設(shè)備類型。以下兩段代碼清單演示了這個(gè)過程。
AndroidManifest.xml中的部分代碼
<activity
android:name=".USBActivity"
android:label="@string/title_activity_usb" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
res/xml/device_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device />
</resources>
能夠處理設(shè)備連接的Activity添加一個(gè)名為USB_DEVICE_ATTACHED動(dòng)作字符串的過濾器和描述想要處理設(shè)備的一些XML元數(shù)據(jù)信息??梢?lt;usb-device>中添加很多設(shè)備屬性字段,從而過濾哪些連接事件可以通知到應(yīng)用程序:
- vendor-id
- product-id
- class
- subclass
- protocol
必要時(shí),可以定義以上很多屬性來適應(yīng)你的應(yīng)用程序。例如,如果只想和某一臺(tái)特定設(shè)備進(jìn)行通信,或許可以想示例代碼一樣同時(shí)定義vendor-id和product-id。如果相匹配某一類型的設(shè)備(例如,所有的大數(shù)據(jù)存儲(chǔ)設(shè)備),或許只需要定義class屬性即可。甚至可以不定義任何屬性,這樣應(yīng)用程序就可以匹配所有連接的設(shè)備。