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

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

Tổng quan về API

Trước khi bắt đầu, bạn cần hiểu rõ các lớp mình cần sử dụng. Bảng sau đây mô tả các API lưu trữ USB trong gói android.hardware.usb.

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

Lớp Nội dung mô tả
UsbManager Cho phép bạn liệt kê và giao tiếp với thiết bị USB đã kết nối.
UsbDevice Biểu thị một thiết bị USB đã 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, giao diện và điểm cuối của thiết bị đó.
UsbInterface Đại diện cho một giao diện của thiết bị USB, giúp xác định một tập hợp chức năng của 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 một điểm cuối giao diện, là kênh giao tiếp 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ó đ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 chuyển dữ liệu trên các điểm 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 nhân Linux.

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ẽ có đượ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 giao diện đó để giao tiếp. Sau khi có được điểm cuối chính xác, hãy mở 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 nội dung 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 lưu trữ USB:

  • Không phải thiết bị chạy Android nào cũng hỗ trợ API lưu trữ USB, hãy thêm 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 lưu trữ USB không có ở các cấp độ API trước đó.
  • Nếu bạn muốn ứng dụng của mình nhận được thông báo về một thiết bị USB được đính kèm, hãy chỉ định một cặp phần tử <intent-filter><meta-data> cho ý định android.hardware.usb.action.USB_DEVICE_ATTACHED trong hoạt động chính của bạn. Phần tử <meta-data> trỏ đến một tệp tài nguyên XML bên ngoài 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 các thiết bị USB mà bạn muốn lọc. Danh sách sau đây mô tả các thuộc tính của <usb-device>. Nhìn chung, hãy sử dụng mã nhà cung cấp và mã sản phẩm nếu bạn muốn lọc một thiết bị cụ thể và dùng các lớp, lớp con và giao thức nếu bạn muốn lọc tìm nhóm 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ó 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 khớp với mọi thiết bị USB, do đó, chỉ thực hiện 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 tên tệp 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ụ dưới đây.

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:

<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 sẽ đượ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 đã chỉ định phải đượ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>

Làm việc với thiết bị

Khi người dùng kết nối thiết bị USB với thiết bị chạy Android, hệ thống Android có thể xác định xem ứ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 hoạt động giao tiếp với thiết bị nếu muốn. Để làm được điều này, ứng dụng của bạn phải:

  1. Khám phá các thiết bị USB đã 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 thiết bị USB hoặc bằng cách liệt kê các thiết bị USB đã kết nối.
  2. Hãy 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 cấp quyền.
  3. Giao tiếp với thiết bị USB bằng cách đọc và ghi dữ liệu trên các điểm cuối giao diện thích hợp.

Khám phá thiết bị

Ứng dụng của bạn có thể khám phá các thiết bị USB 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 với một thiết bị hoặc bằng cách liệt kê các thiết bị USB đã kết nối. Việc sử dụng bộ lọc ý định sẽ hữu ích nếu bạn muốn ứng dụng của mình tự động phát hiện thiết bị mong muốn. Việc liệt kê các thiết bị USB đã kết nối sẽ hữu ích nếu bạn muốn xem danh sách tất cả các thiết bị đã kết nối hoặc nếu ứng dụng của bạn không lọc cho một ý định.

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

Để ứng dụng của bạn khám phá một thiết bị USB cụ thể, bạn có thể chỉ định bộ lọc ý định để lọc ý đị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 thiết bị USB, chẳng hạn như mã sản phẩm và mã nhà cung cấp. Khi người dùng kết nối một thiết bị khớp với bộ lọc của thiết bị, hệ thống sẽ hiện 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 cho biết 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 của mình, bạn có thể lấy UsbDevice đại diện cho thiết bị được đính kèm qua ý đị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 muốn kiểm tra tất cả các thiết bị USB hiện đang kết nối trong khi ứng dụng đang chạy, thì ứng dụng có thể liệt kê các thiết bị trên bus. Sử dụng phương thức getDeviceList() để tải bản đồ băm của tất cả các thiết bị USB đã kết nối. Bản đồ hàm 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 đối tượng lặp từ bản đồ băm và xử lý từng thiết bị 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
}

Yêu cầu cấp quyền để giao tiếp với một thiết bị

Trước khi giao tiếp với thiết bị USB, ứng dụng của bạn phải được người dùng cấp quyền.

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, thì ứng dụng sẽ tự động nhận quyền nếu người dùng cho phép ứng dụng của bạn xử lý ý định. Nếu chưa, bạn phải yêu cầu cấp quyền rõ ràng trong ứng dụ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, chẳng hạn như khi ứng dụng của bạn 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 thiết bị. Bạn phải kiểm tra quyền truy cập vào thiết bị trước khi cố gắng kết nối 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ị.

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

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

Để 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 boolean đại diện cho câu trả lời. Hãy kiểm tra dữ liệu bổ sung này để biết giá trị true trước khi kết nối với thiết bị.

Kết nối với một thiết bị

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ả hoạt động truyền dữ liệu trên đó, để bạn không chặn luồng giao diện người dùng. Để thiết lập hoạt động giao tiếp với thiết bị đúng cách, bạn cần lấy UsbInterfaceUsbEndpoint thích hợp của thiết bị mà bạn muốn giao tiếp trên đó, đồng thời 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 nên:

  • Hãy 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ị hay không.
  • Khi bạn chắc chắn muốn giao tiếp với thiết bị, hãy tìm UsbInterface thích hợp mà bạn muốn sử dụng để giao tiếp với UsbEndpoint thích hợp của giao diện đó. Các giao diện có thể có một hoặc nhiều điểm cuối và thường sẽ có một điểm cuối đầu vào và đầu ra để giao tiếp hai chiều.
  • Khi bạn tìm thấy đúng điểm cuối, hãy mở một UsbDeviceConnection trên điểm cuối đó.
  • Cung cấp dữ liệu mà 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 để tránh 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 Quy trình và luồng.

Đoạn mã sau đây là một cách đơn giản để chuyển dữ liệu đồng bộ. Mã của bạn phải có nhiều logic hơn để tìm chính xác giao diện và điểm cuối cần giao tiếp, đồng thời thực hiện mọi hoạt động 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 đối với một yêu cầu không đồng bộ, sau đó đợi kết quả bằng requestWait().

Chấm dứt kết nối với một thiết bị

Khi bạn đã giao tiếp 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 cách gọi releaseInterface()close(). Để theo dõi các sự kiện đã tách ra, hãy tạo một broadcast receiver như bên dưới:

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 tệp kê khai cho phép ứng dụng của bạn chỉ xử lý các sự kiện đã tách ra khi ứng dụng đang chạy. Bằng cách này, các sự kiện được tách ra chỉ được gửi đến ứng dụng hiện đang chạy chứ không được truyền đến tất cả các ứng dụng.