Descripción general del host USB

Cuando tu dispositivo con Android está en modo de host USB, actúa como host USB, alimenta el bus y enumera los dispositivos USB conectados. El modo de host USB es compatible con Android 3.1 y versiones posteriores.

Descripción general de la API

Antes de comenzar, es importante comprender las clases con las que necesitas trabajar. En la siguiente tabla, se describen las APIs del host USB en el paquete android.hardware.usb.

Tabla 1: API del host USB

Clase Descripción
UsbManager Te permite enumerar los dispositivos USB conectados y establecer una comunicación con ellos.
UsbDevice Representa un dispositivo USB conectado y contiene métodos para acceder a su información de identificación, interfaces y extremos.
UsbInterface Representa una interfaz de un dispositivo USB, que define un conjunto de funcionalidades del dispositivo. Un dispositivo puede tener una o más interfaces para comunicarse.
UsbEndpoint Representa un extremo de la interfaz, que es un canal de comunicación para esta. Una interfaz puede tener uno o más extremos y, por lo general, tiene extremos de entrada y salida para una comunicación bidireccional con el dispositivo.
UsbDeviceConnection Representa una conexión con el dispositivo, que transfiere datos en los extremos. Esta clase te permite enviar y recibir datos de forma síncrona o asíncrona.
UsbRequest Representa una solicitud asíncrona para comunicarse con un dispositivo a través de una UsbDeviceConnection.
UsbConstants Define las constantes USB que corresponden a las definiciones en linux/usb/ch9.h del kernel de Linux.

En la mayoría de las situaciones, debes usar todas estas clases (UsbRequest solo se requiere si realizas una comunicación asíncrona) cuando te comunicas con un dispositivo USB. En general, obtienes un UsbManager para recuperar el UsbDevice deseado. Cuando tienes el dispositivo, debes encontrar el UsbInterface apropiado y el UsbEndpoint de esa interfaz para comunicarte. Una vez que obtengas el extremo correcto, abre una UsbDeviceConnection para comunicarte con el dispositivo USB.

Requisitos del manifiesto de Android

En la siguiente lista, se describe lo que necesitas agregar al archivo de manifiesto de tu aplicación antes de que funcione con las APIs del host USB:

  • Dado que no se garantiza que todos los dispositivos con Android admitan las APIs del host USB, incluye un elemento <uses-feature> que declare que tu aplicación usa la función android.hardware.usb.host.
  • Establece la versión mínima del SDK de la aplicación en API nivel 12 o posterior. Las APIs del host USB no están presentes en los niveles de API anteriores.
  • Si quieres que tu aplicación reciba una notificación sobre un dispositivo USB conectado, especifica un par de elementos <intent-filter> y <meta-data> para el intent android.hardware.usb.action.USB_DEVICE_ATTACHED en tu actividad principal. El elemento <meta-data> apunta a un archivo de recursos XML externo que declara información de identificación sobre el dispositivo que quieres detectar.

    En el archivo de recursos XML, declara elementos <usb-device> para los dispositivos USB que deseas filtrar. En la siguiente lista, se describen los atributos de <usb-device>. En general, usa el proveedor y el ID del producto si deseas filtrar por un dispositivo específico, y también la clase, la subclase y el protocolo si deseas filtrar por un grupo de dispositivos USB, como dispositivos de almacenamiento masivo o cámaras digitales. Puedes especificar ninguno de estos atributos o todos ellos. No especificar ningún atributo coincide con todos los dispositivos USB. Por lo tanto, haz esto solo si tu aplicación lo requiere:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (dispositivo o interfaz)

    Guarda el archivo de recursos en el directorio res/xml/. El nombre del archivo de recursos (sin la extensión .xml) debe ser el mismo que el que especificaste en el elemento <meta-data>. El formato del archivo de recursos XML se indica en el ejemplo de más abajo.

Ejemplos de archivos de manifiesto y de recursos

A continuación, se muestra un ejemplo del manifiesto y de su archivo de recursos correspondiente:

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

En este caso, el siguiente archivo de recursos debe guardarse en res/xml/device_filter.xml. Especifica que se debe filtrar cualquier dispositivo USB con los atributos especificados:

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

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

Cómo trabajar con dispositivos

Cuando los usuarios conectan dispositivos USB a un dispositivo con tecnología Android, el sistema Android puede determinar si a tu aplicación le interesa el dispositivo conectado. De ser así, puedes configurar la comunicación con el dispositivo si lo deseas. Para hacerlo, la aplicación debe realizar lo siguiente:

  1. Descubre los dispositivos USB conectados usando un filtro de intents para recibir notificaciones cuando el usuario conecte un dispositivo USB o enumerando los dispositivos USB que ya están conectados.
  2. Si aún no lo tiene, debe solicitar permiso al usuario para conectarse al dispositivo USB.
  3. Debe comunicarse con el dispositivo USB leyendo y escribiendo datos en los extremos de interfaz apropiados.

Cómo descubrir un dispositivo

Tu aplicación puede descubrir dispositivos USB usando un filtro de intents para recibir notificaciones cuando el usuario conecta un dispositivo o enumerando los dispositivos USB que ya están conectados. El uso de un filtro de intents es útil si deseas que tu aplicación detecte automáticamente un dispositivo deseado. La enumeración de dispositivos USB conectados es útil si deseas obtener una lista de todos los dispositivos conectados o si tu aplicación no filtró por un intent.

Cómo usar un filtro de intents

Para que tu aplicación descubra un dispositivo USB en particular, puedes especificar un filtro de intents y así filtrar por el intent android.hardware.usb.action.USB_DEVICE_ATTACHED. Junto con este filtro de intents, debes especificar un archivo de recursos que especifique las propiedades del dispositivo USB, como el ID del producto y del proveedor. Cuando los usuarios conectan un dispositivo que coincide con el filtro de tu dispositivo, el sistema les presenta un diálogo que les pregunta si desean iniciar tu aplicación. Si los usuarios aceptan, tu aplicación automáticamente tiene permiso para acceder al dispositivo hasta que este se desconecta.

En el siguiente ejemplo, se muestra cómo declarar el filtro de intents:

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

En el siguiente ejemplo, se muestra cómo declarar el archivo de recursos correspondiente que especifica los dispositivos USB que te interesan:

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

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

En tu actividad, puedes obtener el UsbDevice que representa el dispositivo conectado a partir del intent de la siguiente manera:

Kotlin

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

Java

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

Cómo enumerar dispositivos

Si a tu aplicación le interesa inspeccionar todos los dispositivos USB conectados actualmente mientras se está ejecutando, puede enumerar los dispositivos en el bus. Usa el método getDeviceList() para obtener un mapa hash de todos los dispositivos USB que están conectados. Si deseas obtener un dispositivo del mapa hash, usa como clave el nombre del dispositivo 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");

Si lo deseas, también puedes obtener un iterador del mapa hash y procesar cada dispositivo uno por uno:

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
}

Cómo obtener permiso para comunicarse con un dispositivo

Antes de comunicarse con el dispositivo USB, tu aplicación debe tener el permiso de tus usuarios.

Nota: Si tu aplicación usa un filtro de intents para descubrir dispositivos USB a medida que se conectan, recibe permiso automáticamente si el usuario permite que tu aplicación controle el intent. De lo contrario, debes solicitar permiso de forma explícita en tu aplicación antes de conectarte al dispositivo.

Pedir permiso explícito puede ser necesario en algunas situaciones, como cuando tu aplicación enumera los dispositivos USB que ya están conectados y luego desea comunicarse con uno. Debes verificar que se haya otorgado permiso para acceder a un dispositivo antes de intentar comunicarte con él. De lo contrario, aparecerá un error de tiempo de ejecución si el usuario rechazó el permiso para acceder al dispositivo.

A fin de obtener permiso de manera explícita, primero crea un receptor de emisión. Este receptor escucha el intent que se transmite cuando llamas a requestPermission(). La llamada a requestPermission() muestra un diálogo al usuario en el que le solicita permiso para conectarse al dispositivo. En el siguiente código de ejemplo, se muestra cómo crear el receptor de emisión:

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 el receptor de emisión, agrega lo siguiente al método onCreate() de tu actividad:

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 que aparezca el diálogo en el que se solicita permiso de acceso al dispositivo, llama al método requestPermission():

Kotlin

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

Java

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

Cuando los usuarios responden el diálogo, tu receptor de emisión recibe el intent que contiene el EXTRA_PERMISSION_GRANTED adicional, que es un booleano que representa la respuesta. Verifica que este valor adicional tenga el valor verdadero antes de conectarte al dispositivo.

Cómo comunicarse con un dispositivo

La comunicación con un dispositivo USB puede ser síncrona o asíncrona. En cualquier caso, debes crear un subproceso nuevo en el que se lleven a cabo todas las transmisiones de datos, de modo que no bloquees el subproceso de IU. Para configurar correctamente la comunicación con un dispositivo, debes obtener los UsbInterface y UsbEndpoint apropiados del dispositivo con el que quieres comunicarte y enviar solicitudes en este extremo con un UsbDeviceConnection. En general, debes realizar lo siguiente:

  • Verifica los atributos de un objeto UsbDevice, como el ID del producto, el ID del proveedor o la clase de dispositivo, para determinar si deseas comunicarte con el dispositivo o no.
  • Cuando tengas la certeza de que quieres comunicarte con el dispositivo, busca el UsbInterface apropiado que quieres usar para comunicarte junto con el UsbEndpoint apropiado de esa interfaz. Las interfaces pueden tener uno o más extremos y, por lo general, tienen un extremo de entrada y uno de salida para una comunicación bidireccional.
  • Cuando encuentres el extremo correcto, abre una UsbDeviceConnection en ese extremo.
  • Proporciona los datos que deseas transmitir en el extremo con el método bulkTransfer() o controlTransfer(). Debes realizar este paso en otro subproceso para evitar que se bloquee el subproceso de IU principal. Para obtener más información sobre el uso de subprocesos en Android, consulta Procesos y subprocesos.

El siguiente fragmento de código es una forma trivial de realizar una transferencia de datos síncrona. Tu código debería tener más lógica para encontrar correctamente la interfaz y los extremos correctos con los cuales comunicarse, y también debe realizar las transferencias de datos en un subproceso diferente al subproceso de IU 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

Si quieres enviar datos de forma asíncrona, usa la clase UsbRequest para realizar initialize y queue en una solicitud asíncrona. Luego, espera el resultado con requestWait().

Cómo finalizar la comunicación con un dispositivo

Cuando hayas terminado de comunicarte con un dispositivo, o si este se desconectó, llama a releaseInterface() y close() para cerrar UsbInterface y UsbDeviceConnection. Para escuchar eventos de desconexión, crea un receptor de emisión como se muestra a continuación:

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

Crear el receptor de emisión dentro de la aplicación, y no en el manifiesto, permite que la aplicación solo maneje eventos de desconexión mientras se está ejecutando. De esta manera, los eventos de desconexión solo se envían a la aplicación que se está ejecutando actualmente y no se transmiten a todas las aplicaciones.