From 343c41ee2e51784663abea3396e0bdb2447f62c0 Mon Sep 17 00:00:00 2001 From: Henrik Grimler Date: Sat, 27 Nov 2021 14:11:01 +0100 Subject: [PATCH 1/5] enhance(UsbAPI): print more information when listing devices termux-usb -l now returns something like: [ { "device_name": "/dev/bus/usb/001/004", "device_id": 1004, "vendor_id": "0x0a12", "product_id": "0x0001", "device_class": "224 - Wireless controller device", "device_sub_class": 1, "manufacturer_name": null, "device_protocol": 1, "product_name": "CSR8510 A10", "serial_number": null, "configurations": 1, "descriptor_type": 0, "access_granted": false }, { "device_name": "/dev/bus/usb/001/003", "device_id": 1003, "vendor_id": "0x0bda", "product_id": "0x8153", "device_class": "0 - Usb class is determined on a per-interface basis", "device_sub_class": 0, "manufacturer_name": "Realtek", "device_protocol": 0, "product_name": "USB 10/100/1000 LAN", "serial_number": "000000100000", "configurations": 2, "descriptor_type": 0, "access_granted": false } ] --- .../main/java/com/termux/api/apis/UsbAPI.java | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/UsbAPI.java b/app/src/main/java/com/termux/api/apis/UsbAPI.java index 0d9257731..63916185a 100644 --- a/app/src/main/java/com/termux/api/apis/UsbAPI.java +++ b/app/src/main/java/com/termux/api/apis/UsbAPI.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; @@ -127,12 +128,65 @@ protected void listDevices(JsonWriter out) throws IOException { UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); HashMap deviceList = usbManager.getDeviceList(); out.beginArray(); - for (String deviceName : deviceList.keySet()) { - out.value(deviceName); + for (UsbDevice device : deviceList.values()) { + out.beginObject(); + out.name("device_name").value(device.getDeviceName()); + out.name("device_id").value(device.getDeviceId()); + out.name("vendor_id").value(String.format("0x%04x", device.getVendorId())); + out.name("product_id").value(String.format("0x%04x", device.getProductId())); + out.name("device_class").value(device.getDeviceClass()+" - "+translateDeviceClass(device.getDeviceClass())); + out.name("device_subclass").value(device.getDeviceSubclass()); + out.name("manufacturer_name").value(device.getManufacturerName()); + out.name("device_protocol").value(device.getDeviceProtocol()); + out.name("product_name").value(device.getProductName()); + out.name("serial_number").value(device.getSerialNumber()); + out.name("configurations").value(device.getConfigurationCount()); + out.name("descriptor_type").value(device.describeContents()); + out.name("access_granted").value(usbManager.hasPermission(device)); + out.endObject(); } out.endArray(); } + private static String translateDeviceClass(int usbClass){ + switch(usbClass){ + case UsbConstants.USB_CLASS_APP_SPEC: + return "App specific USB class"; + case UsbConstants.USB_CLASS_AUDIO: + return "Audio device"; + case UsbConstants.USB_CLASS_CDC_DATA: + return "CDC device (communications device class)"; + case UsbConstants.USB_CLASS_COMM: + return "Communication device"; + case UsbConstants.USB_CLASS_CONTENT_SEC: + return "Content security device"; + case UsbConstants.USB_CLASS_CSCID: + return "Content smart card device"; + case UsbConstants.USB_CLASS_HID: + return "Human interface device (for example a keyboard)"; + case UsbConstants.USB_CLASS_HUB: + return "USB hub"; + case UsbConstants.USB_CLASS_MASS_STORAGE: + return "Mass storage device"; + case UsbConstants.USB_CLASS_MISC: + return "Wireless miscellaneous devices"; + case UsbConstants.USB_CLASS_PER_INTERFACE: + return "Usb class is determined on a per-interface basis"; + case UsbConstants.USB_CLASS_PHYSICA: + return "Physical device"; + case UsbConstants.USB_CLASS_PRINTER: + return "Printer"; + case UsbConstants.USB_CLASS_STILL_IMAGE: + return "Still image devices (digital cameras)"; + case UsbConstants.USB_CLASS_VENDOR_SPEC: + return "Vendor specific USB class"; + case UsbConstants.USB_CLASS_VIDEO: + return "Video device"; + case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER: + return "Wireless controller device"; + default: return "Unknown USB class!"; + } + } protected void runPermissionAction(Intent intent) { From 71b965655986d2d2f936ce68429f5cba1c11e119 Mon Sep 17 00:00:00 2001 From: Henrik Grimler Date: Sun, 28 Nov 2021 10:40:36 +0100 Subject: [PATCH 2/5] enhance(UsbAPI): add option to open device based on vendor and productId So we can either pass `--es device /dev/bus/usb/001/003`, or `--es vendorId 0x0403 --es productId 0x6001`. `--es device` is used if all three are passed. --- .../main/java/com/termux/api/apis/UsbAPI.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/UsbAPI.java b/app/src/main/java/com/termux/api/apis/UsbAPI.java index 63916185a..b62ba18fb 100644 --- a/app/src/main/java/com/termux/api/apis/UsbAPI.java +++ b/app/src/main/java/com/termux/api/apis/UsbAPI.java @@ -218,10 +218,21 @@ protected void runPermissionAction(Intent intent) { protected void runOpenAction(Intent intent) { mThreadPoolExecutor.submit(() -> { String deviceName = intent.getStringExtra("device"); + String vendorId = intent.getStringExtra("vendorId"); + String productId = intent.getStringExtra("productId"); + if (deviceName == null && (vendorId == null || productId == null)) { + Logger.logError(LOG_TAG, "Missing usb device info in open()"); + } - Logger.logVerbose(LOG_TAG,"Running 'open' action for device \"" + deviceName + "\""); + UsbDevice device; + if (deviceName != null) { + Logger.logVerbose(LOG_TAG,"Running 'open' action for device \"" + deviceName + "\""); + device = getDevice(intent, deviceName); + } else { + Logger.logVerbose(LOG_TAG,"Running 'open' action for vendor Id \"" + vendorId + "\" and product Id \"" + productId + "\""); + device = getDevice(intent, vendorId, productId); + } - UsbDevice device = getDevice(intent, deviceName); if (device == null) return; int status = checkAndRequestUsbDevicePermission(intent, device); @@ -279,6 +290,22 @@ protected UsbDevice getDevice(Intent intent, String deviceName) { return device; } + protected UsbDevice getDevice(Intent intent, String vendorId, String productId) { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + HashMap deviceList = usbManager.getDeviceList(); + for (UsbDevice dev : deviceList.values()) { + if (String.format("0x%04x", dev.getVendorId()).equalsIgnoreCase(vendorId) && + String.format("0x%04x", dev.getProductId()).equalsIgnoreCase(productId)) { + return dev; + } + + } + Logger.logVerbose(LOG_TAG, "Failed to find device with vendor Id \"" + vendorId + "\" and product Id\"" + productId + "\""); + ResultReturner.returnData(this, intent, out -> out.append("No such device.\n")); + + return null; + } protected boolean checkUsbDevicePermission(@NonNull UsbDevice device) { From 5f63744f991af2ad32ec1fad9ba3cebc5f8e89e8 Mon Sep 17 00:00:00 2001 From: Henrik Grimler Date: Mon, 3 Jan 2022 09:48:07 +0100 Subject: [PATCH 3/5] fix(UsbAPI): strip null chars from manufacturer and product name And ensure they are not empty. Apparently some devices have fixed length fields, with extra nulls at the end. This makes parsing the output slightly more annoying in termux so strip them on the java side. Before termux-usb -l could return something like: [ { "device_name": "/dev/bus/usb/002/006", "device_id": 2006, "vendor_id": "0x316d", "product_id": "0x4c4b", "device_class": "0 - Usb class is determined on a per-interface basis", "device_subclass": 0, "manufacturer_name": "Purism, SPC\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "device_protocol": 0, "product_name": "Librem Key\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "serial_number": "00000000000000000000D6G9", "configurations": 1, "descriptor_type": 0, "access_granted": false } ] and now we remove all \u0000. --- app/src/main/java/com/termux/api/apis/UsbAPI.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/UsbAPI.java b/app/src/main/java/com/termux/api/apis/UsbAPI.java index b62ba18fb..6a9efbd6b 100644 --- a/app/src/main/java/com/termux/api/apis/UsbAPI.java +++ b/app/src/main/java/com/termux/api/apis/UsbAPI.java @@ -136,9 +136,17 @@ protected void listDevices(JsonWriter out) throws IOException { out.name("product_id").value(String.format("0x%04x", device.getProductId())); out.name("device_class").value(device.getDeviceClass()+" - "+translateDeviceClass(device.getDeviceClass())); out.name("device_subclass").value(device.getDeviceSubclass()); - out.name("manufacturer_name").value(device.getManufacturerName()); + if (device.getManufacturerName() != null) { + out.name("manufacturer_name").value(device.getManufacturerName().replace("\u0000", "")); + } else { + out.name("manufacturer_name").value(device.getManufacturerName()); + } out.name("device_protocol").value(device.getDeviceProtocol()); - out.name("product_name").value(device.getProductName()); + if (device.getProductName() != null) { + out.name("product name").value(device.getProductName().replace("\u0000", "")); + } else { + out.name("product name").value(device.getProductName()); + } out.name("serial_number").value(device.getSerialNumber()); out.name("configurations").value(device.getConfigurationCount()); out.name("descriptor_type").value(device.describeContents()); From 03fca3097105c148cb763dd25568d78ddf319580 Mon Sep 17 00:00:00 2001 From: Henrik Grimler Date: Thu, 6 Jan 2022 14:06:00 +0100 Subject: [PATCH 4/5] enhance(UsbAPI): add getDevices function which gives serialised data The information received from android's usbmanager is returned to termux's userspace as a serialised protobuf message, giving something that is quite easy to parse and extend. The returned info is loosely modelled after libusb's internal struct libusb_device, and userspace programs can parse the info as a replacement for libusb_get_device_list and libusb_get_device_descriptor. --- app/build.gradle | 36 + .../main/java/com/termux/api/apis/UsbAPI.java | 832 ++++++++++-------- app/src/main/proto/UsbAPI.proto | 31 + build.gradle | 1 + 4 files changed, 522 insertions(+), 378 deletions(-) create mode 100644 app/src/main/proto/UsbAPI.proto diff --git a/app/build.gradle b/app/build.gradle index a50127dcc..1aabda509 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'com.google.protobuf' android { namespace "com.termux.api" @@ -63,6 +64,40 @@ android { exclude "lib/*/libtermux.so" exclude "lib/*/liblocal-socket.so" } + sourceSets { + main { + proto { + srcDir 'src/main/proto' + } + java { + srcDir 'src/main/java' + } + } + } +} + +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:4.29.3' + } + + plugins { + javalite { + artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' + } + } + + // this is a task which will generate classes for our proto files + generateProtoTasks { + all().each { task -> + task.builtins { + remove java + } + task.plugins { + javalite {} + } + } + } } dependencies { @@ -78,6 +113,7 @@ dependencies { // If updates are done, republish there and sync project with gradle files here // https://github.com/termux/termux-app/wiki/Termux-Libraries //implementation "com.termux:termux-shared:0.118.0" + implementation 'com.google.protobuf:protobuf-lite:3.0.1' implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" } diff --git a/app/src/main/java/com/termux/api/apis/UsbAPI.java b/app/src/main/java/com/termux/api/apis/UsbAPI.java index 6a9efbd6b..3100f97b5 100644 --- a/app/src/main/java/com/termux/api/apis/UsbAPI.java +++ b/app/src/main/java/com/termux/api/apis/UsbAPI.java @@ -20,7 +20,12 @@ import com.termux.shared.logger.Logger; import com.termux.shared.termux.TermuxConstants; +import com.termux.api.UsbAPIProto.termuxUsb; +import com.termux.api.UsbAPIProto.termuxUsbDevice; +import com.termux.api.UsbAPIProto.termuxUsbDeviceDescriptor; + import java.io.IOException; +import java.io.OutputStream; import java.io.PrintWriter; import java.util.HashMap; import java.util.concurrent.CountDownLatch; @@ -40,388 +45,459 @@ public class UsbAPI { protected static final String ACTION_USB_PERMISSION = TermuxConstants.TERMUX_API_PACKAGE_NAME + ".USB_PERMISSION"; public static void onReceive(final Context context, final Intent intent) { - Logger.logDebug(LOG_TAG, "onReceive"); - - Intent serviceIntent = new Intent(context, UsbService.class); - serviceIntent.setAction(intent.getAction()); - Bundle extras = intent.getExtras(); - if (extras != null) - serviceIntent.putExtras(extras); - context.startService(serviceIntent); + Logger.logDebug(LOG_TAG, "onReceive"); + + Intent serviceIntent = new Intent(context, UsbService.class); + serviceIntent.setAction(intent.getAction()); + Bundle extras = intent.getExtras(); + if (extras != null) + serviceIntent.putExtras(extras); + context.startService(serviceIntent); } public static class UsbService extends Service { - protected static final String LOG_TAG = "UsbService"; - - private final ThreadPoolExecutor mThreadPoolExecutor; - - public UsbService() { - super(); - mThreadPoolExecutor = new ThreadPoolExecutor(1, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>()); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - public void onCreate() { - Logger.logDebug(LOG_TAG, "onCreate"); - - super.onCreate(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Logger.logDebug(LOG_TAG, "onStartCommand"); - - String action = intent.getAction(); - if (action == null) { - Logger.logError(LOG_TAG, "No action passed"); - ResultReturner.returnData(this, intent, out -> out.append("Missing action\n")); - } - - if (action != null) { - switch (action) { - case "list": - runListAction(intent); - break; - case "permission": - runPermissionAction(intent); - break; - case "open": - runOpenAction(intent); - break; - default: - Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); - ResultReturner.returnData(this, intent, out -> out.append("Invalid action: \"" + action + "\"\n")); - } - } - - return Service.START_NOT_STICKY; - } - - @Override - public void onDestroy() { - Logger.logDebug(LOG_TAG, "onDestroy"); - - super.onDestroy(); - } - - - - protected void runListAction(Intent intent) { - Logger.logVerbose(LOG_TAG,"Running 'list' usb devices action"); - - ResultReturner.returnData(this, intent, new ResultReturner.ResultJsonWriter() { - @Override - public void writeJson(JsonWriter out) throws Exception { - listDevices(out); - } - }); - } - - protected void listDevices(JsonWriter out) throws IOException { - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - HashMap deviceList = usbManager.getDeviceList(); - out.beginArray(); - for (UsbDevice device : deviceList.values()) { - out.beginObject(); - out.name("device_name").value(device.getDeviceName()); - out.name("device_id").value(device.getDeviceId()); - out.name("vendor_id").value(String.format("0x%04x", device.getVendorId())); - out.name("product_id").value(String.format("0x%04x", device.getProductId())); - out.name("device_class").value(device.getDeviceClass()+" - "+translateDeviceClass(device.getDeviceClass())); - out.name("device_subclass").value(device.getDeviceSubclass()); - if (device.getManufacturerName() != null) { - out.name("manufacturer_name").value(device.getManufacturerName().replace("\u0000", "")); - } else { - out.name("manufacturer_name").value(device.getManufacturerName()); - } - out.name("device_protocol").value(device.getDeviceProtocol()); - if (device.getProductName() != null) { - out.name("product name").value(device.getProductName().replace("\u0000", "")); - } else { - out.name("product name").value(device.getProductName()); - } - out.name("serial_number").value(device.getSerialNumber()); - out.name("configurations").value(device.getConfigurationCount()); - out.name("descriptor_type").value(device.describeContents()); - out.name("access_granted").value(usbManager.hasPermission(device)); - out.endObject(); - } - out.endArray(); - } - - private static String translateDeviceClass(int usbClass){ - switch(usbClass){ - case UsbConstants.USB_CLASS_APP_SPEC: - return "App specific USB class"; - case UsbConstants.USB_CLASS_AUDIO: - return "Audio device"; - case UsbConstants.USB_CLASS_CDC_DATA: - return "CDC device (communications device class)"; - case UsbConstants.USB_CLASS_COMM: - return "Communication device"; - case UsbConstants.USB_CLASS_CONTENT_SEC: - return "Content security device"; - case UsbConstants.USB_CLASS_CSCID: - return "Content smart card device"; - case UsbConstants.USB_CLASS_HID: - return "Human interface device (for example a keyboard)"; - case UsbConstants.USB_CLASS_HUB: - return "USB hub"; - case UsbConstants.USB_CLASS_MASS_STORAGE: - return "Mass storage device"; - case UsbConstants.USB_CLASS_MISC: - return "Wireless miscellaneous devices"; - case UsbConstants.USB_CLASS_PER_INTERFACE: - return "Usb class is determined on a per-interface basis"; - case UsbConstants.USB_CLASS_PHYSICA: - return "Physical device"; - case UsbConstants.USB_CLASS_PRINTER: - return "Printer"; - case UsbConstants.USB_CLASS_STILL_IMAGE: - return "Still image devices (digital cameras)"; - case UsbConstants.USB_CLASS_VENDOR_SPEC: - return "Vendor specific USB class"; - case UsbConstants.USB_CLASS_VIDEO: - return "Video device"; - case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER: - return "Wireless controller device"; - default: return "Unknown USB class!"; - } - } - - - protected void runPermissionAction(Intent intent) { - mThreadPoolExecutor.submit(() -> { - String deviceName = intent.getStringExtra("device"); - - Logger.logVerbose(LOG_TAG,"Running 'permission' action for device \"" + deviceName + "\""); - - UsbDevice device = getDevice(intent, deviceName); - if (device == null) return; - - int status = checkAndRequestUsbDevicePermission(intent, device); - ResultReturner.returnData(this, intent, out -> { - if (status == 0) { - Logger.logVerbose(LOG_TAG, "Permission granted for device \"" + device.getDeviceName() + "\""); - out.append("Permission granted.\n" ); - } else if (status == 1) { - Logger.logVerbose(LOG_TAG, "Permission denied for device \"" + device.getDeviceName() + "\""); - out.append("Permission denied.\n" ); - } else if (status == -1) { - out.append("Permission request timeout.\n" ); - } - }); - }); - } - - - - protected void runOpenAction(Intent intent) { - mThreadPoolExecutor.submit(() -> { - String deviceName = intent.getStringExtra("device"); - String vendorId = intent.getStringExtra("vendorId"); - String productId = intent.getStringExtra("productId"); - if (deviceName == null && (vendorId == null || productId == null)) { - Logger.logError(LOG_TAG, "Missing usb device info in open()"); - } - - UsbDevice device; - if (deviceName != null) { - Logger.logVerbose(LOG_TAG,"Running 'open' action for device \"" + deviceName + "\""); - device = getDevice(intent, deviceName); - } else { - Logger.logVerbose(LOG_TAG,"Running 'open' action for vendor Id \"" + vendorId + "\" and product Id \"" + productId + "\""); - device = getDevice(intent, vendorId, productId); - } - - if (device == null) return; - - int status = checkAndRequestUsbDevicePermission(intent, device); - ResultReturner.returnData(this, intent, new ResultReturner.WithAncillaryFd() { - @Override - public void writeResult(PrintWriter out) { - if (status == 0) { - int fd = open(device); - if (fd < 0) { - Logger.logVerbose(LOG_TAG, "Failed to open device \"" + device.getDeviceName() + "\": " + fd); - out.append("Open device failed.\n"); - } else { - Logger.logVerbose(LOG_TAG, "Open device \"" + device.getDeviceName() + "\" successful"); - this.sendFd(out, fd); - } - } else if (status == 1) { - Logger.logVerbose(LOG_TAG, "Permission denied to open device \"" + device.getDeviceName() + "\""); - out.append("Permission denied.\n" ); - } else if (status == -1) { - out.append("Permission request timeout.\n" ); - } - } - }); - }); - } - - protected int open(@NonNull UsbDevice device) { - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - - UsbDeviceConnection connection = usbManager.openDevice(device); - if (connection == null) return -2; - - int fd = connection.getFileDescriptor(); - if (fd == -1) { - connection.close(); - return -1; - } - - openDevices.put(fd, connection); - return fd; - } - - - - protected UsbDevice getDevice(Intent intent, String deviceName) { - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - - HashMap deviceList = usbManager.getDeviceList(); - UsbDevice device = deviceList.get(deviceName); - if (device == null) { - Logger.logVerbose(LOG_TAG, "Failed to find device \"" + deviceName + "\""); - ResultReturner.returnData(this, intent, out -> out.append("No such device.\n")); - } - - return device; - } - - protected UsbDevice getDevice(Intent intent, String vendorId, String productId) { - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - - HashMap deviceList = usbManager.getDeviceList(); - for (UsbDevice dev : deviceList.values()) { - if (String.format("0x%04x", dev.getVendorId()).equalsIgnoreCase(vendorId) && - String.format("0x%04x", dev.getProductId()).equalsIgnoreCase(productId)) { - return dev; - } - - } - Logger.logVerbose(LOG_TAG, "Failed to find device with vendor Id \"" + vendorId + "\" and product Id\"" + productId + "\""); - ResultReturner.returnData(this, intent, out -> out.append("No such device.\n")); - - return null; - } - - - protected boolean checkUsbDevicePermission(@NonNull UsbDevice device) { - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - return usbManager.hasPermission(device); - } - - protected int checkAndRequestUsbDevicePermission(Intent intent, @NonNull UsbDevice device) { - boolean checkResult = checkUsbDevicePermission(device); - Logger.logVerbose(LOG_TAG, "Permission check result for device \"" + device.getDeviceName() + "\": " + checkResult); - if (checkResult) { - return 0; - } - - if(!intent.getBooleanExtra("request", false)) { - return 1; - } - - Logger.logVerbose(LOG_TAG, "Requesting permission for device \"" + device.getDeviceName() + "\""); - - CountDownLatch latch = new CountDownLatch(1); - AtomicReference result = new AtomicReference<>(); - - BroadcastReceiver usbReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent usbIntent) { - if (ACTION_USB_PERMISSION.equals(usbIntent.getAction())) { - boolean requestResult = usbIntent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); - Logger.logVerbose(LOG_TAG, "Permission request result for device \"" + device.getDeviceName() + "\": " + requestResult); - result.set(requestResult); - } - context.unregisterReceiver(this); - latch.countDown(); - } - }; - - UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); - - Intent usbIntent = new Intent(ACTION_USB_PERMISSION); - // Use explicit intent, otherwise permission request intent will be blocked if intent is - // mutable and app uses `targetSdkVersion` `>= 34`, or following exception will be logged - // to logcat if app uses `targetSdkVersion` `< 34`. - // > `android.app.StackTrace: New mutable implicit PendingIntent: pkg=com.termux.api, - // > action=com.termux.api.USB_PERMISSION, featureId=null. This will be blocked once the - // > app targets U+ for security reasons.` - // - https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents - usbIntent.setPackage(getPackageName()); - - // Use mutable intent, otherwise permission request intent will be blocked if app - // uses `targetSdkVersion` `>= 31` and following exception may be logged to logcat. - // > java.lang.IllegalArgumentException: com.termux.api: Targeting S+ (version 31 and above) - // > requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent. - // > Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality - // > depends on the PendingIntent being mutable, e.g. if it needs to be used with inline - // > replies or bubbles. - // The intent must not be immutable as the `EXTRA_PERMISSION_GRANTED` extra needs to be - // returned by the Android framework. Otherwise, if requesting permission after - // reattaching device, and user presses `OK` to grant permission, the - // `EXTRA_PERMISSION_GRANTED` extra would not exist in the intent, and default `false` - // value would get used, and `No permission` condition of the open request would get - // triggered, even though permission was granted and it won't need to be requested for - // next open request. - // - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability - //noinspection ObsoleteSdkInt - int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_MUTABLE : 0; - PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, usbIntent, pendingIntentFlags); - - try { - // Specify flag to not export receiver, otherwise permission request intent will be - // blocked if app uses `targetSdkVersion` `>= 34`. - // - https://developer.android.com/about/versions/14/behavior-changes-14#runtime-receivers-exported - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(usbReceiver, new IntentFilter(ACTION_USB_PERMISSION), - Context.RECEIVER_NOT_EXPORTED); - } else { - //noinspection UnspecifiedRegisterReceiverFlag - registerReceiver(usbReceiver, new IntentFilter(ACTION_USB_PERMISSION)); - } - - // Request permission and wait. - usbManager.requestPermission(device, permissionIntent); - - try { - if (!latch.await(30L, TimeUnit.SECONDS)) { - Logger.logVerbose(LOG_TAG, "Permission request time out for device \"" + device.getDeviceName() + "\" after 30s"); - return -1; - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - Boolean requestResult = result.get(); - if (requestResult != null) { - usbReceiver = null; - return requestResult ? 0 : 1; - } else { - return 1; - } - } finally { - try { - if (usbReceiver != null) { - unregisterReceiver(usbReceiver); - } - } catch (Exception e) { - // Ignore - } - } - } + protected static final String LOG_TAG = "UsbService"; + + private final ThreadPoolExecutor mThreadPoolExecutor; + + public UsbService() { + super(); + mThreadPoolExecutor = new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>()); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public void onCreate() { + Logger.logDebug(LOG_TAG, "onCreate"); + + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Logger.logDebug(LOG_TAG, "onStartCommand"); + + String action = intent.getAction(); + if (action == null) { + Logger.logError(LOG_TAG, "No action passed"); + ResultReturner.returnData(this, intent, out -> out.append("Missing action\n")); + } + + if (action != null) { + switch (action) { + case "list": + runListAction(intent); + break; + case "permission": + runPermissionAction(intent); + break; + case "open": + runOpenAction(intent); + break; + + /* The following cases produce serialised data that is + * supposed to be parsed by libusb (or some other + * program/library) in userspace, and not printed to + * stdout */ + case "getDevices": + runGetDevicesAction(intent); + break; + default: + Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); + ResultReturner.returnData(this, intent, out -> out.append("Invalid action: \"" + action + "\"\n")); + } + } + + return Service.START_NOT_STICKY; + } + + @Override + public void onDestroy() { + Logger.logDebug(LOG_TAG, "onDestroy"); + + super.onDestroy(); + } + + + + protected void runListAction(Intent intent) { + Logger.logVerbose(LOG_TAG,"Running 'list' usb devices action"); + + ResultReturner.returnData(this, intent, new ResultReturner.ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + listDevices(out); + } + }); + } + + protected void listDevices(JsonWriter out) throws IOException { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + HashMap deviceList = usbManager.getDeviceList(); + out.beginArray(); + for (UsbDevice device : deviceList.values()) { + out.beginObject(); + out.name("device_name").value(device.getDeviceName()); + out.name("device_id").value(device.getDeviceId()); + out.name("vendor_id").value(String.format("0x%04x", device.getVendorId())); + out.name("product_id").value(String.format("0x%04x", device.getProductId())); + out.name("device_class").value(device.getDeviceClass()+" - "+translateDeviceClass(device.getDeviceClass())); + out.name("device_subclass").value(device.getDeviceSubclass()); + if (device.getManufacturerName() != null) { + out.name("manufacturer_name").value(device.getManufacturerName().replace("\u0000", "")); + } else { + out.name("manufacturer_name").value(device.getManufacturerName()); + } + out.name("device_protocol").value(device.getDeviceProtocol()); + if (device.getProductName() != null) { + out.name("product name").value(device.getProductName().replace("\u0000", "")); + } else { + out.name("product name").value(device.getProductName()); + } + out.name("serial_number").value(device.getSerialNumber()); + out.name("configurations").value(device.getConfigurationCount()); + out.name("descriptor_type").value(device.describeContents()); + out.name("access_granted").value(usbManager.hasPermission(device)); + out.endObject(); + } + out.endArray(); + } + + private static String translateDeviceClass(int usbClass){ + switch(usbClass){ + case UsbConstants.USB_CLASS_APP_SPEC: + return "App specific USB class"; + case UsbConstants.USB_CLASS_AUDIO: + return "Audio device"; + case UsbConstants.USB_CLASS_CDC_DATA: + return "CDC device (communications device class)"; + case UsbConstants.USB_CLASS_COMM: + return "Communication device"; + case UsbConstants.USB_CLASS_CONTENT_SEC: + return "Content security device"; + case UsbConstants.USB_CLASS_CSCID: + return "Content smart card device"; + case UsbConstants.USB_CLASS_HID: + return "Human interface device (for example a keyboard)"; + case UsbConstants.USB_CLASS_HUB: + return "USB hub"; + case UsbConstants.USB_CLASS_MASS_STORAGE: + return "Mass storage device"; + case UsbConstants.USB_CLASS_MISC: + return "Wireless miscellaneous devices"; + case UsbConstants.USB_CLASS_PER_INTERFACE: + return "Usb class is determined on a per-interface basis"; + case UsbConstants.USB_CLASS_PHYSICA: + return "Physical device"; + case UsbConstants.USB_CLASS_PRINTER: + return "Printer"; + case UsbConstants.USB_CLASS_STILL_IMAGE: + return "Still image devices (digital cameras)"; + case UsbConstants.USB_CLASS_VENDOR_SPEC: + return "Vendor specific USB class"; + case UsbConstants.USB_CLASS_VIDEO: + return "Video device"; + case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER: + return "Wireless controller device"; + default: return "Unknown USB class!"; + } + } + + + protected void runPermissionAction(Intent intent) { + mThreadPoolExecutor.submit(() -> { + String deviceName = intent.getStringExtra("device"); + + Logger.logVerbose(LOG_TAG,"Running 'permission' action for device \"" + deviceName + "\""); + + UsbDevice device = getDevice(intent, deviceName); + if (device == null) return; + + int status = checkAndRequestUsbDevicePermission(intent, device); + ResultReturner.returnData(this, intent, out -> { + if (status == 0) { + Logger.logVerbose(LOG_TAG, "Permission granted for device \"" + device.getDeviceName() + "\""); + out.append("Permission granted.\n" ); + } else if (status == 1) { + Logger.logVerbose(LOG_TAG, "Permission denied for device \"" + device.getDeviceName() + "\""); + out.append("Permission denied.\n" ); + } else if (status == -1) { + out.append("Permission request timeout.\n" ); + } + }); + }); + } + + + + protected void runOpenAction(Intent intent) { + mThreadPoolExecutor.submit(() -> { + String deviceName = intent.getStringExtra("device"); + String vendorId = intent.getStringExtra("vendorId"); + String productId = intent.getStringExtra("productId"); + if (deviceName == null && (vendorId == null || productId == null)) { + Logger.logError(LOG_TAG, "Missing usb device info in open()"); + } + + UsbDevice device; + if (deviceName != null) { + Logger.logVerbose(LOG_TAG,"Running 'open' action for device \"" + deviceName + "\""); + device = getDevice(intent, deviceName); + } else { + Logger.logVerbose(LOG_TAG,"Running 'open' action for vendor Id \"" + vendorId + "\" and product Id \"" + productId + "\""); + device = getDevice(intent, vendorId, productId); + } + + if (device == null) return; + + int status = checkAndRequestUsbDevicePermission(intent, device); + ResultReturner.returnData(this, intent, new ResultReturner.WithAncillaryFd() { + @Override + public void writeResult(PrintWriter out) { + if (status == 0) { + int fd = open(device); + if (fd < 0) { + Logger.logVerbose(LOG_TAG, "Failed to open device \"" + device.getDeviceName() + "\": " + fd); + out.append("Open device failed.\n"); + } else { + Logger.logVerbose(LOG_TAG, "Open device \"" + device.getDeviceName() + "\" successful"); + this.sendFd(out, fd); + } + } else if (status == 1) { + Logger.logVerbose(LOG_TAG, "Permission denied to open device \"" + device.getDeviceName() + "\""); + out.append("Permission denied.\n" ); + } else if (status == -1) { + out.append("Permission request timeout.\n" ); + } + } + }); + }); + } + + protected int open(@NonNull UsbDevice device) { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + UsbDeviceConnection connection = usbManager.openDevice(device); + if (connection == null) return -2; + + int fd = connection.getFileDescriptor(); + if (fd == -1) { + connection.close(); + return -1; + } + + openDevices.put(fd, connection); + return fd; + } + + + + protected UsbDevice getDevice(Intent intent, String deviceName) { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + HashMap deviceList = usbManager.getDeviceList(); + UsbDevice device = deviceList.get(deviceName); + if (device == null) { + Logger.logVerbose(LOG_TAG, "Failed to find device \"" + deviceName + "\""); + ResultReturner.returnData(this, intent, out -> out.append("No such device.\n")); + } + + return device; + } + + protected UsbDevice getDevice(Intent intent, String vendorId, String productId) { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + HashMap deviceList = usbManager.getDeviceList(); + for (UsbDevice dev : deviceList.values()) { + if (String.format("0x%04x", dev.getVendorId()).equalsIgnoreCase(vendorId) && + String.format("0x%04x", dev.getProductId()).equalsIgnoreCase(productId)) { + return dev; + } + + } + Logger.logVerbose(LOG_TAG, "Failed to find device with vendor Id \"" + vendorId + "\" and product Id\"" + productId + "\""); + ResultReturner.returnData(this, intent, out -> out.append("No such device.\n")); + + return null; + } + + + protected boolean checkUsbDevicePermission(@NonNull UsbDevice device) { + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + return usbManager.hasPermission(device); + } + + protected int checkAndRequestUsbDevicePermission(Intent intent, @NonNull UsbDevice device) { + boolean checkResult = checkUsbDevicePermission(device); + Logger.logVerbose(LOG_TAG, "Permission check result for device \"" + device.getDeviceName() + "\": " + checkResult); + if (checkResult) { + return 0; + } + + if(!intent.getBooleanExtra("request", false)) { + return 1; + } + + Logger.logVerbose(LOG_TAG, "Requesting permission for device \"" + device.getDeviceName() + "\""); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference result = new AtomicReference<>(); + + BroadcastReceiver usbReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent usbIntent) { + if (ACTION_USB_PERMISSION.equals(usbIntent.getAction())) { + boolean requestResult = usbIntent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + Logger.logVerbose(LOG_TAG, "Permission request result for device \"" + device.getDeviceName() + "\": " + requestResult); + result.set(requestResult); + } + context.unregisterReceiver(this); + latch.countDown(); + } + }; + + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + Intent usbIntent = new Intent(ACTION_USB_PERMISSION); + // Use explicit intent, otherwise permission request intent will be blocked if intent is + // mutable and app uses `targetSdkVersion` `>= 34`, or following exception will be logged + // to logcat if app uses `targetSdkVersion` `< 34`. + // > `android.app.StackTrace: New mutable implicit PendingIntent: pkg=com.termux.api, + // > action=com.termux.api.USB_PERMISSION, featureId=null. This will be blocked once the + // > app targets U+ for security reasons.` + // - https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents + usbIntent.setPackage(getPackageName()); + + // Use mutable intent, otherwise permission request intent will be blocked if app + // uses `targetSdkVersion` `>= 31` and following exception may be logged to logcat. + // > java.lang.IllegalArgumentException: com.termux.api: Targeting S+ (version 31 and above) + // > requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent. + // > Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality + // > depends on the PendingIntent being mutable, e.g. if it needs to be used with inline + // > replies or bubbles. + // The intent must not be immutable as the `EXTRA_PERMISSION_GRANTED` extra needs to be + // returned by the Android framework. Otherwise, if requesting permission after + // reattaching device, and user presses `OK` to grant permission, the + // `EXTRA_PERMISSION_GRANTED` extra would not exist in the intent, and default `false` + // value would get used, and `No permission` condition of the open request would get + // triggered, even though permission was granted and it won't need to be requested for + // next open request. + // - https://developer.android.com/about/versions/12/behavior-changes-12#pending-intent-mutability + //noinspection ObsoleteSdkInt + int pendingIntentFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_MUTABLE : 0; + PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, usbIntent, pendingIntentFlags); + + try { + // Specify flag to not export receiver, otherwise permission request intent will be + // blocked if app uses `targetSdkVersion` `>= 34`. + // - https://developer.android.com/about/versions/14/behavior-changes-14#runtime-receivers-exported + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(usbReceiver, new IntentFilter(ACTION_USB_PERMISSION), + Context.RECEIVER_NOT_EXPORTED); + } else { + //noinspection UnspecifiedRegisterReceiverFlag + registerReceiver(usbReceiver, new IntentFilter(ACTION_USB_PERMISSION)); + } + + // Request permission and wait. + usbManager.requestPermission(device, permissionIntent); + + try { + if (!latch.await(30L, TimeUnit.SECONDS)) { + Logger.logVerbose(LOG_TAG, "Permission request time out for device \"" + device.getDeviceName() + "\" after 30s"); + return -1; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + Boolean requestResult = result.get(); + if (requestResult != null) { + usbReceiver = null; + return requestResult ? 0 : 1; + } else { + return 1; + } + } finally { + try { + if (usbReceiver != null) { + unregisterReceiver(usbReceiver); + } + } catch (Exception e) { + // Ignore + } + } + } + /* The following actions produce serialised data that is suppose + * to be parsed by some program or library in userspace, and not + * printed to stdout */ + + protected void runGetDevicesAction(Intent intent) { + ResultReturner.returnData(this, intent, new ResultReturner.BinaryOutput() { + @Override + public void writeResult(OutputStream out) throws Exception { + termuxUsb.Builder devices = termuxUsb.newBuilder(); + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + + HashMap deviceList = usbManager.getDeviceList(); + for (UsbDevice dev : deviceList.values()) { + termuxUsbDevice.Builder deviceBuilder = termuxUsbDevice.newBuilder(); + + String[] devName = dev.getDeviceName().split("/"); + int busNum = Integer.valueOf(devName[devName.length - 1]); + int portNum = Integer.valueOf(devName[devName.length - 2]); + + deviceBuilder.setBusNumber(busNum); + deviceBuilder.setPortNumber(portNum); + deviceBuilder.setDeviceAddress(dev.getDeviceName()); + + termuxUsbDeviceDescriptor.Builder deviceDescBuilder = termuxUsbDeviceDescriptor.newBuilder(); + + deviceDescBuilder.setConfigurationCount(dev.getConfigurationCount()); + deviceDescBuilder.setDeviceClass(dev.getDeviceClass()); + deviceDescBuilder.setDeviceProtocol(dev.getDeviceProtocol()); + deviceDescBuilder.setDeviceSubclass(dev.getDeviceSubclass()); + deviceDescBuilder.setProductId(dev.getProductId()); + deviceDescBuilder.setVendorId(dev.getVendorId()); + if (dev.getManufacturerName() != null) { + deviceDescBuilder.setManufacturerName(dev.getManufacturerName().replace("\u0000", "")); + } else { + deviceDescBuilder.setManufacturerName(""); + } + if (dev.getProductName() != null) { + deviceDescBuilder.setProductName(dev.getProductName().replace("\u0000", "")); + } else { + deviceDescBuilder.setProductName(""); + } + if (dev.getSerialNumber() != null) { + deviceDescBuilder.setSerialNumber(dev.getSerialNumber()); + } else { + deviceDescBuilder.setSerialNumber(""); + } + + termuxUsbDeviceDescriptor deviceDesc = deviceDescBuilder.build(); + + deviceBuilder.setDevice(deviceDesc); + + termuxUsbDevice device = deviceBuilder.build(); + + Logger.logDebug(LOG_TAG, device.toString()); + devices.addDevice(device); + } + devices.build().writeTo(out); + } + }); + } + } + }); + } } } diff --git a/app/src/main/proto/UsbAPI.proto b/app/src/main/proto/UsbAPI.proto new file mode 100644 index 000000000..463dfab3d --- /dev/null +++ b/app/src/main/proto/UsbAPI.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package usbapi; + +option java_package = "com.termux.api"; +option java_outer_classname = "UsbAPIProto"; + +/* Modelled after libusb's struct libusb_device_descriptor */ +message termuxUsbDeviceDescriptor { + int32 configurationCount = 1; + int32 deviceClass = 2; + int32 deviceProtocol = 3; + int32 deviceSubclass = 4; + int32 productId = 5; + int32 vendorId = 6; + string manufacturerName = 7; + string productName = 8; + string serialNumber = 9; +} + +/* Loosely modelled after libusb's internal struct libusb_device */ +message termuxUsbDevice { + int32 busNumber = 1; + int32 portNumber = 2; + string deviceAddress = 3; + termuxUsbDeviceDescriptor device = 4; +} + +message termuxUsb { + repeated termuxUsbDevice device = 1; +} diff --git a/build.gradle b/build.gradle index b23ea2692..ff5128776 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:8.7.3" + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.4' } } From d3e5c52409b477c12efa9cb75d76092845cab9cc Mon Sep 17 00:00:00 2001 From: Henrik Grimler Date: Sat, 8 Mar 2025 01:03:47 +0100 Subject: [PATCH 5/5] enhance(UsbAPI): add getConfigDescriptor function A config descriptor can contain several interfaces, and each interface can in turn contain several endpoints, so building and parsing the messages is a bit complicated. --- .../main/java/com/termux/api/apis/UsbAPI.java | 78 +++++++++++++++++++ app/src/main/proto/UsbAPI.proto | 26 +++++++ 2 files changed, 104 insertions(+) diff --git a/app/src/main/java/com/termux/api/apis/UsbAPI.java b/app/src/main/java/com/termux/api/apis/UsbAPI.java index 3100f97b5..c5bc81b6f 100644 --- a/app/src/main/java/com/termux/api/apis/UsbAPI.java +++ b/app/src/main/java/com/termux/api/apis/UsbAPI.java @@ -6,9 +6,12 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.hardware.usb.UsbConfiguration; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbManager; import android.os.Build; import android.os.Bundle; @@ -21,8 +24,11 @@ import com.termux.shared.termux.TermuxConstants; import com.termux.api.UsbAPIProto.termuxUsb; +import com.termux.api.UsbAPIProto.termuxUsbConfigDescriptor; import com.termux.api.UsbAPIProto.termuxUsbDevice; import com.termux.api.UsbAPIProto.termuxUsbDeviceDescriptor; +import com.termux.api.UsbAPIProto.termuxUsbEndpointDescriptor; +import com.termux.api.UsbAPIProto.termuxUsbInterfaceDescriptor; import java.io.IOException; import java.io.OutputStream; @@ -108,6 +114,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { case "getDevices": runGetDevicesAction(intent); break; + case "getConfigDescriptor": + getConfigDescriptorAction(intent); + break; default: Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); ResultReturner.returnData(this, intent, out -> out.append("Invalid action: \"" + action + "\"\n")); @@ -495,6 +504,75 @@ public void writeResult(OutputStream out) throws Exception { } }); } + + protected void getConfigDescriptorAction(Intent intent) { + ResultReturner.returnData(this, intent, new ResultReturner.BinaryOutput() { + @Override + public void writeResult(OutputStream out) throws Exception { + String deviceName = intent.getStringExtra("device"); + int configIndex = intent.getIntExtra("config", 0); + UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); + HashMap deviceList = usbManager.getDeviceList(); + + if (deviceName == null) { + Logger.logError(LOG_TAG, "Missing device argument\n"); + return; + } + + UsbDevice device = deviceList.get(deviceName); + if (device == null) { + Logger.logError(LOG_TAG, "No such device\n"); + return; + } + if (device.getConfigurationCount()-1 < configIndex) { + Logger.logError(LOG_TAG, "Requested config does not exist\n"); + return; + } + UsbConfiguration configuration = device.getConfiguration(configIndex); + int numInterfaces = configuration.getInterfaceCount(); + + termuxUsbConfigDescriptor.Builder configBuilder = termuxUsbConfigDescriptor.newBuilder(); + configBuilder.setConfigurationValue(configIndex); + configBuilder.setMaxPower(configuration.getMaxPower()); + if (configuration.getName() != null) { + configBuilder.setConfiguration(configuration.getName()); + } else { + configBuilder.setConfiguration(""); + } + + for (int i = 0; i < numInterfaces; i++) { + UsbInterface intf = configuration.getInterface(i); + + termuxUsbInterfaceDescriptor.Builder intfDescBuilder = termuxUsbInterfaceDescriptor.newBuilder(); + intfDescBuilder.setAlternateSetting(intf.getAlternateSetting()); + intfDescBuilder.setInterfaceClass(intf.getInterfaceClass()); + intfDescBuilder.setInterfaceSubclass(intf.getInterfaceSubclass()); + intfDescBuilder.setInterfaceProtocol(intf.getInterfaceProtocol()); + if (intf.getName() != null) { + intfDescBuilder.setInterface(intf.getName()); + } else { + intfDescBuilder.setInterface(""); + } + int numEndpoints = intf.getEndpointCount(); + + for (int j = 0; j < numEndpoints; j++) { + UsbEndpoint endpoint = intf.getEndpoint(j); + + termuxUsbEndpointDescriptor.Builder endpointDescBuilder = termuxUsbEndpointDescriptor.newBuilder(); + endpointDescBuilder.setEndpointAddress(endpoint.getAddress()); + endpointDescBuilder.setAttributes(endpoint.getAttributes()); + endpointDescBuilder.setMaxPacketSize(endpoint.getMaxPacketSize()); + endpointDescBuilder.setInterval(endpoint.getInterval()); + + intfDescBuilder.addEndpoint(endpointDescBuilder.build()); + } + + configBuilder.addInterface(intfDescBuilder.build()); + } + + termuxUsbConfigDescriptor config = configBuilder.build(); + Logger.logDebug(LOG_TAG, config.toString()); + config.writeTo(out); } }); } diff --git a/app/src/main/proto/UsbAPI.proto b/app/src/main/proto/UsbAPI.proto index 463dfab3d..5a1d89c5c 100644 --- a/app/src/main/proto/UsbAPI.proto +++ b/app/src/main/proto/UsbAPI.proto @@ -5,6 +5,32 @@ package usbapi; option java_package = "com.termux.api"; option java_outer_classname = "UsbAPIProto"; +/* Modelled after libusb's struct libusb_endpoint_descriptor */ +message termuxUsbEndpointDescriptor { + int32 endpointAddress = 1; + int32 attributes = 2; + int32 maxPacketSize = 3; + int32 interval = 4; +} + +/* Modelled after libusb's struct libusb_interface_descriptor */ +message termuxUsbInterfaceDescriptor { + int32 alternateSetting = 1; + int32 interfaceClass = 2; + int32 interfaceSubclass = 3; + int32 interfaceProtocol = 4; + string interface = 5; + repeated termuxUsbEndpointDescriptor endpoint = 6; +} + +/* Modelled after libusb's struct libusb_config_descriptor */ +message termuxUsbConfigDescriptor { + int32 configurationValue = 1; + int32 maxPower = 2; + string configuration = 3; + repeated termuxUsbInterfaceDescriptor interface = 4; +} + /* Modelled after libusb's struct libusb_device_descriptor */ message termuxUsbDeviceDescriptor { int32 configurationCount = 1;