USB 호스트 개요

Android 지원 기기가 USB 호스트 모드일 때 USB 호스트 역할을 하고 버스에 전원을 공급하며 연결된 USB 기기를 열거합니다. USB 호스트 모드는 Android 3.1 이상에서 지원됩니다.

API 개요

시작하기 전에 먼저 사용해야 하는 클래스를 이해하는 것이 중요합니다. 다음 표는 android.hardware.usb 패키지의 USB 호스트 API를 설명합니다.

표 1. USB 호스트 API

클래스 설명
UsbManager 이 클래스를 사용하면 연결된 USB 기기를 열거하고 기기와 통신할 수 있습니다.
UsbDevice 연결된 USB 기기를 나타내며 식별 정보, 인터페이스 및 엔드포인트에 액세스하는 메서드를 포함합니다.
UsbInterface 기기의 기능 집합을 정의하는 USB 기기의 인터페이스를 나타냅니다. 기기에는 통신할 인터페이스가 하나 이상 있을 수 있습니다.
UsbEndpoint 인터페이스 엔드포인트를 나타내며 이 인터페이스의 통신 채널입니다. 인터페이스에는 엔드포인트가 하나 이상 있을 수 있으며 일반적으로 기기와의 양방향 통신을 위한 입력 및 출력 엔드포인트가 있습니다.
UsbDeviceConnection 엔드포인트에서 데이터를 전송하는 기기와의 연결을 나타냅니다. 이 클래스를 사용하면 동기식 또는 비동기식으로 데이터를 주고받을 수 있습니다.
UsbRequest UsbDeviceConnection을 통해 기기와 통신하는 비동기식 요청을 나타냅니다.
UsbConstants Linux 커널의 linux/usb/ch9.h에 있는 정의에 상응하는 USB 상수를 정의합니다.

대부분의 상황에서는 USB 기기와 통신할 때 이러한 클래스를 모두 사용해야 합니다 (UsbRequest는 비동기 통신을 하는 경우에만 필요). 일반적으로 원하는 UsbDevice를 검색하려면 UsbManager를 얻어야 합니다. 기기가 있으면 통신할 적절한 UsbInterface 및 이 인터페이스의 UsbEndpoint를 찾아야 합니다. 적절한 엔드포인트를 얻었으면 USB 기기와 통신할 UsbDeviceConnection을 열어야 합니다.

Android manifest 요구사항

다음 목록에서는 USB 호스트 API로 작업하기 전에 애플리케이션의 매니페스트 파일에 추가해야 하는 항목을 설명합니다.

  • 모든 Android 지원 기기가 USB 호스트 API를 지원한다는 보장이 없으므로 애플리케이션에서 android.hardware.usb.host 기능을 사용한다고 선언하는 <uses-feature> 요소를 포함합니다.
  • 애플리케이션의 최소 SDK를 API 레벨 12 이상으로 설정해야 합니다. 이전 API 레벨에는 USB 호스트 API가 없습니다.
  • 애플리케이션에서 연결된 USB 기기에 관한 알림을 받도록 하려면 기본 활동에서 android.hardware.usb.action.USB_DEVICE_ATTACHED 인텐트의 <intent-filter><meta-data> 요소 쌍을 지정합니다. <meta-data> 요소는 감지하려는 기기에 관한 식별 정보를 선언하는 외부 XML 리소스 파일을 가리킵니다.

    XML 리소스 파일에서 필터링하려는 USB 기기의 <usb-device> 요소를 선언합니다. 다음 목록은 <usb-device>의 속성을 설명합니다. 일반적으로 특정 기기를 기준으로 필터링하려면 공급업체 및 제품 ID를 사용하고 대용량 저장소 기기 또는 디지털 카메라와 같은 USB 기기 그룹을 필터링하려면 클래스, 서브클래스, 프로토콜을 사용합니다. 이러한 속성을 모두 지정하거나 전혀 지정하지 않을 수 있습니다. 속성을 지정하지 않으면 모든 USB 기기와 일치하므로 애플리케이션에서 필요로 하는 경우에만 이렇게 합니다.

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol(기기 또는 인터페이스)

    리소스 파일을 res/xml/ 디렉터리에 저장해야 합니다. 리소스 파일 이름(.xml 확장자 제외)은 <meta-data> 요소에서 지정한 이름과 동일해야 합니다. XML 리소스 파일의 형식은 아래 에 나와 있습니다.

manifest 및 리소스 파일 예

다음 예는 샘플 manifest 및 이에 상응하는 리소스 파일을 보여줍니다.

<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 인텐트를 필터링할 인텐트 필터를 지정하면 됩니다. 이 인텐트 필터와 함께 제품 및 공급업체 ID와 같은 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를 인텐트에서 가져올 수 있습니다.

Kotlin

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

Java

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

기기 열거

애플리케이션이 실행되는 동안 현재 연결된 모든 USB 기기를 검사하는 데 관심이 있다면 버스에 있는 기기를 열거할 수 있습니다. getDeviceList() 메서드를 사용하여 연결된 모든 USB 기기의 해시 맵을 가져옵니다. 맵에서 기기를 가져오려면 해시 맵에 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");

원하는 경우 해시 맵에서 반복자를 가져와 각 기기를 하나씩 처리할 수도 있습니다.

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
}

기기와 통신할 수 있는 권한 얻기

USB 기기와 통신하려면 애플리케이션에서 사용자로부터 권한을 받아야 합니다.

참고: 애플리케이션이 인텐트 필터를 사용하여 연결된 USB 기기를 검색하는 경우, 사용자가 애플리케이션에서 인텐트를 처리하도록 허용하면 애플리케이션이 권한을 자동으로 수신합니다. 권한이 없다면 기기에 연결하기 전에 애플리케이션에서 명시적으로 권한을 요청해야 합니다.

애플리케이션이 이미 연결된 USB 기기를 열거한 후 기기와 통신하려는 경우와 같은 일부 상황에서는 명시적으로 권한을 요청해야 할 수 있습니다. 기기와 통신하려고 하기 전에 기기에 액세스할 수 있는 권한이 있는지 확인해야 합니다. 그러지 않으면 사용자가 기기 액세스 권한을 거부하면 런타임 오류가 발생합니다.

명시적으로 권한을 얻으려면 먼저 broadcast receiver를 생성하세요. broadcast receiver는 requestPermission()를 호출할 때 브로드캐스트를 받는 인텐트를 수신 대기합니다. requestPermission()를 호출하면 사용자에게 기기 연결 권한을 요청하는 대화상자가 표시됩니다. 다음 샘플 코드는 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);
                }
            }
        }
    }
};

broadcast receiver를 등록하려면 활동의 onCreate() 메서드에 다음을 추가합니다.

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

사용자에게 기기 연결 권한을 요청하는 대화상자를 표시하려면 다음과 같이 requestPermission() 메서드를 호출하세요.

Kotlin

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

Java

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

사용자가 대화상자에 응답할 때 broadcast receiver는 답변을 나타내는 불리언인 EXTRA_PERMISSION_GRANTED 엑스트라가 포함된 인텐트를 수신합니다. 기기에 연결하기 전에 이 엑스트라가 true 값인지 확인합니다.

기기와 통신

USB 기기와의 통신은 동기적 또는 비동기적일 수 있습니다. 두 경우 모두 모든 데이터 전송을 실행할 새 스레드를 만들어 UI 스레드를 차단하지 않도록 해야 합니다. 기기와의 통신을 올바르게 설정하려면 통신하려는 기기의 적절한 UsbInterfaceUsbEndpoint를 가져오고 이 엔드포인트에서 UsbDeviceConnection를 사용하여 요청을 전송해야 합니다. 일반적으로 코드에서 다음과 같이 해야 합니다.

  • 제품 ID, 공급업체 ID 또는 기기 클래스와 같은 UsbDevice 객체의 속성을 확인하여 기기와 통신할지 여부를 알아봅니다.
  • 기기와 통신하려는 것이 확실하다면 통신에 사용할 적절한 UsbInterface와 함께 이 인터페이스의 적절한 UsbEndpoint를 찾습니다. 인터페이스에는 엔드포인트가 하나 이상 있을 수 있으며 일반적으로 양방향 통신을 위한 입력 및 출력 엔드포인트가 있습니다.
  • 올바른 엔드포인트를 찾으면 해당 엔드포인트에서 UsbDeviceConnection을 엽니다.
  • bulkTransfer() 또는 controlTransfer() 메서드를 사용하여 엔드포인트에서 전송하려는 데이터를 제공해야 합니다. 기본 UI 스레드가 차단되지 않도록 하려면 다른 스레드에서 이 단계를 실행해야 합니다. Android에서 스레드를 사용하는 방법에 관한 자세한 내용은 프로세스 및 스레드를 참고하세요.

다음 코드 스니펫은 동기식 데이터 전송을 실행하는 간단한 방법입니다. 코드에는 통신할 올바른 인터페이스와 엔드포인트를 올바르게 찾는 로직이 더 많이 있어야 하며 기본 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

데이터를 비동기식으로 전송하려면 UsbRequest 클래스를 사용하여 비동기식 요청을 initializequeue한 다음 requestWait()로 결과를 기다립니다.

기기와의 통신 종료

기기와의 통신이 완료되었거나 기기가 분리되었다면 releaseInterface()close()를 호출하여 UsbInterfaceUsbDeviceConnection를 닫습니다. 분리된 이벤트를 수신 대기하려면 다음과 같이 broadcast receiver를 만드세요.

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

매니페스트가 아닌 애플리케이션 내에서 broadcast receiver를 만들면 애플리케이션이 실행 중에 분리된 이벤트만 처리할 수 있습니다. 이렇게 하면 분리된 이벤트가 현재 실행 중인 애플리케이션에만 전송되며 모든 애플리케이션에 브로드캐스트되지는 않습니다.