USB 附件概述

USB 附件模式允许用户连接专为 Android 设备设计的 USB 主机硬件。附件必须符合 Android 附件开发套件 文档中概述的 Android 附件协议。这使得无法充当 USB 主机的 Android 设备仍然可以与 USB 硬件交互。当 Android 设备处于 USB 附件模式时,连接的 Android USB 附件充当主机,为 USB 总线供电并枚举连接的设备。Android 3.1(API 级别 12)支持 USB 附件模式,并且该功能也向后移植到 Android 2.3.4(API 级别 10)以支持更广泛的设备。

选择合适的 USB 附件 API

虽然 USB 附件 API 是在 Android 3.1 中引入到平台中的,但它们也可以在 Android 2.3.4 中使用 Google API 附加库。由于这些 API 是使用外部库向后移植的,因此您可以导入两个包来支持 USB 附件模式。根据您想要支持的 Android 设备,您可能必须使用其中一个而不是另一个

  • com.android.future.usb:为了支持 Android 2.3.4 中的 USB 附件模式,Google API 附加库 包含向后移植的 USB 附件 API,它们包含在此命名空间中。Android 3.1 也支持导入和调用此命名空间内的类以支持使用附加库编写的应用。此附加库是 android.hardware.usb 附件 API 周围的薄包装器,并且不支持 USB 主机模式。如果您想支持支持 USB 附件模式的最广泛的设备范围,请使用附加库并导入此包。需要注意的是,并非所有 Android 2.3.4 设备都需要支持 USB 附件功能。每个单独的设备制造商决定是否支持此功能,因此您必须在清单文件中声明它。
  • android.hardware.usb:此命名空间包含支持 Android 3.1 中 USB 附件模式的类。此软件包包含在框架 API 中,因此 Android 3.1 无需使用附加库即可支持 USB 附件模式。如果您只关心 Android 3.1 或更新的设备(具有 USB 附件模式的硬件支持),则可以使用此软件包,您可以在清单文件中声明此支持。

安装 Google APIs 附加库

如果您想安装附加库,可以通过使用 SDK Manager 安装 Google APIs Android API 10 软件包来安装。有关安装附加库的更多信息,请参阅安装 Google APIs 附加库

API 概述

由于附加库是框架 API 的包装器,因此支持 USB 附件功能的类相似。即使您使用的是附加库,也可以使用android.hardware.usb的参考文档。

注意:但是,附加库和框架 API 之间存在一个小的用法差异,您应该注意这一点。

下表描述了支持 USB 附件 API 的类

描述
UsbManager 允许您枚举和与连接的 USB 附件通信。
UsbAccessory 表示 USB 附件,并包含用于访问其识别信息的方法。

附加库和平台 API 之间的用法差异

使用 Google APIs 附加库和平台 API 之间有两个用法差异。

如果您使用的是附加库,则必须以以下方式获取UsbManager对象

Kotlin

val manager = UsbManager.getInstance(this)

Java

UsbManager manager = UsbManager.getInstance(this);

如果您未使用附加库,则必须以以下方式获取UsbManager对象

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);

当您使用意图过滤器过滤连接的附件时,UsbAccessory对象包含在传递给应用程序的意图中。如果您使用的是附加库,则必须以以下方式获取UsbAccessory对象

Kotlin

val accessory = UsbManager.getAccessory(intent)

Java

UsbAccessory accessory = UsbManager.getAccessory(intent);

如果您未使用附加库,则必须以以下方式获取UsbAccessory对象

Kotlin

val accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY) as UsbAccessory

Java

UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);

Android 清单要求

以下列表描述了在使用 USB 附件 API 之前需要添加到应用程序清单文件中的内容。清单和资源文件示例显示了如何声明这些项目

  • 由于并非所有 Android 设备都保证支持 USB 附件 API,因此请包含一个<uses-feature>元素,该元素声明您的应用程序使用android.hardware.usb.accessory功能。
  • 如果您使用的是附加库,请添加<uses-library>元素,为库指定com.android.future.usb.accessory
  • 如果使用附加库,则将应用程序的最低 SDK 设置为 API 级别 10;如果使用android.hardware.usb软件包,则将其设置为 12。
  • 如果希望您的应用程序收到连接的 USB 附件的通知,请在主活动中为android.hardware.usb.action.USB_ACCESSORY_ATTACHED意图指定一个<intent-filter><meta-data>元素对。<meta-data>元素指向一个外部 XML 资源文件,该文件声明了要检测的附件的识别信息。

    在 XML 资源文件中,为要过滤的附件声明<usb-accessory>元素。每个<usb-accessory>可以具有以下属性

    • 制造商
    • 型号
    • 版本

    不建议根据version进行过滤。附件或设备可能并不总是指定版本字符串(有意或无意)。当您的应用声明一个版本属性进行过滤,而附件或设备未指定版本字符串时,这会导致 Android 早期版本出现NullPointerException。此问题已在 Android 12 中修复。

    将资源文件保存在res/xml/目录中。资源文件名(不带 .xml 扩展名)必须与您在<meta-data>元素中指定的名称相同。XML 资源文件的格式也显示在下面的示例中。

清单和资源文件示例

以下示例显示了一个示例清单及其对应的资源文件

<manifest ...>
    <uses-feature android:name="android.hardware.usb.accessory" />
    
    <uses-sdk android:minSdkVersion="<version>" />
    ...
    <application>
      <uses-library android:name="com.android.future.usb.accessory" />
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
            </intent-filter>

            <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                android:resource="@xml/accessory_filter" />
        </activity>
    </application>
</manifest>

在这种情况下,以下资源文件应保存在res/xml/accessory_filter.xml中,并指定应过滤任何具有相应型号、制造商和版本的附件。附件将这些属性发送到 Android 设备

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

<resources>
    <usb-accessory model="DemoKit" manufacturer="Google" version="1.0"/>
</resources>

使用附件

当用户将 USB 附件连接到 Android 设备时,Android 系统可以确定您的应用程序是否对连接的附件感兴趣。如果是,则可以根据需要设置与附件的通信。为此,您的应用程序必须

  1. 使用意图过滤器发现连接的附件以过滤附件连接事件,或者通过枚举连接的附件并找到合适的附件。
  2. 如果尚未获得,请向用户请求与附件通信的权限。
  3. 通过在相应的接口端点上读取和写入数据来与附件通信。

发现附件

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

使用意图过滤器

要使您的应用程序发现特定的 USB 附件,您可以指定一个意图过滤器来过滤android.hardware.usb.action.USB_ACCESSORY_ATTACHED意图。除了此意图过滤器之外,您还需要指定一个资源文件,该文件指定 USB 附件的属性,例如制造商、型号和版本。

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

<activity ...>
    ...
    <intent-filter>
        <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
    </intent-filter>

    <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
        android:resource="@xml/accessory_filter" />
</activity>

以下示例显示了如何声明相应的资源文件,该文件指定您感兴趣的 USB 附件

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

<resources>
    <usb-accessory manufacturer="Google, Inc." model="DemoKit" version="1.0" />
</resources>

在您的活动中,您可以从意图中获取表示连接附件的UsbAccessory(使用附加库)

Kotlin

val accessory = UsbManager.getAccessory(intent)

Java

UsbAccessory accessory = UsbManager.getAccessory(intent);

或这样(使用平台 API)

Kotlin

val accessory = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY) as UsbAccessory

Java

UsbAccessory accessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);

枚举附件

您可以让您的应用程序在应用程序运行期间枚举已识别的附件。

使用getAccessoryList()方法获取所有连接的 USB 附件的数组

Kotlin

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
val accessoryList: Array<out UsbAccessory> = manager.accessoryList

Java

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbAccessory[] accessoryList = manager.getAccessoryList();

注意:一次仅支持一个连接的附件。

获取与附件通信的权限

在与 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 accessory: UsbAccessory? = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY)

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

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) {
                UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);

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

要注册广播接收器,请将其放在活动中的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 accessory: UsbAccessory
...
usbManager.requestPermission(accessory, permissionIntent)

Java

UsbAccessory accessory;
...
usbManager.requestPermission(accessory, permissionIntent);

当用户回复对话框时,您的广播接收器会收到包含EXTRA_PERMISSION_GRANTED额外内容的意图,该额外内容是表示答案的布尔值。在连接到附件之前,请检查此额外内容的值是否为 true。

与附件通信

您可以使用UsbManager与附件通信,以获取文件描述符,您可以设置输入和输出流以读取和写入数据到描述符。这些流表示附件的输入和输出批量端点。您应该在另一个线程中设置设备和附件之间的通信,这样就不会锁定主 UI 线程。以下示例显示了如何打开附件进行通信

Kotlin

private lateinit var accessory: UsbAccessory
private var fileDescriptor: ParcelFileDescriptor? = null
private var inputStream: FileInputStream? = null
private var outputStream: FileOutputStream? = null
...

private fun openAccessory() {
    Log.d(TAG, "openAccessory: $mAccessory")
    fileDescriptor = usbManager.openAccessory(accessory)
    fileDescriptor?.fileDescriptor?.also { fd ->
        inputStream = FileInputStream(fd)
        outputStream = FileOutputStream(fd)
        val thread = Thread(null, this, "AccessoryThread")
        thread.start()
    }
}

Java

UsbAccessory accessory;
ParcelFileDescriptor fileDescriptor;
FileInputStream inputStream;
FileOutputStream outputStream;
...

private void openAccessory() {
    Log.d(TAG, "openAccessory: " + accessory);
    fileDescriptor = usbManager.openAccessory(accessory);
    if (fileDescriptor != null) {
        FileDescriptor fd = fileDescriptor.getFileDescriptor();
        inputStream = new FileInputStream(fd);
        outputStream = new FileOutputStream(fd);
        Thread thread = new Thread(null, this, "AccessoryThread");
        thread.start();
    }
}

在线程的run()方法中,您可以使用FileInputStreamFileOutputStream对象读取和写入附件。当使用FileInputStream对象从附件读取数据时,请确保您使用的缓冲区足够大以存储 USB 数据包数据。Android 附件协议支持最大 16384 字节的数据包缓冲区,因此为了简单起见,您可以选择始终将缓冲区声明为此大小。

注意:在较低级别,数据包对于 USB 全速附件为 64 字节,对于 USB 高速附件为 512 字节。为了简单起见,Android 附件协议将两种速度的数据包捆绑在一起,形成一个逻辑数据包。

有关在 Android 中使用线程的更多信息,请参阅进程和线程

终止与附件的通信

完成与附件的通信或附件已分离时,请通过调用close()关闭打开的文件描述符。要侦听分离事件,请创建如下所示的广播接收器

Kotlin

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

        if (UsbManager.ACTION_USB_ACCESSORY_DETACHED == intent.action) {
            val accessory: UsbAccessory? = intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY)
            accessory?.apply {
                // call your method that cleans up and closes communication with the accessory
            }
        }
    }
}

Java

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

        if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
            UsbAccessory accessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
            if (accessory != null) {
                // call your method that cleans up and closes communication with the accessory
            }
        }
    }
};

在应用程序内创建广播接收器,而不是在清单中创建,这使得您的应用程序仅在运行时处理分离事件。这样,分离事件仅发送到当前正在运行的应用程序,而不是广播到所有应用程序。