Handle ML Kit face detector native library load failures gracefully - #196
Merged
Merged
Conversation
A device that cannot load `libface_detector_v2_jni.so` took the whole app down. `FaceTracker` built ML Kit's bundled detector in a field initializer with no guard, and ML Kit runs that model load off the caller's thread — on its own worker, inside a GMS `Task`. `Task` funnels only `Exception` into `addOnFailureListener`, so the `UnsatisfiedLinkError` escaped the worker's `Runnable` and killed the process, which is why Crashlytics reported it as fatal rather than as a failed task. Wrapping `FaceDetection.getClient` would have caught nothing. So load the library first, on our own thread, where the failure is catchable. A load that succeeds makes ML Kit's own `System.loadLibrary` a no-op; one that throws means ML Kit would have crashed, so the detector is never built: `detector` is null, `analyze` closes the proxy and publishes nothing, `close` is a no-op, and a Crashlytics non-fatal records the population. The lenses go inert and capture, trim, save and gallery are untouched — the same trade `CameraManager.bindWithLensEffect` already makes when the analysis use case cannot bind, and the one `HandTracker` makes for MediaPipe. The proxy is still closed on every frame in that state: under KEEP_ONLY_LATEST an unclosed proxy stalls the stream the hand tracker shares, so "lenses are inert" would otherwise become "the analyzer is dead". Loading by name couples one string to the dependency's `jni/` folder, so the version-catalog pin and Lesson 040 now say to re-check it on an ML Kit bump — a rename would read as "absent" and take face lenses off every device. Fixes #195 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBjSmDxvBq7XtFE9PA28Ko
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes a fatal crash when ML Kit's bundled face detection native library (
libface_detector_v2_jni.so) fails to load on certain devices.Why a
tryaroundgetClient()would not have fixed it. ML Kit loads that library from the static initializer of its ownThickFaceDetectorCreator, and it runs the model load off the caller's thread — on its own worker, inside a GMSTask.Taskfunnels onlyExceptionintoaddOnFailureListener, so theUnsatisfiedLinkErrorescapes the worker'sRunnableand kills the process. That the crash was reported as fatal rather than as a logged task failure is the evidence for this reading.The fix. Load the library ourselves first, on our own thread, where the failure is catchable:
System.loadLibraryis a no-op, nothing else changes;LinkageError→ ML Kit would have crashed, so the detector is never built.detectoris null,analyze()closes the proxy and publishes nothing,close()is a no-op, andReverseCrashlytics.reportFaceTrackerUnavailablerecords the population as a non-fatal.Face lenses go inert; capture, trim, save and gallery are untouched. That is the same trade
CameraManager.bindWithLensEffectalready makes when the analysis use case cannot bind ("a black viewfinder is not an acceptable trade for a novelty feature"), and the oneHandTrackermakes for MediaPipe (Lesson 040).The proxy is still closed on every frame in that state — under
KEEP_ONLY_LATESTan unclosed proxy stalls the stream the hand tracker shares, so "lenses are inert" would otherwise become "the analyzer is dead".Loading by name couples one string to the dependency's
jni/folder, so the version-catalog pin and Lesson 040 now both say to re-check it on an ML Kit bump — a rename would read as "absent" and take face lenses off every device.Changes Made
camera/lens/FaceTracker.ktdetectoris nullable, built by aLinkageError-guardedcreateDetector()that loads the native library first;analyze()bails but still closes the proxy;close()is null-safediagnostics/ReverseCrashlytics.ktreportFaceTrackerUnavailable()— sibling of the existing hand-tracker non-fataldocs/lessons_learned/040-…tryat all; where it is not, load the library yourselfgradle/libs.versions.tomlmlkit-face-detectionpincspell.json,.idea/dictionaries/project.xmllibface(dictionaries regenerated in sync)Not done, deliberately
play-services-mlkit-face-detection. The unbundled variant trades a missing.sofor a missing Play-services module and contradicts the bundled-model decision indocs/PRD-camera-lenses.md§5.1. Larger change, different failure mode, not minimal.System.loadLibraryalways fails on the JVM, so the guard is exercised for free): constructFaceTracker, assert it does not throw and thatanalyze()closes a mockedImageProxywithout publishing. It was not added because it could not be run in the authoring environment (below), and an unverified test is worse than none.Related Issue
Fixes #195
Type of Change
How Has This Been Tested?
Warning
The pre-PR sweep did NOT run, and this branch has NOT been built or run on a device. It was authored in a cloud session with no Android SDK, no
pwsh, no emulator, anddl.google.comblocked by the egress policy. Gradle fails before reaching any project code:scripts/hooks/require-sweep.mjscorrectly blockedcreate_pull_requestfor this commit; the PR was opened separately from the Claude Code UI. Perdocs/DEFINITION_OF_DONE.mdthis is not at "Ready for PR" — the sweep still has to run locally before it can merge.What was actually verified
-Werrorkotlinc2.4.10 against hand-written stubs for the Android / ML Kit / Firebase surface, compiled alongside the realLensPhysics.kt,LensAnchor.kt,FaceRoster.kt||at-Werrorcspell.jsonscripts/test-*.pyself-checkstest-pre-pr-sweep.pyerrors on missingpwsh(environment, not this diff)Markdown · spelling · JSON,New .md file placementStub-based type-checking proves the diff is well-formed Kotlin against the documented ML Kit signatures. It does not prove it links against the real AARs.
What was NOT verified — needs a local sweep
allWarningsAsErrors, R8),zipalign -P 16FaceTrackerNormalizationTestin particular)run-verification-loops.py— the one that matters.System.loadLibrary("face_detector_v2_jni")succeeding is now a precondition for any face lens, so this change must be proven not to disable lenses on a healthy device.Manual QA checklist
adb logcat -s OpenLoopFaceTrackermust not showML Kit face detection unavailable. If it does, the library name moved and the pin comment ingradle/libs.versions.tomlis what to re-check.libface_detector_v2_jni.soin an installed build and confirm the app opens, the camera previews, a face lens simply does nothing, and Crashlytics logsface_tracker_unavailableinstead of crashing.Checklist
allWarningsAsErrorsis on) — stub compile is clean at-Werror; the real build is unverifiedpwsh/ emulator, Google Maven blocked. Inspect Code export: SKIPPED because the IDE is unavailable in this environment. Onboarding loop: SKIPPED — not verified.🤖 Generated with Claude Code
https://claude.ai/code/session_01FBjSmDxvBq7XtFE9PA28Ko
Note
Medium Risk
Changes camera lens initialization and per-frame analysis when the detector is missing; wrong JNI name after an ML Kit bump could silently disable face lenses on all devices.
Overview
Fixes Issue #195 by preventing a fatal crash when ML Kit’s bundled face-detection native library (
face_detector_v2_jni) fails to load. ML Kit loads that.soon a GMSTaskworker whereUnsatisfiedLinkErroris not delivered to failure listeners, so wrappingFaceDetection.getClient()alone cannot catch it.FaceTrackernow callsSystem.loadLibraryon the app thread insidecreateDetector(), then builds the detector only if load succeeds; onLinkageErrorthe detector staysnull, face lenses are inert, andanalyze()still closes eachImageProxyso the shared analysis stream (including hand tracking) does not stall.ReverseCrashlytics.reportFaceTrackerUnavailablerecords a non-fatal instead of a process kill.Docs and tooling note the coupling: Lesson 040 adds guidance to pre-load JNI when failures are uncatchable, and the
mlkit-face-detectionversion pin warns to re-verify the JNI library name on bumps (a rename would disable face lenses everywhere). Dictionary entries addlibfacefor spelling.Reviewed by Cursor Bugbot for commit 4c2b20a. Bugbot is set up for automated code reviews on this repo. Configure here.