Обзор USB-хоста

Когда устройство Android находится в режиме хоста USB, оно действует как хост USB, питает шину и перечисляет подключенные USB-устройства. Режим USB-хоста поддерживается в Android 3.1 и более поздних версиях.

Общие сведения об API

Прежде чем начать, важно понять, с какими классами вам нужно работать. В таблице ниже описаны API USB-хоста в пакете android.hardware.usb.

Таблица 1. USB Host API

Класс Описание
UsbManager Позволяет перечислять подключенные USB-устройства и обмениваться с ними данными.
UsbDevice Представляет подключенное USB-устройство и содержит методы для доступа к его идентифицирующей информации, интерфейсам и конечным точкам.
UsbInterface Представляет интерфейс USB-устройства, который определяет набор функций для устройства. У устройства может быть один или несколько интерфейсов для связи.
UsbEndpoint Представляет конечную точку интерфейса, которая является каналом связи для этого интерфейса. У интерфейса может быть одна или несколько конечных точек, и обычно у него есть конечные точки ввода и вывода для двусторонней связи с устройством.
UsbDeviceConnection Представляет подключение к устройству, которое передает данные на конечные точки. Этот класс позволяет отправлять и получать данные синхронно или асинхронно.
UsbRequest Представляет асинхронный запрос на обмен данными с устройством через UsbDeviceConnection.
UsbConstants Определяет константы USB, которые соответствуют определениям в linux/usb/ch9.h ядра Linux.

В большинстве случаев при взаимодействии с USB-устройством необходимо использовать все эти классы (UsbRequest требуется только для асинхронной связи). Как правило, чтобы получить нужный объект UsbDevice, необходимо получить объект UsbManager. После этого нужно найти подходящий интерфейс UsbInterface и его конечную точку UsbEndpoint для обмена данными. Получив нужную конечную точку, откройте UsbDeviceConnection для связи с USB-устройством.

Требования к манифесту Android

В следующем списке описано, что нужно добавить в файл манифеста приложения, прежде чем работать с API USB-хоста:

  • Поскольку не все устройства Android поддерживают API USB-хоста, добавьте элемент <uses-feature>, который указывает, что ваше приложение использует функцию android.hardware.usb.host.
  • Установите минимальный уровень API для SDK приложения на 12 или выше. API USB-хоста отсутствуют на более ранних уровнях API.
  • Если вы хотите, чтобы ваше приложение получало уведомления о подключении USB-устройства, укажите пару элементов <intent-filter> и <meta-data> для намерения android.hardware.usb.action.USB_DEVICE_ATTACHED в основном действии. Элемент <meta-data> указывает на внешний XML-файл ресурсов, в котором содержится идентифицирующая информация об устройстве, которое вы хотите обнаружить.

    В XML-файле ресурсов объявите элементы <usb-device> для USB-устройств, которые вы хотите отфильтровать. Ниже перечислены атрибуты <usb-device>. Как правило, идентификаторы поставщика и товара используются для фильтрации по конкретному устройству, а класс, подкласс и протокол – для фильтрации по группе USB-устройств, например по накопителям или цифровым камерам. Вы можете указать все эти атрибуты или ни одного. Если не указать атрибуты, будут выбраны все 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-устройства, например идентификатор продукта и поставщика. Когда пользователь подключает устройство, соответствующее вашему фильтру, система показывает диалоговое окно с вопросом, хочет ли он запустить ваше приложение. Если пользователь согласится, ваше приложение автоматически получит разрешение на доступ к устройству, пока оно не будет отключено.

В следующем примере показано, как объявить фильтр интентов:

<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 вы можете получить объект UsbDevice, представляющий подключенное устройство, из объекта intent следующим образом:

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() своего класса activity следующий код:

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. Чтобы правильно настроить связь с устройством, необходимо получить соответствующие UsbInterface и UsbEndpoint устройства, с которым вы хотите связаться, и отправить запросы на эту конечную точку с помощью UsbDeviceConnection. В целом ваш код должен:

  • Проверьте атрибуты объекта UsbDevice, например идентификатор товара, идентификатор поставщика или класс устройства, чтобы определить, нужно ли вам взаимодействовать с устройством.
  • Когда вы будете уверены, что хотите установить связь с устройством, найдите нужный интерфейс 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 для initialize и queue асинхронного запроса, а затем дождитесь результата с помощью requestWait().

Завершение связи с устройством

Когда вы закончите обмен данными с устройством или если оно было отключено, закройте UsbInterface и UsbDeviceConnection, вызвав releaseInterface() и close(). Чтобы прослушивать события отсоединения, создайте широковещательный приемник, как показано ниже.

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

Создание широковещательного приемника в приложении, а не в манифесте, позволяет приложению обрабатывать события отсоединения только во время работы. Таким образом, события отсоединения отправляются только в приложение, которое запущено в данный момент, а не во все приложения.