USB 配件概览

USB 配件模式允许用户连接专为 Android 设备设计的 USB 主机硬件。这些配件必须遵守 Android Accessory Development Kit 文档中概述的 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 中引入平台的,但通过 Google API 附加库,它们也可用于 Android 2.3.4。由于这些 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 配件模式。如果您只关注具有 USB 配件模式硬件支持的 Android 3.1 或更高版本设备,并且可以在清单文件中声明此功能,请使用此包。

安装 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 API 附加库和平台 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);

当您使用 intent 过滤器筛选连接的配件时,UsbAccessory 对象包含在传递给您的应用的 intent 中。如果您使用附加库,必须按以下方式获取 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 配件连接时收到通知,请在主 activity 中为 android.hardware.usb.action.USB_ACCESSORY_ATTACHED intent 指定一对 <intent-filter><meta-data> 元素。<meta-data> 元素指向一个外部 XML 资源文件,该文件声明了您要检测的配件的识别信息。

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

    • manufacturer
    • model
    • version

    不建议根据 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. 通过使用筛选配件连接事件的 intent 过滤器来发现连接的配件,或通过枚举已连接的配件并找到合适的配件。
  2. 如果尚未获得权限,向用户请求与配件通信的权限。
  3. 通过在适当的接口端点上读取和写入数据与配件通信。

发现配件

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

使用 intent 过滤器

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

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

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

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

注意:如果您的应用使用 intent 过滤器在配件连接时发现它们,则如果用户允许您的应用处理该 intent,您的应用会自动获得权限。否则,您必须在连接配件之前在应用中显式请求权限。

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

要显式获取权限,首先创建一个广播接收器。此接收器会监听您调用 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 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);
                }
            }
        }
    }
};

要注册广播接收器,请将其放在 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), 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 附加数据的 intent,这是一个表示答案的布尔值。在连接到配件之前,请检查此附加数据的值是否为 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
            }
        }
    }
};

在应用内部而非清单中创建广播接收器,可让您的应用仅在运行时处理分离事件。这样,分离事件只会发送到当前正在运行的应用,而不会广播给所有应用。