3D visionTroubleshooting

Depth camera crash on Android: fix USB permission handling

Your depth camera app crashes on Android at USB open with a JNI ERROR: the USB permission was never granted. Fix the request flow and prove the fix in logcat.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01Why does the app crash the moment it opens the depth camera?
  2. 02What does logcat show when USB permission is the cause?
  3. 03Which causes are most likely, in what order?
  4. 04How do you request USB permission before the native camera open?
  5. 05Why does the native SDK thread need a JNI attach?
  6. 06Why does the chmod workaround fail after every reboot?
  7. 07How do you verify the fix and keep it fixed?
  8. 08When is the crash not a permission problem at all?
  9. 09Questions

Your depth camera app builds cleanly, the tablet enumerates the device, the status LED is lit — and the first open call kills the process with the system dialog announcing the app has stopped. The reflex is to suspect the camera, the cable or the SDK build. In this exact symptom pattern the usual culprit is Android's USB permission handling and the way a native library opens the device afterwards. This page walks the diagnosis in the order you meet it on the bench: read the logcat trace, rank the five likely causes, request the permission before any native call, attach the SDK's worker threads properly, and prove the fix with one clean plug-in cycle.

Why does the app crash the moment it opens the depth camera?

The crash is almost always Android's USB permission layer rather than the camera itself: the SDK's native code opens a /dev/bus/usb node your app was never granted, the open is refused, and a fatal JNI error tears the process down.

Three details in the symptom confirm it. The system dialog names no cause — it only reports that the process died. The LED stays lit, even after the app is gone, because the LED is driven by the camera's own firmware the moment the port enumerates it: enumeration powers and identifies the device, but grants your app nothing. And the timing is the giveaway: the death lands on the open call, not on a frame or a computation.

Android separates those steps deliberately. Your app can list a UsbDevice while holding no right to open it — UsbManager.openDevice returns null without a grant, and a library that touches /dev/bus/usb directly meets an SELinux refusal instead. When that refusal, or a JNI misuse right behind it, reaches code that aborts, ART kills the whole process while the camera keeps running on bus power.

What does logcat show when USB permission is the cause?

Reproduce once with a cleared buffer and look for two decisive records: an SELinux audit line that refuses your app's domain access to a usb_device node, then an E/art JNI ERROR inside your package right at the open call.

Clear the log, replug, launch, and capture:

adb logcat -c
adb logcat | grep -E "usb|JNI|denied"

Two record types decide it:

07-05 06:02:23.337 type=1400 audit: avc: denied { open } for path="/dev/bus/usb" scontext=u:r:untrusted_app tcontext=u:object_r:usb_device:s0 tclass=dir permissive=1
07-11 06:19:58.102 E/art: JNI ERROR

The avc record is the kernel's access log: scontext is who tried (your app's untrusted_app domain), tcontext is what it touched (a node labeled usb_device), and denied { open } is the refused operation. In permissive mode the kernel logs and allows; in enforcing mode the open fails outright. Either way, the line proves your app reached the device node without a granted permission.

The E/art JNI ERROR is the fatal record — that abort ends the process. Newer Android builds print the same failure as a longer JNI DETECTED ERROR line, so match on JNI rather than on the exact wording. If your log instead shows UnsatisfiedLinkError before any USB line, stop: the library never loaded, which is a different failure — see the last section.

Which causes are most likely, in what order?

Rank the causes in this order: permission never requested, the native open racing the permission dialog, a worker thread that never attached to the JVM, the root chmod recipe wiped by a reboot, and a second process that already owns the device. Each has a one-minute test:

  1. Permission never requested. The code treats USB like a manifest permission and calls the SDK straight away. Test: search your source for requestPermission — if nothing calls UsbManager.requestPermission before the first native call, this is your bug.
  2. The open races the dialog. The result of requestPermission arrives asynchronously, and the dialog takes seconds of human time, so an immediate SDK open runs before the grant. Test: log one line in your BroadcastReceiver — if the SDK's open log precedes the receiver's log, you have the race.
  3. The worker thread never attached. The SDK opens or streams on its own native thread with a JNIEnv that belongs elsewhere. Test: run the SDK open on the main thread once; if the crash disappears, the attach pattern is your bug (next section).
  4. The chmod recipe expired. A root shell's chmod fixed it once; the next replug or reboot rebuilt the device nodes and the crash returned. Test: reboot without re-running the commands and reproduce.
  5. A second process owns the interface. A stuck earlier instance or another app claims the camera exclusively, and the open or claim fails. Test: force-stop every app that touches the camera, replug, retry; if a device-busy error replaces the crash, follow the two-depth-cameras USB busy fix.

How do you request USB permission before the native camera open?

Ask for the permission with UsbManager.requestPermission and open the device only inside the BroadcastReceiver that reports EXTRA_PERMISSION_GRANTED; the grant then covers the connection, so one accepted dialog is enough for the whole session.

There is no manifest line that grants USB access — it is a runtime exchange with the user, in four steps:

  1. Declare the host capability in AndroidManifest.xml. That is a feature declaration, not a permission.
  2. Find the device: usbManager.deviceList, matched on your camera's vendorId and productId. Read both from the tablet with adb shell cat /sys/bus/usb/devices/*/idVendor and the matching idProduct file.
  3. If usbManager.hasPermission(device) is false, build a PendingIntent carrying your own action string and call requestPermission. The system shows the allow dialog.
  4. In the receiver registered for that action, read EXTRA_PERMISSION_GRANTED and open the camera only when it is true — never on a timer, never before the receiver fires.
// AndroidManifest.xml declares the capability only:
//   <uses-feature android:name="android.hardware.usb.host" />

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION" // the platform docs' example action string; any unique one works

// Register the receiver BEFORE requesting, so the callback cannot be missed:
private val usbReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
        val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
        if (granted && device != null) openDepthCamera(device)   // your own open function
    }
}
context.registerReceiver(usbReceiver, IntentFilter(ACTION_USB_PERMISSION))

val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val device: UsbDevice = usbManager.deviceList.values.first {
    it.vendorId == 0x1234 && it.productId == 0x5678   // illustrative IDs: use your camera's
}
if (usbManager.hasPermission(device)) {
    openDepthCamera(device)
} else {
    val flags = if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0
    val intent = PendingIntent.getBroadcast(context, 0, Intent(ACTION_USB_PERMISSION), flags)
    usbManager.requestPermission(device, intent)       // the dialog appears here
}

Two platform details bite here. From Android 12 (API level 31) the PendingIntent must be mutable — FLAG_MUTABLE — because the system writes the result extras into it; an immutable intent silently drops the callback. From Android 13 (API 33), a context-registered receiver must declare an export flag; system broadcasts such as this result still reach a receiver declared not exported.

To start the flow by itself at plug-in, add an intent filter for ACTION_USB_DEVICE_ATTACHED with a res/xml device filter naming your vendor and product IDs in decimal; the system then offers to launch your app when the cable goes in. The dialog still appears once per connection — no Android release grants a third-party app silently.

The test that proves the flow: unplug, replug, launch, accept. Your receiver's granted line must precede the SDK's open, and the avc denial from the previous section must be gone.

Why does the native SDK thread need a JNI attach?

A JNIEnv pointer is valid only on the thread that received it: when the SDK's capture thread calls back into Java, or reuses an env cached from another thread, ART raises the fatal JNI ERROR even though the permission was granted correctly.

The fix lives in the library's JNI layer, and the pattern is short. Store the JavaVM pointer once in JNI_OnLoad. On every thread that will touch JNI, call GetEnv first; attach only when it reports the thread detached, and detach before the thread exits.

static JavaVM* g_vm = nullptr;                     // stored once in JNI_OnLoad

jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
    g_vm = vm;
    return JNI_VERSION_1_6;
}

JNIEnv* attached_env() {                           // call at the top of each worker thread
    JNIEnv* env = nullptr;
    if (g_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_OK) {
        return env;                                // thread was already attached
    }
    g_vm->AttachCurrentThread(&env, nullptr);      // first call on this thread
    return env;
}
// call g_vm->DetachCurrentThread() before the thread exits

If you build the SDK from source, apply that in the open and streaming callbacks — an unattached thread is exactly what an E/art JNI ERROR at open looks like. If the SDK is a closed library with this defect, it fails on every tablet, not only yours: that is a vendor support case, and the logcat trace you captured earlier is the evidence they will ask for.

Why does the chmod workaround fail after every reboot?

The chmod recipe treats one symptom: the kernel rebuilds every /dev/bus/usb node at each enumeration with its default modes and SELinux labels, so a root shell's chmod 0777 is erased at the next replug or reboot and the crash returns on schedule.

The recipe that circulates for rooted tablets — adb shell su, then chmod 0777 on /dev/bus/usb and its children, then setenforce 0 — does enable a native open. It loosens the node's file mode and switches SELinux to permissive (getenforce prints the current mode), which is why it feels like a fix and why it must be re-run after every restart. It also does nothing for the other causes in the ranking: if the fatal line was the JNI ERROR rather than the denial, the process still dies at the open, workaround or not.

How do you verify the fix and keep it fixed?

One clean cycle proves the fix: clear the log, replug the camera, launch the app, accept the dialog, and the pass condition is a permission-granted line ahead of the SDK open with no avc denial and no JNI ERROR for a full session.

  1. adb logcat -c, unplug the camera, replug it, launch the app.
  2. Accept the permission dialog once.
  3. Let the stream run for minutes, not seconds, and watch the frame rate hold.

Then harden against the two ways it regresses. Do not assume a grant survived: after any replug, reboot or crash, call hasPermission(device) again and fall through to the dialog when it returns false. And on ACTION_USB_DEVICE_DETACHED, close the handle and return to the device-list step, because a cached UsbDevice or open descriptor from the previous plug-in fails or wedges the next session.

Make it a bench habit: a scripted plug-unplug-run loop of ten cycles catches the regression before your users do. If the process dies mid-run instead of at open, that is a different family — follow diagnosing USB recovery after a capture crash.

When is the crash not a permission problem at all?

If the log shows UnsatisfiedLinkError or a missing .so before any USB record, the crash is an ABI packaging problem; if the failure starts at plug-in with a descriptor error, the cable, hub or camera itself is dying — different repairs, and logcat separates them in seconds.

None of this is specific to one depth camera: the permission flow is a property of Android, so it applies equally to the ToF module, structured-light camera or laser profiler you bring up next. If the Android build is still at hardware selection, the camera finder and the camera configurator match resolution, interface and mount before you write integration code — and a USB3 board-level camera built into your own device follows the same request-then-open discipline. The rest of the depth bring-up guides sit on the 3D topic page.

Cameras3D sensors

Laser profilers and stereo 3D cameras

170 models · from $699.00

Questions

How long does a granted USB permission last?

Until the cable comes out. Android grants access per connection and revokes it at disconnect, so the receiver fires again on every plug-in. Design for that: expect the dialog once per connection, treat every ATTACHED event as a fresh negotiation, and never try to store or restore the permission anywhere.

Why does the permission dialog never appear at all?

Usually the receiver and the PendingIntent disagree. The action string in the PendingIntent must match the IntentFilter your receiver is registered for, the receiver must be registered before requestPermission fires, and on Android 12 the PendingIntent must be mutable — an immutable one silently drops the callback, so nothing shows. Fix those three and the dialog returns.

Since which Android version can an app open a USB device?

Since Android 3.1, API level 12, which introduced the android.hardware.usb package — anything you deploy today is far newer. The only version-specific trap that matters for this crash is Android 12: build the permission PendingIntent with FLAG_MUTABLE or the callback never arrives. No release lets a third-party app skip the dialog.

Can two apps use the same depth camera at once?

Not the same interface. Android hands out the permission per app, but the device's interface is claimed exclusively by whoever opens it first, so a second app gets open or claim failures while yours streams. Force-stop every app that touches the camera before bench tests, and read a device-busy error as an ownership conflict, not a permission denial.