Omówienie hosta USB

Gdy urządzenie z Androidem jest w trybie hosta USB, działa jako host USB, zasila magistrali i wyświetla podłączone urządzenia USB. Tryb hosta USB jest obsługiwany w Androidzie 3.1 i nowszych wersjach.

Omówienie interfejsu API

Zanim zaczniesz, musisz wiedzieć, z którymi zajęciami chcesz pracować. W tabeli poniżej opisujemy interfejsy API hosta USB w pakiecie android.hardware.usb.

Tabela 1. Interfejsy API hostów USB

Kategoria Opis
UsbManager Umożliwia wyliczenie podłączonych urządzeń USB i komunikowanie się z nimi.
UsbDevice Reprezentuje połączone urządzenie USB i zawiera metody dostępu do jego informacji identyfikujących, interfejsów i punktów końcowych.
UsbInterface Reprezentuje interfejs urządzenia USB, który określa zestaw funkcji urządzenia. Urządzenie może mieć jeden lub więcej interfejsów do komunikacji.
UsbEndpoint Reprezentuje punkt końcowy interfejsu, który jest kanałem komunikacji tego interfejsu. Interfejs może mieć 1 lub więcej punktów końcowych i zwykle ma punkty końcowe wejściowe i wyjściowe do dwukierunkowej komunikacji z urządzeniem.
UsbDeviceConnection Reprezentuje połączenie z urządzeniem, które przesyła dane w punktach końcowych. Ta klasa umożliwia wysyłanie danych w obie strony synchronicznie lub asynchronicznie.
UsbRequest Reprezentuje asynchroniczne żądanie komunikacji z urządzeniem przez interfejs UsbDeviceConnection.
UsbConstants Definiuje stałe USB, które odpowiadają definicjom w Linuxie/usb/ch9.h jądra systemu Linux.

W większości sytuacji trzeba używać wszystkich tych klas (UsbRequest jest wymagany tylko w przypadku komunikacji asynchronicznej) podczas komunikacji z urządzeniem USB. Ogólnie rzecz biorąc, otrzymujesz UsbManager, aby pobrać żądane UsbDevice. Gdy masz urządzenie, musisz znaleźć odpowiednie UsbInterface i UsbEndpoint tego interfejsu, aby się z nim komunikować. Gdy uzyskasz właściwy punkt końcowy, otwórz UsbDeviceConnection, aby połączyć się z urządzeniem USB.

Wymagania dotyczące pliku manifestu na Androidzie

Na tej liście opisujemy, co należy dodać do pliku manifestu aplikacji przed rozpoczęciem korzystania z interfejsów API hosta USB:

  • Nie wszystkie urządzenia z Androidem będą obsługiwać interfejsy API hosta USB, dlatego dodaj element <uses-feature>, który deklaruje, że Twoja aplikacja używa funkcji android.hardware.usb.host.
  • Ustaw minimalny pakiet SDK aplikacji na interfejs API na poziomie 12 lub wyższym. Interfejsy API hosta USB nie występują na poprzednich poziomach API.
  • Jeśli chcesz, aby aplikacja była powiadamiana o podłączonym urządzeniu USB, w głównej aktywności określ parę elementów <intent-filter> i <meta-data> dla intencji android.hardware.usb.action.USB_DEVICE_ATTACHED. Element <meta-data> wskazuje zewnętrzny plik zasobów XML z deklaracją informacji identyfikujących urządzenie, które chcesz wykryć.

    W pliku zasobów XML zadeklaruj elementy <usb-device> dla urządzeń USB, które chcesz filtrować. Atrybuty <usb-device> znajdziesz na liście poniżej. Ogólnie, gdy chcesz filtrować dane według określonego urządzenia, użyj dostawcy i identyfikatora produktu, a następnie użyj klasy, podklasy i protokołu, jeśli chcesz odfiltrować grupę urządzeń USB, takich jak urządzenia pamięci masowej lub aparaty cyfrowe. Możesz określić żaden z nich lub wszystkie z nich. Określanie żadnych atrybutów nie pasuje do każdego urządzenia USB, więc rób to tylko wtedy, gdy aplikacja tego wymaga:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (urządzenie lub interfejs)

    Zapisz plik zasobów w katalogu res/xml/. Nazwa pliku zasobów (bez rozszerzenia .xml) musi być taka sama jak nazwa określona w elemencie <meta-data>. Format pliku zasobów XML znajdziesz w przykładzie poniżej.

Przykłady plików manifestu i plików zasobów

Ten przykład przedstawia przykładowy plik manifestu i odpowiadający mu plik zasobów:

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

W tym przypadku ten plik zasobów należy zapisać w res/xml/device_filter.xml i określić, że powinno zostać odfiltrowane wszystkie urządzenia USB o określonych atrybutach:

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

<resources>
    <usb-device vendor-id="1234" product-id="5678" class="255" subclass="66" protocol="1" />
</resources>

Praca z urządzeniami

Gdy użytkownicy podłączają urządzenia USB do urządzenia z Androidem, system ten może określić, czy aplikacja jest zainteresowana podłączonym urządzeniem. Jeśli tak, w razie potrzeby możesz skonfigurować komunikację z urządzeniem. Aby to umożliwić, aplikacja musi:

  1. Wykrywaj podłączone urządzenia USB, korzystając z filtra intencji, aby otrzymywać powiadomienia, gdy użytkownik połączy się z urządzeniem USB, lub przez listę urządzeń USB, które są już podłączone.
  2. Poproś użytkownika o pozwolenie na połączenie się z urządzeniem USB, jeśli jeszcze nie ma tego uprawnienia.
  3. Komunikacja z urządzeniem USB przez odczytywanie i zapisywanie danych w odpowiednich punktach końcowych interfejsu.

Odkryj urządzenie

Aplikacja może wykrywać urządzenia USB, korzystając z filtra intencji, który jest wysyłany, gdy użytkownik połączy się z urządzeniem, lub wyświetlając listę urządzeń USB, które są już podłączone. Filtr intencji jest przydatny, jeśli chcesz, aby aplikacja automatycznie wykrywała odpowiednie urządzenie. Wyliczenie podłączonych urządzeń USB jest przydatne, jeśli chcesz uzyskać listę wszystkich podłączonych urządzeń lub jeśli Twoja aplikacja nie filtrowała intencji.

Używanie filtra intencji

Aby aplikacja wykrywała określone urządzenie USB, możesz określić filtr intencji, który będzie filtrował intencję android.hardware.usb.action.USB_DEVICE_ATTACHED. Oprócz tego filtra intencji musisz też podać plik zasobów, który określa właściwości urządzenia USB, takie jak identyfikator produktu i dostawcy. Gdy użytkownicy podłączą urządzenie zgodne z filtrem urządzenia, system wyświetli im okno z pytaniem, czy chcą uruchomić aplikację. Jeśli użytkownicy wyrażą na to zgodę, aplikacja automatycznie uzyska dostęp do urządzenia, dopóki urządzenie nie zostanie odłączone.

Z przykładu poniżej dowiesz się, jak zadeklarować filtr intencji:

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

Z przykładu poniżej dowiesz się, jak zadeklarować plik zasobów określający interesujące Cię urządzenia USB:

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

<resources>
    <usb-device vendor-id="1234" product-id="5678" />
</resources>

W swojej aktywności możesz uzyskać identyfikator UsbDevice, który reprezentuje podłączone urządzenie, za pomocą intencji w ten sposób:

Kotlin

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

Java

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

Wyliczenie urządzeń

Jeśli aplikacja chce sprawdzić wszystkie urządzenia USB podłączone obecnie do jej działania, może wyliczyć urządzenia w magistrali. Aby uzyskać mapę skrótów wszystkich podłączonych urządzeń USB, użyj metody getDeviceList(). Jeśli chcesz pobrać urządzenie z mapy, kluczowa jest nazwa urządzenia 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");

W razie potrzeby możesz też uzyskać iterator z mapy haszowania i przetwarzać każde urządzenie po kolei:

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
}

Uzyskiwanie uprawnień do komunikacji z urządzeniem

Przed rozpoczęciem komunikacji z urządzeniem USB aplikacja musi mieć pozwolenie od użytkowników.

Uwaga: jeśli aplikacja używa filtra intencji do wykrywania podłączonych urządzeń USB, automatycznie otrzymuje uprawnienia, gdy użytkownik zezwoli jej na obsługę intencji. Jeśli nie, przed połączeniem się z urządzeniem musisz jawnie poprosić o nie w aplikacji.

W niektórych sytuacjach pytanie o uprawnienia może być konieczne, np. gdy aplikacja wymienia urządzenia USB, które są już podłączone, a potem chce się z nim połączyć. Musisz sprawdzić uprawnienia dostępu do urządzenia, zanim spróbujesz się z nim połączyć. W przeciwnym razie w przypadku odmowy dostępu do urządzenia pojawi się błąd czasu działania.

Aby uzyskać wyraźne pozwolenie, najpierw utwórz odbiornik. Ten odbiornik nasłuchuje intencji, która jest wysyłana, gdy dzwonisz pod numer requestPermission(). W wywołaniu metody requestPermission() pojawi się okno z prośbą o zgodę na połączenie z urządzeniem. Poniższy przykładowy kod pokazuje, jak utworzyć odbiornik:

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

Aby zarejestrować odbiornik, dodaj w swojej aktywności w metodzie onCreate() ten fragment kodu:

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

Aby wyświetlić okno z prośbą o pozwolenie na połączenie się z urządzeniem, wywołaj metodę requestPermission():

Kotlin

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

Java

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

Gdy użytkownik odpowie w oknie, odbiornik otrzyma intencję zawierającą dodatkowy element (EXTRA_PERMISSION_GRANTED), który jest wartością logiczną reprezentującą odpowiedź. Sprawdź, czy przed połączeniem się z urządzeniem ma wartość true (prawda).

Komunikacja z urządzeniem

Komunikacja z urządzeniem USB może być synchroniczna lub asynchroniczna. W obu przypadkach utwórz nowy wątek, w którym będą wykonywane wszystkie transmisje danych, aby nie zablokować wątku UI. Aby prawidłowo skonfigurować komunikację z urządzeniem, musisz uzyskać odpowiednie UsbInterface i UsbEndpoint urządzenia, z którym chcesz się komunikować, i wysyłać żądania za pomocą interfejsu UsbDeviceConnection w tym punkcie końcowym. Ogólnie kod powinien:

  • Sprawdź atrybuty obiektu UsbDevice, takie jak identyfikator produktu, identyfikator dostawcy lub klasa urządzenia, aby określić, czy chcesz komunikować się z urządzeniem.
  • Gdy masz pewność, że chcesz komunikować się z urządzeniem, znajdź odpowiedni obiekt UsbInterface, którego chcesz używać do komunikacji z odpowiednim elementem UsbEndpoint tego interfejsu. Interfejsy mogą mieć jeden lub więcej punktów końcowych oraz zwykle mają punkt końcowy wejściowy i wyjściowy na potrzeby komunikacji dwukierunkowej.
  • Gdy znajdziesz właściwy punkt końcowy, otwórz w nim UsbDeviceConnection.
  • Dostarcz dane, które chcesz przesyłać w punkcie końcowym, korzystając z metody bulkTransfer() lub controlTransfer(). Aby uniknąć zablokowania głównego wątku interfejsu, wykonaj ten krok w innym wątku. Więcej informacji o używaniu wątków na Androidzie znajdziesz w artykule Procesy i wątki.

Poniższy fragment kodu to prosty sposób na synchroniczne przesyłanie danych. Twój kod powinien mieć więcej logiki, aby poprawnie znaleźć właściwy interfejs i punkty końcowe do komunikacji, a także przesyłać dane do innego wątku niż główny wątek 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

Aby wysyłać dane asynchronicznie, użyj klasy UsbRequest do initialize i queue żądania asynchronicznego, a następnie poczekaj na wynik, używając funkcji requestWait().

Kończenie komunikacji z urządzeniem

Gdy zakończysz komunikację z urządzeniem lub zostało ono odłączone, zamknij UsbInterface i UsbDeviceConnection, wywołując releaseInterface() i close(). Aby nasłuchiwać odłączonych zdarzeń, utwórz odbiornik, jak poniżej:

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

Utworzenie odbiornika w aplikacji, a nie w pliku manifestu, umożliwia aplikacji obsługę odłączonych zdarzeń tylko podczas działania. Dzięki temu zdarzenia odłączone będą wysyłane tylko do działającej aplikacji, a nie do wszystkich aplikacji.