Visão geral do host USB

Quando seu dispositivo Android está no modo de host USB, ele funciona como o host USB, alimenta o barramento e enumera os dispositivos USB conectados. O modo de host USB é compatível com o Android 3.1 e posterior.

Visão geral da API

Antes de começar, é importante entender as classes com as quais você precisa trabalhar. A tabela a seguir descreve as APIs de host USB no pacote android.hardware.usb.

Tabela 1. APIs de host USB

Classe. Descrição
UsbManager Permite que você enumere e se comunique com dispositivos USB conectados.
UsbDevice Representa um dispositivo USB conectado e contém métodos para acessar as informações de identificação, as interfaces e os endpoints.
UsbInterface Representa uma interface de um dispositivo USB, que define um conjunto de funcionalidades para o dispositivo. Um dispositivo pode ter uma ou mais interfaces de comunicação.
UsbEndpoint Representa um endpoint da interface, que é um canal de comunicação dessa interface. Uma interface pode ter um ou mais endpoints, e geralmente tem endpoints de entrada e saída para comunicação bidirecional com o dispositivo.
UsbDeviceConnection Representa uma conexão com o dispositivo, que transfere dados em endpoints. Essa classe permite que você envie dados de maneira síncrona ou assíncrona.
UsbRequest Representa uma solicitação assíncrona para se comunicar com um dispositivo por meio de UsbDeviceConnection.
UsbConstants Define as constantes USB que correspondem às definições em linux/usb/ch9.h do kernel do Linux.

Na maioria das situações, você precisa usar todas essas classes ao se comunicar com um dispositivo USB. UsbRequest só será necessário se você estiver fazendo comunicação assíncrona. Em geral, você usa um UsbManager para recuperar o UsbDevice desejado. Quando você estiver com o dispositivo, precisará encontrar o UsbInterface e o UsbEndpoint adequados dessa interface para se comunicar. Depois de ter o endpoint correto, abra um UsbDeviceConnection para se comunicar com o dispositivo USB.

Requisitos de manifesto do Android

A lista a seguir descreve o que você precisa adicionar ao arquivo de manifesto do app antes de trabalhar com as APIs de host USB.

  • Como nem todos os dispositivos Android têm suporte às APIs de host USB, inclua um elemento <uses-feature> que declare que seu app usa o recurso android.hardware.usb.host.
  • Defina o SDK mínimo do app como a API nível 12 ou posterior. As APIs de host USB não estão presentes nos níveis anteriores da API.
  • Se você quiser que seu app seja notificado sobre um dispositivo USB conectado, especifique um par de elementos <intent-filter> e <meta-data> para a intent android.hardware.usb.action.USB_DEVICE_ATTACHED na sua atividade principal. O elemento <meta-data> aponta para um arquivo de recurso XML externo que declara informações de identificação sobre o dispositivo que você quer detectar.

    No arquivo de recurso XML, declare elementos <usb-device> para os dispositivos USB que você quer filtrar. A lista a seguir descreve os atributos de <usb-device>. Em geral, use o ID do fornecedor e do produto se quiser filtrar um dispositivo específico e use classe, subclasse e protocolo se quiser filtrar um grupo de dispositivos USB, como dispositivos de armazenamento em massa ou câmeras digitais. É possível especificar nenhum ou todos esses atributos. Especificar que nenhum atributo corresponde a cada dispositivo USB. Portanto, faça isso apenas se o aplicativo exigir:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (dispositivo ou interface)

    Salve o arquivo de recurso no diretório res/xml/. O nome do arquivo de recurso (sem a extensão .xml) precisa ser o mesmo que você especificou no elemento <meta-data>. O formato do arquivo de recurso XML está no exemplo abaixo.

Exemplos de arquivo de manifesto e recurso

O exemplo a seguir mostra um manifesto e o arquivo de recurso correspondente:

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

Nesse caso, o seguinte arquivo de recurso precisa ser salvo em res/xml/device_filter.xml e especifica que qualquer dispositivo USB com os atributos especificados precisa ser filtrado:

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

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

Trabalhar com dispositivos

Quando os usuários conectam dispositivos USB a um dispositivo Android, o sistema Android pode determinar se seu app tem interesse no dispositivo conectado. Nesse caso, você pode configurar a comunicação com o dispositivo, se quiser. Para fazer isso, seu app precisa:

  1. descobrir dispositivos USB conectados usando um filtro de intent para ser notificado quando o usuário conectar um dispositivo USB ou enumerando dispositivos USB que já estejam conectados.
  2. pedir permissão ao usuário para se conectar ao dispositivo USB, se ainda não tiver recebido;
  3. comunicar-se com o dispositivo USB lendo e gravando dados nos endpoints de interface adequados.

Descobrir um dispositivo

Seu app pode descobrir dispositivos USB usando um filtro de intent para ser notificado quando o usuário conectar um dispositivo ou enumerando dispositivos USB que já estejam conectados. Usar um filtro de intent é útil se você quer que seu aplicativo detecte automaticamente um determinado dispositivo. Enumerar dispositivos USB conectados será útil se você quiser ver uma lista de todos os dispositivos conectados ou se o aplicativo não filtrar um intent.

Usar um filtro de intent

Para que seu app descubra um dispositivo USB específico, você pode especificar um filtro para o intent android.hardware.usb.action.USB_DEVICE_ATTACHED. Junto com esse filtro de intent, você precisa especificar um arquivo de recurso que especifique as propriedades do dispositivo USB, como o ID do produto e do fornecedor. Quando os usuários conectam um dispositivo que corresponde ao filtro correspondente, o sistema apresenta uma caixa de diálogo perguntando se eles querem iniciar o app. Se os usuários aceitarem, seu aplicativo terá permissão automática para acessar o dispositivo até que ele seja desconectado.

O exemplo a seguir mostra como declarar o filtro de intent:

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

O exemplo a seguir mostra como declarar o arquivo de recurso correspondente que especifica os dispositivos USB em que você tem interesse:

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

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

Na sua atividade, você pode acessar o UsbDevice que representa o dispositivo anexado no intent desta forma:

Kotlin

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

Java

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

Enumerar dispositivos

Se seu app estiver interessado em inspecionar todos os dispositivos USB conectados enquanto ele estiver em execução, ele poderá enumerar dispositivos no barramento. Use o método getDeviceList() para ver um mapa hash de todos os dispositivos USB conectados. O mapa de hash é codificado pelo nome do dispositivo USB caso você queira acessar um dispositivo no mapa.

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

Se você quiser, também pode conseguir um iterador do mapa hash e processar cada dispositivo um por um:

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
}

Receber permissão para se comunicar com um dispositivo

Antes de se comunicar com o dispositivo USB, seu app precisa ter a permissão dos usuários.

Observação:se o app usar um filtro de intent para descobrir dispositivos USB à medida que estão conectados, ele vai receber automaticamente a permissão se o usuário permitir que o app processe a intent. Caso contrário, solicite a permissão explicitamente no seu aplicativo antes de se conectar ao dispositivo.

Solicitar explicitamente permissão pode ser necessário em algumas situações, como quando o app enumera dispositivos USB que já estão conectados e depois quer se comunicar com um. Você precisa verificar a permissão para acessar um dispositivo antes de tentar se comunicar com ele. Caso contrário, você receberá um erro de tempo de execução se o usuário tiver negado a permissão para acessar o dispositivo.

Para receber permissão explicitamente, primeiro crie um broadcast receiver. Esse receptor detecta a intent que recebe a transmissão quando você chama requestPermission(). A chamada para requestPermission() exibe uma caixa de diálogo ao usuário pedindo permissão para se conectar ao dispositivo. O exemplo de código a seguir mostra como criar o broadcast receiver:

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

Para registrar o broadcast receiver, adicione-o ao método onCreate() na sua atividade:

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

Para exibir a caixa de diálogo que solicita aos usuários permissão para se conectar ao dispositivo, chame o método requestPermission():

Kotlin

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

Java

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

Quando o usuário responde à caixa de diálogo, o broadcast receiver recebe o intent que contém o EXTRA_PERMISSION_GRANTED extra, que é um booleano que representa a resposta. Verifique se esse extra tem um valor "true" antes de se conectar ao dispositivo.

Comunicar-se com um dispositivo

A comunicação com um dispositivo USB pode ser síncrona ou assíncrona. Em ambos os casos, você precisa criar uma nova linha de execução na qual realizar todas as transmissões de dados, de modo que não bloqueie a linha de execução de IU. Para configurar corretamente a comunicação com um dispositivo, é necessário ter o UsbInterface e o UsbEndpoint adequados do dispositivo com que você quer se comunicar e enviar solicitações nesse endpoint com um UsbDeviceConnection. Em geral, seu código precisa:

  • Verifique os atributos de um objeto UsbDevice, como ID do produto, ID do fornecedor ou classe de dispositivo, para descobrir se você quer ou não se comunicar com o dispositivo.
  • Quando tiver certeza de que você quer se comunicar com o dispositivo, encontre o UsbInterface apropriado que quer usar para se comunicar com o UsbEndpoint apropriado dessa interface. As interfaces podem ter um ou mais endpoints, e geralmente terão um endpoint de entrada e saída para comunicação bidirecional.
  • Quando encontrar o endpoint correto, abra um UsbDeviceConnection nele.
  • fornecer os dados que você quer transmitir no endpoint com o método bulkTransfer() ou controlTransfer(). Você precisa realizar essa etapa em outra linha de execução para evitar o bloqueio da linha de execução de interface principal. Para mais informações sobre o uso de linhas de execução no Android, consulte Processos e linhas de execução.

O snippet de código a seguir é uma maneira fácil de fazer uma transferência de dados síncrona. Seu código precisa ter mais lógica para encontrar corretamente a interface e os endpoints corretos para se comunicar, além de fazer qualquer transferência de dados em uma linha de execução diferente da linha de execução de interface principal:

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

Para enviar dados de forma assíncrona, use a classe UsbRequest para initialize e queue (uma solicitação assíncrona) e aguarde o resultado com requestWait().

Como encerrar a comunicação com um dispositivo

Quando você terminar de se comunicar com um dispositivo ou se ele tiver sido desconectado, feche UsbInterface e UsbDeviceConnection chamando releaseInterface() e close(). Para ouvir eventos desconectados, crie um broadcast receiver como abaixo:

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

Criar o broadcast receiver dentro do app, e não o manifesto, permite que seu aplicativo processe apenas eventos desconectados enquanto está em execução. Dessa forma, os eventos independentes são enviados apenas para o app que está em execução no momento e não são transmitidos para todos eles.