From 2d8d534cf2f087115a7ff47325b8c08b2c589436 Mon Sep 17 00:00:00 2001 From: Ross Smith Date: Wed, 6 May 2026 09:41:14 +0000 Subject: [PATCH] Fix USB permission flow on Android 12+ UsbHelper.requestPermission() relies on UsbManager mutating the PendingIntent it broadcasts back to us so we can read EXTRA_DEVICE and EXTRA_PERMISSION_GRANTED. On Android 12 (API 31) PendingIntents must declare mutability explicitly, and on Android 13 (API 33) runtime-registered receivers must declare export-ness. Without these, the PendingIntent registration is rejected on API 31+ with IllegalArgumentException, the receiver registration is rejected on API 33+ with SecurityException, and (when an immutable PendingIntent is used as a workaround) the permission broadcast arrives with a null EXTRA_DEVICE and the flow stalls silently. This change: - Adds FLAG_MUTABLE to the USB_PERMISSION PendingIntent on Build.VERSION_CODES.S and above. FLAG_IMMUTABLE is wrong here: UsbManager needs to attach EXTRA_DEVICE and EXTRA_PERMISSION_GRANTED to the intent before broadcasting it back, which an immutable PendingIntent forbids. - Passes Context.RECEIVER_NOT_EXPORTED to registerReceiver on Build.VERSION_CODES.TIRAMISU and above. The USB_PERMISSION action is delivered by the system to our own process; not-exported is the correct choice and is required on API 33+. - Defensively ignores broadcasts whose action or EXTRA_DEVICE is null, and clears mPermissionPending in the USB_PERMISSION case so a swallowed grant cannot leave the helper stuck refusing further permission requests. Verified on Android 13 (DUDU7 head unit, UNISOC UMS9620, SDK 33) with a 16C0:05DC USB DAB dongle: the USB permission grant path (USB_DEVICE_ATTACHED -> requestPermission -> permission granted -> openDevice) now completes without exception. Pre-fix, the same flow failed at the PendingIntent / registerReceiver call sites on Android 12+ / 13+ respectively. File licence is unchanged (Apache-2.0, IRT GmbH 2018). --- .../java/org/omri/radio/impl/UsbHelper.java | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/omriusb/src/main/java/org/omri/radio/impl/UsbHelper.java b/omriusb/src/main/java/org/omri/radio/impl/UsbHelper.java index ec24cc4..1625996 100644 --- a/omriusb/src/main/java/org/omri/radio/impl/UsbHelper.java +++ b/omriusb/src/main/java/org/omri/radio/impl/UsbHelper.java @@ -83,13 +83,29 @@ private UsbHelper(Context context) { if(mContext != null) { mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE); - mUsbPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0); + // Android 12 (S, API 31) requires PendingIntent flags to explicitly declare mutability. + // UsbManager.requestPermission() needs to populate EXTRA_DEVICE and + // EXTRA_PERMISSION_GRANTED on the intent before broadcasting it back to us, which + // requires FLAG_MUTABLE. (FLAG_IMMUTABLE would deliver an empty broadcast and the + // permission flow would silently fail.) + int piFlags = PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + piFlags |= PendingIntent.FLAG_MUTABLE; + } + mUsbPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), piFlags); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); - mContext.registerReceiver(mUsbBroadcastReceiver, filter); + // Android 13 (TIRAMISU, API 33) requires runtime-registered receivers to specify + // export-ness. ACTION_USB_PERMISSION is delivered by the system to our own process, + // so RECEIVER_NOT_EXPORTED is correct. + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver(mUsbBroadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + mContext.registerReceiver(mUsbBroadcastReceiver, filter); + } created(); } } @@ -228,6 +244,20 @@ void removeDevice(UsbDevice remDev) { public void onReceive(Context context, Intent intent) { UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); String action = intent.getAction(); + if (action == null) { + if (DEBUG) Log.w(TAG, "Received broadcast with null action; ignoring"); + return; + } + if (device == null) { + // Some Android builds deliver a USB_PERMISSION broadcast with EXTRA_DEVICE missing + // (e.g. when the system retracts a pending request). Without a device handle there + // is nothing meaningful to do; clearing pending state avoids getting stuck. + if (DEBUG) Log.w(TAG, "Received " + action + " with null EXTRA_DEVICE; ignoring"); + if (ACTION_USB_PERMISSION.equals(action)) { + mPermissionPending = false; + } + return; + } synchronized (this) { if (ACTION_USB_PERMISSION.equals(action)) { if(DEBUG)Log.d(TAG, "Received Permission request: " + action);