Tổng quan về máy chủ USB

Khi thiết bị chạy Android của bạn ở chế độ hỗ trợ USB, thiết bị sẽ hoạt động như một thiết bị lưu trữ USB, cấp nguồn cho bus, và liệt kê các thiết bị USB được kết nối. Chế độ hỗ trợ USB được hỗ trợ trong Android 3.1 trở lên.

Tổng quan về API

Trước khi bắt đầu, điều quan trọng là bạn phải hiểu rõ các lớp mà bạn cần xử lý. Chiến lược phát hành đĩa đơn bảng sau đây mô tả các API máy chủ USB trong gói android.hardware.usb.

Bảng 1. API lưu trữ USB

Lớp Mô tả
UsbManager Cho phép bạn liệt kê và giao tiếp với các thiết bị USB được kết nối.
UsbDevice Đại diện cho một thiết bị USB được kết nối và chứa các phương thức để truy cập vào thông tin nhận dạng thiết bị thông tin, giao diện và điểm cuối.
UsbInterface Biểu thị một giao diện của thiết bị USB, xác định một tập hợp chức năng cho thiết bị. Một thiết bị có thể có một hoặc nhiều giao diện để giao tiếp.
UsbEndpoint Đại diện cho điểm cuối của giao diện, là kênh liên lạc của giao diện này. Một giao diện có thể có một hoặc nhiều điểm cuối và thường có các điểm cuối đầu vào và đầu ra để giao tiếp hai chiều với thiết bị.
UsbDeviceConnection Biểu thị một kết nối đến thiết bị có chức năng truyền dữ liệu trên các thiết bị đầu cuối. Lớp này cho phép bạn gửi dữ liệu qua lại một cách đồng bộ hoặc không đồng bộ.
UsbRequest Biểu thị một yêu cầu không đồng bộ để giao tiếp với một thiết bị thông qua UsbDeviceConnection.
UsbConstants Xác định các hằng số USB tương ứng với các định nghĩa trong linux/usb/ch9.h của Linux nhân hệ điều hành.

Trong hầu hết trường hợp, bạn cần sử dụng tất cả các lớp này (chỉ bắt buộc phải sử dụng UsbRequest nếu bạn đang giao tiếp không đồng bộ) khi giao tiếp với thiết bị USB. Nhìn chung, bạn sẽ nhận được UsbManager để truy xuất UsbDevice mong muốn. Khi có thiết bị, bạn cần tìm UsbInterface thích hợp và UsbEndpoint của thiết bị đó để giao tiếp. Sau khi bạn có được đúng điểm cuối, hãy mở một UsbDeviceConnection để giao tiếp với thiết bị USB.

Yêu cầu về tệp kê khai Android

Danh sách sau đây mô tả những gì bạn cần thêm vào tệp kê khai của ứng dụng trước khi làm việc với API máy chủ USB:

  • Vì không phải tất cả thiết bị chạy Android đều đảm bảo hỗ trợ API máy chủ USB, thêm một phần tử <uses-feature> khai báo rằng ứng dụng của bạn sử dụng tính năng android.hardware.usb.host.
  • Đặt SDK tối thiểu của ứng dụng thành API cấp 12 trở lên. API máy chủ USB không có trên các cấp độ API trước đó.
  • Nếu bạn muốn ứng dụng của mình được thông báo về thiết bị USB đi kèm, hãy chỉ định Cặp phần tử <intent-filter><meta-data> cho phần tử Ý định android.hardware.usb.action.USB_DEVICE_ATTACHED trong hoạt động chính của bạn. Chiến lược phát hành đĩa đơn Phần tử <meta-data> trỏ đến một tệp tài nguyên XML bên ngoài có khai báo thông tin nhận dạng về thiết bị mà bạn muốn phát hiện.

    Trong tệp tài nguyên XML, hãy khai báo các phần tử <usb-device> cho USB thiết bị mà bạn muốn lọc. Danh sách sau đây mô tả các thuộc tính của <usb-device>. Nói chung, hãy sử dụng mã sản phẩm và nhà cung cấp nếu bạn muốn lọc cho một thiết bị cụ thể và sử dụng lớp, lớp con và giao thức nếu muốn lọc theo một nhóm các thiết bị USB, chẳng hạn như thiết bị lưu trữ dung lượng lớn hoặc máy ảnh kỹ thuật số. Bạn có thể chỉ định không có gì hoặc tất cả các thuộc tính này. Việc chỉ định không có thuộc tính nào phù hợp với mọi thiết bị USB, vì vậy, bạn chỉ nên làm việc này nếu ứng dụng của bạn yêu cầu:

    • vendor-id
    • product-id
    • class
    • subclass
    • protocol (thiết bị hoặc giao diện)

    Lưu tệp tài nguyên trong thư mục res/xml/. Tên tệp tài nguyên (không có đuôi .xml) phải giống với đuôi mà bạn đã chỉ định trong Phần tử <meta-data>. Định dạng của tệp tài nguyên XML nằm trong ví dụ bên dưới.

Ví dụ về tệp kê khai và tệp tài nguyên

Ví dụ sau đây cho thấy một tệp kê khai mẫu và tệp tài nguyên tương ứng của tệp kê khai đó:

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

Trong trường hợp này, tệp tài nguyên sau cần được lưu trong res/xml/device_filter.xml và chỉ định rằng mọi thiết bị USB có các thuộc tính cần được lọc:

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

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

Hoạt động với thiết bị

Khi người dùng kết nối thiết bị USB với một thiết bị chạy Android, hệ thống Android có thể xác định ứng dụng của bạn có quan tâm đến thiết bị được kết nối hay không. Nếu có, bạn có thể thiết lập giao tiếp với thiết bị nếu muốn. Để làm được việc này, ứng dụng của bạn phải:

  1. Khám phá các thiết bị USB được kết nối bằng cách sử dụng bộ lọc ý định để nhận thông báo khi người dùng kết nối một thiết bị USB hoặc bằng cách liệt kê các thiết bị USB đã được kết nối.
  2. Yêu cầu người dùng cấp quyền kết nối với thiết bị USB nếu chưa được nhận.
  3. Giao tiếp với thiết bị USB bằng cách đọc và ghi dữ liệu trên giao diện thích hợp điểm cuối.

Khám phá một thiết bị

Ứng dụng của bạn có thể phát hiện các thiết bị USB bằng cách sử dụng bộ lọc ý định để nhận được thông báo khi người dùng kết nối một thiết bị hoặc bằng cách liệt kê các thiết bị USB đã được kết nối. Sử dụng rất hữu ích nếu bạn muốn ứng dụng của bạn tự động phát hiện thiết bị mong muốn. Việc liệt kê các thiết bị USB được kết nối rất hữu ích nếu bạn muốn có danh sách tất cả trên các thiết bị đã kết nối hoặc nếu ứng dụng của bạn không lọc ý định.

Sử dụng bộ lọc ý định

Để ứng dụng của bạn phát hiện một thiết bị USB cụ thể, bạn có thể chỉ định bộ lọc ý định để bộ lọc cho ý định android.hardware.usb.action.USB_DEVICE_ATTACHED. Cùng với bộ lọc ý định này, bạn cần chỉ định một tệp tài nguyên chỉ định các thuộc tính của USB thiết bị, chẳng hạn như sản phẩm và mã nhà cung cấp. Khi người dùng kết nối một thiết bị với thiết bị đó bộ lọc, hệ thống sẽ hiển thị cho họ một hộp thoại hỏi xem họ có muốn khởi động ứng dụng của bạn hay không. Nếu người dùng chấp nhận, ứng dụng của bạn sẽ tự động có quyền truy cập vào thiết bị cho đến khi thiết bị đã bị ngắt kết nối.

Ví dụ sau đây cho thấy cách khai báo bộ lọc ý định:

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

Ví dụ sau đây trình bày cách khai báo tệp tài nguyên tương ứng chỉ định Các thiết bị USB mà bạn quan tâm:

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

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

Trong hoạt động, bạn có thể lấy UsbDevice biểu thị thiết bị đính kèm theo ý định như sau:

Kotlin

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

Java

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

Liệt kê thiết bị

Nếu ứng dụng của bạn muốn kiểm tra tất cả thiết bị USB hiện được kết nối trong khi ứng dụng đang chạy, ứng dụng có thể liệt kê các thiết bị trên xe buýt. Sử dụng phương thức getDeviceList() để tải bản đồ băm của tất cả thiết bị USB được kết nối. Sơ đồ băm được khoá theo tên của thiết bị USB nếu bạn muốn lấy một thiết bị từ bản đồ.

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

Nếu muốn, bạn cũng có thể chỉ cần lấy một biến lặp từ bản đồ băm và xử lý từng thiết bị một theo một:

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
}

Xin phép giao tiếp với một thiết bị

Trước khi kết nối với thiết bị USB, ứng dụng của bạn phải được người dùng.

Lưu ý: Nếu ứng dụng của bạn sử dụng bộ lọc ý định để khám phá các thiết bị USB khi chúng được kết nối, bộ lọc sẽ tự động nhận nếu người dùng cho phép ứng dụng của bạn xử lý ý định. Nếu không, bạn phải yêu cầu trong ứng dụng của bạn một cách rõ ràng trước khi kết nối với thiết bị.

Bạn có thể cần phải yêu cầu cấp quyền rõ ràng trong một số trường hợp như khi ứng dụng liệt kê các thiết bị USB đã được kết nối và sau đó muốn giao tiếp với một. Bạn phải kiểm tra quyền truy cập vào một thiết bị trước khi cố gắng giao tiếp với thiết bị đó. Nếu không, bạn sẽ gặp lỗi thời gian chạy nếu người dùng từ chối cấp quyền truy cập vào thiết bị.

Để có được quyền rõ ràng, trước tiên, hãy tạo một broadcast receiver. Bộ thu này lắng nghe ý định được truyền tin khi bạn gọi requestPermission(). Lệnh gọi đến requestPermission() sẽ hiển thị một hộp thoại cho lệnh gọi đến người dùng yêu cầu quyền kết nối với thiết bị. Mã mẫu sau đây cho biết cách tạ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);
                }
            }
        }
    }
};

Để đăng ký broadcast receiver, hãy thêm đoạn mã này vào phương thức onCreate() trong hoạt động:

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), PendingIntent.FLAG_IMMUTABLE)
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), PendingIntent.FLAG_IMMUTABLE);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter);

Để hiện hộp thoại yêu cầu người dùng cấp quyền kết nối với thiết bị, hãy gọi phương thức requestPermission():

Kotlin

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

Java

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

Khi người dùng trả lời hộp thoại, broadcast receiver của bạn sẽ nhận được ý định chứa EXTRA_PERMISSION_GRANTED bổ sung, là một giá trị boolean cho câu trả lời. Kiểm tra phần bổ sung này để biết giá trị true trước khi kết nối với thiết bị.

Giao tiếp với thiết bị

Hoạt động giao tiếp với thiết bị USB có thể đồng bộ hoặc không đồng bộ. Trong cả hai trường hợp, bạn nên tạo một luồng mới để thực hiện tất cả quá trình truyền dữ liệu, như vậy bạn sẽ không chặn Luồng giao diện người dùng. Để thiết lập giao tiếp đúng cách với một thiết bị, bạn cần có UsbInterfaceUsbEndpoint của thiết bị mà bạn muốn giao tiếp và gửi yêu cầu trên điểm cuối này bằng UsbDeviceConnection. Nói chung, mã của bạn phải:

  • Kiểm tra các thuộc tính của đối tượng UsbDevice, chẳng hạn như mã sản phẩm, mã nhà cung cấp hoặc lớp thiết bị để tìm hiểu xem bạn có muốn giao tiếp với thiết bị.
  • Khi bạn chắc chắn rằng mình muốn giao tiếp với thiết bị, hãy tìm UsbInterface mà bạn muốn dùng để giao tiếp cùng với UsbEndpoint thích hợp của giao diện đó. Giao diện có thể có một và thường sẽ có một điểm cuối đầu vào và đầu ra cho hai chiều giao tiếp.
  • Khi bạn tìm thấy điểm cuối chính xác, hãy mở một UsbDeviceConnection trên điểm cuối đó.
  • Cung cấp dữ liệu bạn muốn truyền trên điểm cuối bằng phương thức bulkTransfer() hoặc controlTransfer(). Bạn nên thực hiện bước này trong một luồng khác để ngăn chặn việc chặn luồng giao diện người dùng chính. Để biết thêm thông tin về cách sử dụng luồng trong Android, hãy xem phần Quy trình và Chuỗi cuộc trò chuyện.

Đoạn mã sau đây là một cách đơn giản để thực hiện quá trình chuyển dữ liệu đồng bộ. Mã của bạn cần có nhiều logic hơn để tìm chính xác giao diện và điểm cuối chính xác để giao tiếp đồng thời nên chuyển dữ liệu trong một luồng khác với luồng giao diện người dùng chính:

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

Để gửi dữ liệu không đồng bộ, hãy sử dụng lớp UsbRequest cho initializequeue một yêu cầu không đồng bộ, sau đó chờ kết quả cùng với requestWait().

Chấm dứt hoạt động giao tiếp với thiết bị

Khi bạn kết nối xong với một thiết bị hoặc nếu thiết bị đã bị ngắt kết nối, hãy đóng UsbInterfaceUsbDeviceConnection bằng đang gọi releaseInterface()close(). Để theo dõi các sự kiện đã tách rời, hãy tạo một broadcast receiver như dưới đây:

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

Việc tạo broadcast receiver trong ứng dụng chứ không phải trong tệp kê khai cho phép để chỉ xử lý các sự kiện được tách ra khi đang chạy. Bằng cách này, các sự kiện được tách chỉ gửi tới ứng dụng hiện đang chạy và không truyền phát tới tất cả ứng dụng.