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。获取设备后,需要找到该设备上用于通信的适当 UsbInterface 和该接口的 UsbEndpoint。一旦获得正确的端点,打开 UsbDeviceConnection 即可与 USB 设备通信。

Android 清单要求

以下列表描述了在使用 USB 主机 API 之前需要在应用清单文件中添加的内容

  • 并非所有 Android 设备都保证支持 USB 主机 API,因此请添加一个 <uses-feature> 元素,声明您的应用使用了 android.hardware.usb.host 功能。
  • 将应用的最低 SDK 设置为 API 级别 12 或更高。早期的 API 级别不支持 USB 主机 API。
  • 如果您希望应用在连接 USB 设备时收到通知,请在主 activity 中为 android.hardware.usb.action.USB_DEVICE_ATTACHED intent 指定一对 <intent-filter><meta-data> 元素。 <meta-data> 元素指向一个外部 XML 资源文件,该文件声明了您想要检测的设备的识别信息。

    在 XML 资源文件中,为要过滤的 USB 设备声明 <usb-device> 元素。以下列表描述了 <usb-device> 的属性。通常,如果要过滤特定设备,请使用供应商 ID 和产品 ID;如果要过滤 USB 设备组(例如大容量存储设备或数码相机),请使用类、子类和协议。您可以指定这些属性中的任意一个或全部。不指定任何属性将匹配所有 USB 设备,因此仅在您的应用需要时才这样做

    • vendor-id (供应商 ID)
    • product-id (产品 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. 通过使用 intent 过滤器在用户连接 USB 设备时收到通知,或通过枚举已连接的 USB 设备来发现已连接的 USB 设备。
  2. 如果尚未获得许可,请向用户请求连接到 USB 设备的权限。
  3. 通过在适当的接口端点上读写数据与 USB 设备通信。

发现设备

您的应用可以通过使用 intent 过滤器在用户连接设备时收到通知,或通过枚举已连接的 USB 设备来发现 USB 设备。如果您希望应用能够自动检测所需的设备,使用 intent 过滤器会很有用。如果您想获取所有已连接设备的列表,或者您的应用未过滤 intent,枚举已连接的 USB 设备会很有用。

使用 intent 过滤器

为了让您的应用发现特定的 USB 设备,您可以指定一个 intent 过滤器来过滤 android.hardware.usb.action.USB_DEVICE_ATTACHED intent。除了此 intent 过滤器之外,您还需要指定一个资源文件,该文件指定了 USB 设备的属性,例如产品 ID 和供应商 ID。当用户连接与您的设备过滤器匹配的设备时,系统会向他们显示一个对话框,询问他们是否要启动您的应用。如果用户接受,您的应用将自动获得访问设备的权限,直到设备断开连接。

以下示例显示了如何声明 intent 过滤器

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

在您的 activity 中,您可以像这样从 intent 中获取表示已连接设备的 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 设备通信之前,您的应用必须获得用户的许可。

注意:如果您的应用使用 intent 过滤器来发现已连接的 USB 设备,并且用户允许您的应用处理该 intent,则您的应用会自动获得权限。否则,您必须在连接到设备之前在应用中明确请求权限。

在某些情况下,例如当您的应用枚举已连接的 USB 设备并希望与其中一个设备通信时,可能需要明确请求权限。在尝试与设备通信之前,您必须检查是否有权访问该设备。否则,如果用户拒绝了访问设备的权限,您将收到运行时错误。

要明确获取权限,首先创建一个广播接收器。当您调用 requestPermission() 时,此接收器会监听广播的 intent。调用 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);
                }
            }
        }
    }
};

要注册广播接收器,请将其添加到您的 activity 中的 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), PendingIntent.FLAG_IMMUTABLE)
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), PendingIntent.FLAG_IMMUTABLE);
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 extra 的 intent,这是一个布尔值,表示用户的回答。在连接到设备之前,请检查此 extra 的值是否为 true。

与设备通信

与 USB 设备通信可以是同步的或异步的。无论哪种情况,您都应该创建一个新线程来执行所有数据传输,以免阻塞 UI 线程。要正确设置与设备的通信,您需要获取要用于通信的设备的适当 UsbInterfaceUsbEndpoint,并使用 UsbDeviceConnection 在此端点上发送请求。通常,您的代码应该

  • 检查 UsbDevice 对象的属性(例如产品 ID、供应商 ID 或设备类),以确定您是否要与该设备通信。
  • 确定要与设备通信后,找到要用于通信的适当 UsbInterface 以及该接口的适当 UsbEndpoint。接口可以有一个或多个端点,通常会有用于双向通信的输入和输出端点。
  • 找到正确的端点后,在该端点上打开 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
            }
        }
    }
};

在应用内部而不是在清单中创建广播接收器,可以使您的应用仅在运行时处理设备拔出事件。这样,设备拔出事件只会发送到当前正在运行的应用,而不会广播到所有应用。