C
Android
SELinux
JNI
USB

Android 5.0以降におけるnative CでのUSBデバイスプログラミング

Android Lolipop以降でUSBデバイスをCライブラリで扱いたい

Lolipop以降、Android端末はSELinuxがベースとなっているため、Permission関係の扱いがちょっと難しい。この記事では、USBホストとしてのAndroid端末において、USBデバイスを扱うC言語によるライブラリを使用する際/書く際の注意点を述べている。

なぜC言語?

伝統的に、デバイスドライバに近い層はCで書かれたライブラリが多い。AndroidもLinuxの派生形であるため、USBドライバについてもusbfsを扱う点で同じであり、基本的にLinuxのUSBデバイスに関するライブラリを利用できる。Android独特のUSBライブラリを用いてわざわざ車輪の再発明をすることはない。

Cでの制約

AndroidでUSBデバイスを扱うCライブラリは例えばlibusbなどがあるのだが、libusbのREADMEを見ると、次のような書きっぷりである。

The default system configuration on most Android device will not allow
access to USB devices. There are several options for changing this.

If you have control of the system image then you can modify the
ueventd.rc used in the image to change the permissions on
/dev/bus/usb//. If using this approach then it is advisable to
create a new Android permission to protect access to these files.
It is not advisable to give all applications read and write permissions
to these files.

For rooted devices the code using libusb could be executed as root
using the "su" command. An alternative would be to use the "su" command
to change the permissions on the appropriate /dev/bus/usb/ files.

Users have reported success in using android.hardware.usb.UsbManager
to request permission to use the UsbDevice and then opening the
device. The difficulties in this method is that there is no guarantee
that it will continue to work in the future Android versions, it
requires invoking Java APIs and running code to match each
android.hardware.usb.UsbDevice to a libusb_device.

なるほど、SELinuxによって/dev/bus/usb/配下のアクセスが制限されてるから、自分でアクセス許可を与えるなどして頑張ってね、というアプローチが書いてある。SELinuxとしてのAndroidとして見ると正しい記述であるが、root権限を取らずに開発しなければならない大多数のAndroidアプリ開発者にとってはピントが外れている。もちろん、この情報だけでは解決は難しい。

libusbなどでの手順

通常、Cのライブラリを用いる場合は、以下のようなプログラムになる。
1. ポートをオープンしてファイルディスクプリタを得る。
2. ファイルディスクプリタを用いて当該デバイスのUSB Descriptorを読み込む。(参考:SyncHack-USB/Descriptor)
3. USBインタフェースやUSBエンドポイントをUSB Descriptorから識別してデバイスと通信する。

usb.c
const char* port_name = "/dev/bus/usb/000/001"; //USBデバイス(接続の度に変わるので実際はサーチなどする)
int fd; //ファイルディスクプリタ
int rc;
int n_read;
byte[] desc; //USB Descriptorの格納先

fd = open(port_name, 0_RDWR); //ポートのオープン
...
rc = read(fd, desc, n_read); //USB Descriptorの最初の読み込み
...

このコードの問題点 - パーミッション

このコードはAndroid端末でも、root権限で動かしたり、デバイスをカスタマイズしたりできる場合には問題なく動作させることができる。しかし、非rootでの端末ではポートのオープンでパーミッションエラーとなり、動作しない。
ではどうするか?

回避策

Android SDK上でUSBデバイスにアクセスする手順を踏むと、きちんと当該デバイスへのパーミッションを取得できる。普通にAndroid SDK上でUSBデバイスへのアクセス権限を取得する方法は、Developer GuideのUSB Hostに書いてある手順をやれば、特に問題なく取得できる。Cライブラリを使わないのであれば、UsbManagerUsbDeviceUsbDeviceConnectionなどを通じて地道に組んでいけばいい。
しかし、例えばUSBManager#openDevice()を実施した後に上記のusb.cを実行してもエラーになる。これは、実はopenDevice()を実行した時点でSDK内部で払い出されているファイルディスクプリタがあり、そこ経由のアクセスのみ許可されているためである。新たにオープンすることは許可されておらず、当該ファイルディスクプリタのみを利用する必要がある。ファイルディスクプリタはUsbDeviceConnection#getFileDescpritor()で取得できる。
この「ファイルディスクプリタの再利用」について対応されたlibusbのブランチもある。以下のように、op_open2()というファンクションの引数に、SDKの世界から取得したファイルディスクプリタを与えるようにできている。

static int op_open2(struct libusb_device_handle *handle, int fd)

さらなる問題点 - USB Descpritor

さて、ファイルディスクプリタを再利用したとしてももう1つ問題が発生する。それは、USB Descpritorの読み込みでI/Oエラーになることである。実は、SDKはUSBManager#openDevice()の際に同時にUSB Descpritorを読み込んでしまうため、ファイルディスクプリタを再利用すると既に読み込み済みのために自分のコードでは読めないのである。
では読み込まれたUSB Descriptorはどこに行ったのかというと、USBDeviceConnection#getRawDescpritors()で取り出せるようになっている。

完成形

ということで、次のような手順でUSBデバイスのCライブラリを活用すれば、パーミッションの問題もUSB Descpritorの問題も回避して、既存のライブラリを活用できることが確認できた。

Usb.java
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();  
for(UsbDevice device : deviceList.values()) {
  if(device.getVenforId() == 0xZZ && device.getProductId() == 0xYY) { //プロダクト指定で制御する場合
    UsbDeviceConnection connection = manager.openDevice(device); //この時点でopen(), read()が実行されている。
    int fd = connection.getFileDescptitor(); //SDKによって許可された、通信可能なファイルディスクプリタ
    byte[] descpritors = connection.getRawDescpritors(); //Device, Configuration, Interface, EndpointなどすべてのDescpritorが含まれる
    callJNIUsbLib(fd, descpritors); //JNI側にはこの2つの情報を引き渡せば処理できる。
  }
}

Cライブラリ側では、「ファイルのオープンの代わりにファイルディスクプリタを使う」「USB Descpritorの取得をせずに渡されたものを使う」という実装とすれば良い。