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设备的通知,请在您的主活动中为android.hardware.usb.action.USB_DEVICE_ATTACHED意图指定一个<intent-filter><meta-data>元素对。<meta-data>元素指向一个外部XML资源文件,该文件声明了要检测的设备的识别信息。

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

    • 厂商ID (vendor-id)
    • 产品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和厂商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), 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额外内容的意图,这是一个表示答案的布尔值。在连接到设备之前,请检查此额外内容的值是否为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类来初始化排队异步请求,然后使用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
            }
        }
    }
};

在应用程序(而不是清单)中创建广播接收器,允许您的应用程序仅在运行时处理断开事件。这样,断开事件只发送到当前正在运行的应用程序,而不是广播到所有应用程序。