📜 ⬆️ ⬇️

Access to HID devices from a Qt program for Android

Introduction


With the release of Qt 5, there was a convenient opportunity to expand the list of platforms supported by the program to mobile OS, in particular on Android.
The process of porting the program from the desktop version of Qt to the mobile has been reduced to a banal recompilation. The interface and the logic started right away, except for the part without which, in fact, the program is useless: exchange with an HID device.

First difficulties


From the very beginning, the HIDAPI library is used to communicate with the device. It is multiplatform and easy to use.
The first difficulty encountered was that there is no hidraw for Android, which was used to access devices for desktop Linux. To work around, I had to switch to using the libusb library and its interface to it in HIDAPI.
The first launch showed that the enumeration of devices works, but it is impossible to open the device file, due to the lack of rights of the application.
In the README libusb / android file there is 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 root device confirmed the operability of the chosen scheme, but also led to the understanding that this is not quite the right way, because It works on a very small circle of devices. Therefore, I had to climb into the jungle API Android.

Reading the documentation showed that there are two ways to access the device: using an intent filter and a simple listing of devices.
The first way to make it 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 accessing USB.
')
In order to get by with a minimum of alterations in the existing code, it was decided to bring 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. The rest of the work on enumerating devices and exchanging data is done by the HIDAPI-libusb bundle.

Implementation.


The first thing that had to be done was to request permission from the user to open the device.
Again, adapting to the existing algorithm, we got the following: when a device is found, the path to its file is passed to a function in the Activity class of the program via the JNI interface:

int HidTransport::openAndroidDevice(QString devPath) { QAndroidJniObject dP = QAndroidJniObject::fromString(devPath); jint dFD = QAndroidJniObject::callStaticMethod<jint>("org/HidManager/HidDevice", "tryOpenDevice", "(Ljava/lang/String;)I", dP.object<jstring>()); return dFD; } 


Here I want to make some digression: for some reason, the Qt interface to the JNI can only be called static class methods. Therefore, we are actually creating singelton. Below 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<String, Integer> deviceCache = new HashMap<String, Integer>(); 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<String, UsbDevice> deviceList = m_usbManager.getDeviceList(); Iterator<UsbDevice> 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 storage for the state of the device opening process. This option was chosen because the native code algorithm tries to open every found but not yet open device with a certain frequency.

Next, the Android permissions mechanism comes in and this function is used to get 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 can be seen from it, if permission is obtained, then the function of 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 saves the resulting file descriptor in deviceCache.

After going through all these steps, we get the file handle of the open device. But another problem appears: HIDAPI and libusb cannot take descriptors as a pointer to a device.

Fortunately, this problem was solved simply. There is a libusb fork that takes an open device file descriptor as an argument.

Conclusion


So, quite simply, you can access the USB from the native code.

Unfortunately, this approach does not work on all devices. Many manufacturers do not include permission
android.hardware.usb.host in its 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. In this case, the USB device file is created by the kernel, but even UsbManager does not see them.

In this case, it is possible to bypass this restriction on devices with a working root, changing access rights to the file, since libusb is able to see connected devices. But for now this is only a theory.

Source: https://habr.com/ru/post/238241/


All Articles