14.USB連線
14.1 問題
14.2 解決方案
(API Level 12)
對於擁有USB主機電路的裝置,Android以及內建了對它的支援,可以與已經連線的USB裝置進行模擬和通訊。USBManager是一項系統服務,可以讓應用程式訪問任何通過USB連線的外部裝置,接下來我們將看一下在應用程式中如何使用這個服務來建立連線。
裝置上的USB主機電路已經越來越普及,但還是很普及,但還是很稀少。剛開始,只有平板電腦裝置擁有這種能力,但隨著科技的快速發展,在商用Android手機上它也可能很快成為一個通用的介面。正因為如此,無疑需要在應用程式的清單中中包含以下元素:
<uses-feature android:name="android.hareware.usb.host"/>
這樣只有真正擁有相應硬體的裝置,才可以使用你的應用程式。
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
<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主機上查詢裝置的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<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()) { //開啟最後一臺 (如果有多臺的話) 檢測到的裝置 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在裝置連線時可以通知你的應用程式,需要在清單中通過<intent-filter>註冊要匹配的裝置型別。以下兩段程式碼清單演示了這個過程。
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>
能夠處理裝置連線的Activity新增一個名為USB_DEVICE_ATTACHED動作字串的過濾器和描述想要處理裝置的一些XML元資料資訊。可以<usb-device>中新增很多裝置屬性欄位,從而過濾哪些連線事件可以通知到應用程式:
- vendor-id
- product-id
- class
- subclass
- protocol
必要時,可以定義以上很多屬性來適應你的應用程式。例如,如果只想和某一臺特定裝置進行通訊,或許可以想示例程式碼一樣同時定義vendor-id和product-id。如果相匹配某一型別的裝置(例如,所有的大資料儲存裝置),或許只需要定義class屬性即可。甚至可以不定義任何屬性,這樣應用程式就可以匹配所有連線的裝置。