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

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

Обзор API

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

Таблица 1. API-интерфейсы USB-хоста

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

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

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

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

  • Поскольку не все устройства под управлением Android гарантированно поддерживают API-интерфейсы USB-хоста, включите элемент <uses-feature> , который объявляет, что ваше приложение использует функцию android.hardware.usb.host .
  • Установите минимальный SDK приложения на уровень API 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>

В своей деятельности вы можете получить UsbDevice , представляющий подключенное устройство, следующим образом:

Котлин

val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

Ява

UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

Перечислить устройства

Если ваше приложение заинтересовано в проверке всех USB-устройств, подключенных в данный момент во время работы вашего приложения, оно может перечислить устройства на шине. Используйте метод getDeviceList() , чтобы получить хеш-карту всех подключенных USB-устройств. Хэш-карта включается по имени USB-устройства, если вы хотите получить устройство по карте.

Котлин

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
...
val deviceList = manager.getDeviceList()
val device = deviceList.get("deviceName")

Ява

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
...
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
UsbDevice device = deviceList.get("deviceName");

При желании вы также можете просто получить итератор из хэш-карты и обработать каждое устройство по отдельности:

Котлин

val manager = getSystemService(Context.USB_SERVICE) as UsbManager
..
val deviceList: HashMap<String, UsbDevice> = manager.deviceList
deviceList.values.forEach { device ->
    // your code
}

Ява

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() отображает пользователю диалоговое окно с запросом разрешения на подключение к устройству. В следующем примере кода показано, как создать широковещательный приемник:

Котлин

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")
                }
            }
        }
    }
}

Ява

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() в своей деятельности:

Котлин

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)

Ява

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() :

Котлин

lateinit var device: UsbDevice
...
usbManager.requestPermission(device, permissionIntent)

Ява

UsbDevice device;
...
usbManager.requestPermission(device, permissionIntent);

Когда пользователи отвечают на диалог, ваш широковещательный приемник получает намерение, содержащее дополнительное значение EXTRA_PERMISSION_GRANTED , которое представляет собой логическое значение, представляющее ответ. Проверьте это дополнение на наличие значения true перед подключением к устройству.

Общайтесь с устройством

Связь с USB-устройством может быть синхронной или асинхронной. В любом случае вам следует создать новый поток, в котором будут осуществляться все передачи данных, чтобы не блокировать поток пользовательского интерфейса. Чтобы правильно настроить связь с устройством, вам необходимо получить соответствующие UsbInterface и UsbEndpoint устройства, с которым вы хотите обмениваться данными, и отправлять запросы на эту конечную точку с помощью UsbDeviceConnection . В общем, ваш код должен:

  • Проверьте атрибуты объекта UsbDevice , такие как идентификатор продукта, идентификатор поставщика или класс устройства, чтобы выяснить, хотите ли вы взаимодействовать с устройством.
  • Если вы уверены, что хотите установить связь с устройством, найдите соответствующий UsbInterface , который вы хотите использовать для связи, вместе с соответствующей UsbEndpoint этого интерфейса. Интерфейсы могут иметь одну или несколько конечных точек и обычно имеют конечные точки ввода и вывода для двусторонней связи.
  • Когда вы найдете правильную конечную точку, откройте UsbDeviceConnection в этой конечной точке.
  • Предоставьте данные, которые вы хотите передать в конечную точку, с помощью метода bulkTransfer() или controlTransfer() . Вам следует выполнить этот шаг в другом потоке, чтобы предотвратить блокировку основного потока пользовательского интерфейса. Дополнительные сведения об использовании потоков в Android см. в разделе Процессы и потоки .

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

Котлин

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

Ява

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() . Чтобы прослушивать отдельные события, создайте широковещательный приемник, как показано ниже:

Котлин

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

Ява

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

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