Fix camera preview/mesh overlay freeze after in-app language change - #26
Merged
Conversation
setApplicationLocales() recreates the Activity (documented AppCompat behavior), but MainViewModel survives it -- initializeTracking()'s idempotency guard (point 6) didn't distinguish that from a repeat call on the same Activity, so the camera pipeline stayed bound to the dead lifecycle: CameraX unbinds on a DESTROYED owner, and on the OPTIMAL tier the ARCore session just stayed paused forever (its ON_START/ON_STOP observer only existed on the old owner). Confirmed on device (ASUS_I001DC, OPTIMAL tier) -- an app restart was the only recovery. initializeTracking() now compares the incoming LifecycleOwner by identity and calls the new rebindTrackingPipeline() when it differs, rebuilding CameraController (genuinely lifecycle-bound) and re-registering the ON_START/ON_STOP observer, without redoing tier selection or FaceLandmarkerHelper setup. ArCoreHeadPoseTracker is deliberately never rebuilt here -- it's Application-scoped already (built with getApplication(), not the Activity), so nothing to rebind. Confirmed the hard way via adb logcat: an earlier version of this fix rebuilt it anyway and hit GLSurfaceView$GLThread.surfaceCreated() NPE, since GLSurfaceView's setRenderer() (attachTo(), called from MainScreen's AndroidView factory, which runs before window attachment) must run before the surface is created -- a rebuilt tracker wasn't ready in time. Not locale-specific: any config change Android doesn't find declared in android:configChanges (font scale, dark/light theme, multi-window resize...) recreates the Activity the same way. See revue technique point 63 (local file, not tracked) for the full diagnosis narrative across both attempts.
3 tasks
guyiome
added a commit
that referenced
this pull request
Sep 2, 2026
Patch release over v0.3.0: the new artist logo (#30), the camera preview/overlay freeze fix on in-app language change (#26), the security policy and issue templates (#24), and two dependency batches (#25, #31). Bumping versionName is not cosmetic here, it's required before tagging: the in-app update checker compares BuildConfig.VERSION_NAME against the latest GitHub release tag, so tagging v0.3.1 while the build still reported 0.3.0 would show a permanent, unclearable "update available" badge to users already running the newest build. No README sync this time, unlike the v0.3.0 preparation: that one tracked newly added features, whereas everything since is fixes plus the logo. Checked that no tracked doc still credits the previous placeholder logo. Verified the built APK actually reports the new version (aapt2 dump badging: versionCode='4' versionName='0.3.1') rather than assuming the edit took effect. Unit tests, debug build and lint all pass.
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.
Summary
setApplicationLocales()recreates the Activity (documented AppCompat behavior), butMainViewModelsurvives it.initializeTracking()'s idempotency guard didn't distinguish that from a repeat call on the same Activity, so the camera pipeline stayed bound to the dead lifecycle -- CameraX unbinds on aDESTROYEDowner, and on the OPTIMAL tier the ARCore session just stayed paused forever (its ON_START/ON_STOP observer only existed on the old owner). Only an app restart recovered.initializeTracking()now compares the incomingLifecycleOwnerby identity and calls the newrebindTrackingPipeline()when it differs -- rebuildsCameraController(genuinely lifecycle-bound) and re-registers the ON_START/ON_STOP observer, without redoing tier selection orFaceLandmarkerHelpersetup.ArCoreHeadPoseTrackeris deliberately never rebuilt on a rebind -- it'sApplication-scoped already, so nothing to rebind. An earlier version of this fix rebuilt it anyway and hit aGLSurfaceView$GLThread.surfaceCreated()NPE (confirmed viaadb logcat), sinceGLSurfaceView.setRenderer()must run before the view is attached to the window.android:configChanges(font scale, dark/light theme, multi-window resize...) recreates the Activity the same way.Test plan
./gradlew testDebugUnitTest assembleDebug lintDebuggreen.adb logcatacross two iterations rather than guessing.