La Vista previa para desarrolladores de Android 11 ya está disponible. Pruébala y comparte tus comentarios.

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 API 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 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 funciones 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 y 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 los casos, debes usar todas estas clases (UsbRequest solo se necesita 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 la UsbInterface apropiada 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 API del host USB.

  • Debido a que no se garantiza que todos los dispositivos con Android admitan las API 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 API 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> dirige 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 quieres filtrar. En la siguiente lista, se describen los atributos de <usb-device>. En general, usa el proveedor y el ID del producto si quieres filtrar por un dispositivo específico; en cambio, y usa 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 todos estos atributos o no especificar ninguno. Si no especificas ningún atributo, la coincidencia se realiza con todos los dispositivos USB, así que haz eso solamente 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 especificado en el elemento <meta-data>. El formato del archivo de recursos XML se indica en el ejemplo 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 se debe guardar en res/xml/device_filter.xml. Especifica que se debe filtrar cualquier dispositivo USB con los atributos indicados:

    <?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 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. Debe descubrir 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 utilizando 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 quieres que tu aplicación detecte automáticamente un dispositivo específico. 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 detalle las propiedades del dispositivo USB, como el ID del producto y del proveedor. Cuando los usuarios conectan un dispositivo que coincide con tu filtro, el sistema les muestra un diálogo que les pregunta si desean iniciar tu aplicación. Si 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 desde el 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 en ese momento 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 quieres 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 de a uno por vez:

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 los dispositivos USB cuando se conectan, automáticamente recibe permiso si el usuario autoriza a la aplicación a manejar el intent. De lo contrario, debes solicitar permiso de manera explícita en la aplicación antes de conectarte al dispositivo.

Pedir permiso explícito puede ser necesario en algunos casos, 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 emite 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 ejemplo de código, 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 al 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 booleano adicional tenga un 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 nuevo subproceso en el que se lleven a cabo todas las transmisiones de datos para que no se bloquee el subproceso de IU. Para configurar de manera correcta la comunicación con un dispositivo, debes obtener la UsbInterface y el UsbEndpoint apropiados del dispositivo con el que quieres comunicarte y enviar solicitudes en este extremo con una UsbDeviceConnection. En general, debes realizar lo siguiente:

  • Comprobar 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 o no con el dispositivo.
  • Cuando tengas la certeza de que quieres comunicarte, busca la UsbInterface apropiada que quieras usar para comunicarte y el UsbEndpoint adecuado de esa interfaz. Las interfaces pueden tener uno o más extremos y, por lo general, tienen un extremo de entrada y otro 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 a fin de evitar que se bloquee el subproceso de IU principal. Para obtener más información sobre cómo usar 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 debe tener más lógica a fin de encontrar correctamente la interfaz adecuada y los extremos para comunicarse. 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 aplicar initialize y queue a una solicitud asíncrona. Luego, espera el resultado con requestWait().

Para obtener más información, consulta el ejemplo de AdbTest, que muestra cómo realizar transferencias masivas asíncronas, y el ejemplo de MissileLauncher, que muestra cómo escuchar un extremo de interrupción de manera asíncrona.

Cómo finalizar la comunicación con un dispositivo

Cuando hayas finalizado la comunicación con un dispositivo, o si este se desconectó, llama a releaseInterface() y a close() para cerrar UsbInterface y UsbDeviceConnection. Si quieres escuchar eventos de desconexión, crea un receptor de emisión como se indica aquí:

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 del manifiesto, permite que la app 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 en el momento y no se emiten a todas las aplicaciones.