USB 主機總覽

當 Android 裝置處於 USB 主機模式時,可做為 USB 主機的供電,並列舉連接的 USB 裝置。Android 3.1 以上版本支援 USB 主機模式。

API 總覽

在開始之前,請務必瞭解要使用的類別。下表說明 android.hardware.usb 套件中的 USB 主機 API。

表 1. USB 主機 API

類別 說明
UsbManager 允許您列舉和通訊的 USB 裝置。
UsbDevice 代表已連線的 USB 裝置,並包含存取裝置識別資訊、介面和端點的方法。
UsbInterface 代表 USB 裝置的介面,可定義裝置的一組功能。每部裝置都可以有一或多個介面進行通訊。
UsbEndpoint 代表介面端點,是這個介面的通訊管道。介面可以有一或多個端點,且通常具備可與裝置雙向通訊的輸入和輸出端點。
UsbDeviceConnection 代表與裝置的連線,可在端點上傳輸資料。這個類別可讓您以同步或非同步的方式來回傳送資料。
UsbRequest 代表透過 UsbDeviceConnection 與裝置通訊的非同步要求。
UsbConstants 定義與 Linux 核心 linux/usb/ch9.h 定義的 USB 常數。

在多數情況下,當您與 USB 裝置通訊時,必須使用所有類別 (只有在進行非同步通訊時才需要 UsbRequest)。一般來說,您會取得 UsbManager 來擷取所需的 UsbDevice。使用裝置時,您需要找出該介面的適當 UsbInterfaceUsbEndpoint 以供通訊。取得正確的端點後,開啟 UsbDeviceConnection 與 USB 裝置通訊。

Android 資訊清單規定

以下清單說明使用 USB 主機 API 前,需要新增至應用程式的資訊清單檔案:

  • 由於並非所有 Android 裝置都支援 USB 主機 API,因此請加入 <uses-feature> 元素,宣告應用程式使用 android.hardware.usb.host 功能。
  • 將應用程式的最低 SDK 設為 API 級別 12 以上。USB 主機 API 不包含在先前的 API 級別中。
  • 如果您希望應用程式收到連接的 USB 裝置通知,請在主要活動中為 android.hardware.usb.action.USB_DEVICE_ATTACHED 意圖指定 <intent-filter><meta-data> 元素配對。<meta-data> 元素會指向外部 XML 資源檔案,宣告要偵測的裝置識別資訊。

    在 XML 資源檔案中,為您要篩選的 USB 裝置宣告 <usb-device> 元素。以下清單說明 <usb-device> 的屬性。一般來說,如要篩選出特定裝置,並使用類別、子類別和通訊協定來篩選一組 USB 裝置 (例如大量儲存裝置或數位相機),請使用廠商和產品 ID。您可以指定完全不或所有屬性。未指定與每部 USB 裝置相符的屬性,因此只有在應用程式需要屬性時,才需要這樣做:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (裝置或介面)

    將資源檔案儲存在 res/xml/ 目錄中。資源檔案名稱 (不含 .xml 副檔名) 必須與您在 <meta-data> 元素中指定的名稱相同。XML 資源檔案的格式為以下範例

資訊清單和資源檔案範例

以下範例顯示資訊清單範例及其對應的資源檔案:

<manifest ...>
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk android:minSdkVersion="12" />
    ...
    <application>
        <activity ...>
            ...
            <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>
    </application>
</manifest>

在這種情況下,下列資源檔案應儲存在 res/xml/device_filter.xml 中,並指定應篩選所有具有指定屬性的 USB 裝置:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>

使用裝置

當使用者將 USB 裝置連線至 Android 裝置時,Android 系統可判斷您的應用程式是否對已連結的裝置感興趣。如果是的話,您可以視需要設定與裝置之間的通訊。如要這麼做,您的應用程式必須:

  1. 透過意圖篩選器,在使用者連接 USB 裝置或列舉已連接的 USB 裝置時收到通知,藉此找出已連接的 USB 裝置。
  2. 如果尚未取得 USB 裝置連線的權限,請要求使用者授權。
  3. 在適當的介面端點上讀取及寫入資料,以便與 USB 裝置通訊。

探索裝置

應用程式可透過意圖篩選器,在使用者連接裝置時收到通知,或列舉已連接的 USB 裝置,藉此偵測 USB 裝置。如果希望應用程式能自動偵測所需的裝置,使用意圖篩選器就會很實用。如果您希望取得所有已連結裝置的清單,或應用程式未篩選意圖,列舉已連接的 USB 裝置會相當實用。

使用意圖篩選器

如要讓應用程式探索特定 USB 裝置,您可以指定意圖篩選器來篩選出 android.hardware.usb.action.USB_DEVICE_ATTACHED 意圖。除了使用這項意圖篩選器之外,您還必須指定資源檔案,以指定 USB 裝置的屬性,例如產品和供應商 ID。當使用者連線的裝置符合裝置篩選器時,系統會顯示對話方塊,詢問他們是否要啟動應用程式。如果使用者接受邀請,您的應用程式會自動取得裝置存取權,直到裝置中斷連線為止。

以下範例說明如何宣告意圖篩選器:

<activity ...>
...
    <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>

以下範例說明瞭如何宣告指定所需 USB 裝置的對應資源檔案:

<?xml version="1.0" encoding="utf-8"?>

<resources>
    <usb-device vendor-id="1234" product-id="5678" />
</resources>

在活動中,您可以從意圖取得代表已連結裝置的 UsbDevice,如下所示:

Kotlin

val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

Java

UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

列舉裝置

如果您的應用程式想在應用程式執行期間檢查目前連線的所有 USB 裝置,可以列舉公車上的裝置。使用 getDeviceList() 方法取得所有已連接的 USB 裝置的雜湊對應。如要從地圖取得裝置,雜湊對應會以 USB 裝置名稱組成的索引鍵。

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
val deviceList = manager.getDeviceList()
val device = deviceList.get("deviceName")

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
UsbDevice device = deviceList.get("deviceName");

如有需要,您也可以直接從雜湊對應取得疊代器,逐一處理每部裝置:

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
..
val deviceList: HashMap<String, UsbDevice> = manager.deviceList
deviceList.values.forEach { device ->
    // your code
}

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
while(deviceIterator.hasNext()){
    UsbDevice device = deviceIterator.next();
    // your code
}

取得與裝置通訊的權限

與 USB 裝置通訊前,應用程式必須先取得使用者的授權。

注意:如果應用程式使用意圖篩選器探索連線的 USB 裝置,當使用者允許應用程式處理意圖時,應用程式會自動取得權限。如果不能,您必須先在應用程式中明確要求權限,才能連線至裝置。

在某些情況下,您可能需要明確要求權限,例如應用程式列舉已連接的 USB 裝置,然後想要與該裝置通訊時。在嘗試與裝置通訊前,您必須先檢查裝置的存取權限。否則,如果使用者拒絕存取裝置,您會收到執行階段錯誤。

如要明確取得權限,請先建立廣播接收器。這個接收器會監聽您呼叫 requestPermission() 時廣播的意圖。呼叫 requestPermission() 會向使用者顯示對話方塊,要求他們授予裝置連線權限。下列程式碼範例說明如何建立廣播接收器:

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"

private val usbReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (ACTION_USB_PERMISSION == intent.action) {
            synchronized(this) {
                val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    device?.apply {
                        // call method to set up device communication
                    }
                } else {
                    Log.d(TAG, "permission denied for device $device")
                }
            }
        }
    }
}

Java

private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (ACTION_USB_PERMISSION.equals(action)) {
            synchronized (this) {
                UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    if(device != null){
                      // call method to set up device communication
                   }
                }
                else {
                    Log.d(TAG, "permission denied for device " + device);
                }
            }
        }
    }
};

如要註冊廣播接收器,請在活動的 onCreate() 方法中新增:

Kotlin

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
...
val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)

Java

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
private static final String ACTION_USB_PERMISSION =
    "com.android.example.USB_PERMISSION";
...
permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter);

如要顯示要求使用者授予裝置連線權限的對話方塊,請呼叫 requestPermission() 方法:

Kotlin

lateinit var device: UsbDevice
...
usbManager.requestPermission(device, permissionIntent)

Java

UsbDevice device;
...
usbManager.requestPermission(device, permissionIntent);

當使用者回覆對話方塊時,廣播接收器會收到包含 EXTRA_PERMISSION_GRANTED 額外項目的意圖,這是代表答案的布林值。連線至裝置前,請先檢查這些額外項目是否為 true 的值。

與裝置通訊

與 USB 裝置的通訊可以同步或非同步。無論是哪一種情況,您都應建立新的執行緒來執行所有資料傳輸,以免封鎖 UI 執行緒。如要正確設定與裝置的通訊,您必須針對要連線的裝置,取得適當的 UsbInterfaceUsbEndpoint,並透過 UsbDeviceConnection 在這個端點上傳送要求。一般來說,您的程式碼應:

  • 查看 UsbDevice 物件的屬性,例如產品 ID、供應商 ID 或裝置類別,判斷是否要與裝置通訊。
  • 當您確定與裝置通訊時,請找出要用來與介面的適當 UsbEndpoint 通訊的適當 UsbInterface。介面可以有一或多個端點,通常也會有雙向通訊的輸入和輸出端點。
  • 找到正確的端點後,在該端點上開啟 UsbDeviceConnection
  • 使用 bulkTransfer()controlTransfer() 方法提供您要在端點上傳輸的資料。您必須在其他執行緒中執行這個步驟,以免封鎖主 UI 執行緒。如果想進一步瞭解如何在 Android 中使用執行緒,請參閱「處理程序和執行緒」。

以下程式碼片段是執行同步資料移轉的簡單方法。程式碼應包含更多邏輯,才能正確找到要用於通訊的正確介面和端點,也應執行與主要 UI 執行緒不同執行緒中的資料傳輸:

Kotlin

private lateinit var bytes: ByteArray
private val TIMEOUT = 0
private val forceClaim = true

...

device?.getInterface(0)?.also { intf ->
    intf.getEndpoint(0)?.also { endpoint ->
        usbManager.openDevice(device)?.apply {
            claimInterface(intf, forceClaim)
            bulkTransfer(endpoint, bytes, bytes.size, TIMEOUT) //do in another thread
        }
    }
}

Java

private Byte[] bytes;
private static int TIMEOUT = 0;
private boolean forceClaim = true;

...

UsbInterface intf = device.getInterface(0);
UsbEndpoint endpoint = intf.getEndpoint(0);
UsbDeviceConnection connection = usbManager.openDevice(device);
connection.claimInterface(intf, forceClaim);
connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); //do in another thread

如要以非同步方式傳送資料,請使用 UsbRequest 類別至 initializequeue 非同步要求,然後使用 requestWait() 等待結果。

終止與裝置的通訊

當您完成與裝置的通訊,或如果裝置已卸離,請呼叫 releaseInterface()close() 來關閉 UsbInterfaceUsbDeviceConnection。如要監聽卸離的事件,請建立如下的廣播接收器:

Kotlin

var usbReceiver: BroadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {

        if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) {
            val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
            device?.apply {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
}

Java

BroadcastReceiver usbReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

      if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
            UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            if (device != null) {
                // call your method that cleans up and closes communication with the device
            }
        }
    }
};

在應用程式中建立廣播接收器 (而非資訊清單),可讓應用程式只在執行期間處理卸離的事件。這樣一來,卸離的事件只會傳送至目前正在執行的應用程式,而不會向所有應用程式播送。