Access to HID devices from Qt program for Android
Introduction
With the release of Qt 5, a convenient opportunity has appeared to expand the list of platforms supported by the program to mobile OS, in particular, to Android.
The process of porting the program from the desktop version of Qt to mobile has been reduced to a banal recompilation. The interface and logic started immediately, with the exception of the part without which, in fact, the program is useless: exchanging with a HID device.
First difficulties
From the very beginning, the HIDAPI library has been used to communicate with the device. It is multi-platform and easy to use.
The first difficulty that I had to deal with was that under Android there is no hidraw that was used to access devices under desktop Linux. For a workaround, I had to switch to using the libusb library and its interface to it in HIDAPI.
The first launch showed that the device enumeration works, but you cannot open the device file, due to the lack of rights for the application.
The README libusb / android file contains a description of possible ways to circumvent this problem: either change the rights of the device file, or use the android.hardware.usb.UsbDevice interface to open devices.
A simple change of the file access mask to 777 on the rooted device confirmed the operability of the selected scheme, but also led to the understanding that this is not quite the right path, because it works on a very small circle of devices. Therefore, I had to climb into the jungle of the Android API.
Reading the documentation showed that there are two ways to access the device: using the intent filter and a simple enumeration of devices.
The first way to get work failed, no events occurred when the device appeared in the system. Actually this path is also a dead end, because implies that the program must be started before the device is connected, and this means that we should take full advantage of the provided API for access to USB.
In order to avoid a minimum of alterations in the existing code, it was decided to transfer to the Java part only the code associated with the request for permission to access the device from the user and the actual opening of the device. All other work on enumerating devices and exchanging data is performed by the HIDAPI-libusb bundle.
Implementation.
The first thing I had to do was request permission from the user to open the device.
Again, adjusting to the existing algorithm, the following happened: when the device is found, the path to its file is transferred to the function in the Activity class of the program through the JNI interface:
int HidTransport::openAndroidDevice(QString devPath)
{
QAndroidJniObject dP = QAndroidJniObject::fromString(devPath);
jint dFD = QAndroidJniObject::callStaticMethod("org/HidManager/HidDevice", "tryOpenDevice", "(Ljava/lang/String;)I", dP.object());
return dFD;
}
I would like to make a digression here: for some reason, the Qt interface to JNI can only call static methods of the class. Therefore, we are creating a virtually singleton. The following are the constructor and onCreate of the Activity class:
private static HidDevice m_instance;
private static UsbManager m_usbManager;
private static PendingIntent mPermissionIntent;
private static HashMap deviceCache = new HashMap();
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
public HidDevice()
{
m_instance = this;
}
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (m_usbManager==null) {
m_usbManager = (UsbManager) m_instance.getSystemService(Context.USB_SERVICE);
}
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(mUsbReceiver, filter);
}
@Override
public void onDestroy()
{
super.onDestroy();
}
The tryOpenDevice function, called from native code, looks like this:
public static int tryOpenDevice(String devPath)
{
if (deviceCache.containsKey(devPath)) {
int fd = deviceCache.get(devPath);
return fd;
}
deviceCache.put(devPath, -1);
HashMap deviceList = m_usbManager.getDeviceList();
Iterator deviceIterator = deviceList.values().iterator();
while(deviceIterator.hasNext()) {
UsbDevice device = deviceIterator.next();
if (devPath.compareTo(device.getDeviceName())==0) {
m_usbManager.requestPermission(device, mPermissionIntent);
break;
}
}
}
deviceCache acts as an intermediate store for the state of the device open process. This option was chosen because the native code algorithm tries to open every device found but not yet open with some frequency.
Next, the Android permissions mechanism comes into operation and this function serves to obtain the results:
private final BroadcastReceiver mUsbReceiver = 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){
m_instance.openDevice(device);
}
}
}
}
}
};
As you can see from it, if permission is obtained, the function for opening the device is called:
public void openDevice(UsbDevice device)
{
try {
if (!res) return;
UsbDeviceConnection devConn = m_usbManager.openDevice(device);
Integer fd = devConn.getFileDescriptor();
deviceCache.put(device.getDeviceName(), fd);
}
catch (InterruptedException e) {
return;
}
}
Which stores the resulting file descriptor in deviceCache.
After going through all these steps, we get the file descriptor of the open device. But here another problem appears: HIDAPI and libusb do not know how to accept descriptors as a pointer to a device.
Fortunately, this problem was solved simply. There is a fork of libusb that takes an open device file descriptor as an argument.
Conclusion
So, quite simply, you can access USB from the native code.
Unfortunately, this approach does not work on all devices. Many manufacturers do not include the resolution of
android.hardware.usb.host in their firmware, which leads to a situation where physically the tablet can work as a host for flash drives or mice, but other devices do not work. At the same time, a USB device file is created by the kernel, but even UsbManager does not see them.
In this case, this limitation can be circumvented on devices with a working root by changing file permissions, as libusb is able to see connected devices. But for now, this is just a theory.