fix(android): don't crash when stop() lands after the activity saved its state - #423
Conversation
|
@ryaa Do you have time to have a quick look on this? |
|
Hi @abumalick — before reviewing this PR, I had already implemented the fix for #424 and opened #425. After reviewing #423, I realized that #425 overlaps with part of the work here, so I would like to coordinate the two changes before either approach is finalized. As part of its preview-session lifecycle handling, #425:
I tested the original background/rotate/return scenario from this PR on the #425 branch and could no longer reproduce the released-camera crash. This is consistent with #425 superseding #315 and the However, #425 does not address the separate To verify that path independently, I temporarily delayed only the existing
This reliably reproduced the exact process-fatal exception on a Samsung SM-S731N: The process was terminated, confirming that the Would you be able to review #425, especially its pause/detach lifecycle handling? Once #425 has been reviewed and merged, I think either of these approaches would work:
The first option preserves this PR’s existing discussion and history. The second may provide a cleaner standalone diff. I am happy to retest the final version on the same physical device whichever approach you prefer. |
|
Thanks @ryaa for the thorough check, and for confirming the state-loss path independently. Agreed that #425 supersedes the #425 on the device from the original reportRedmi Note 8, now on Android 16 (it has been reflashed since the PR description). Example app built from each branch, driven over adb, rear camera, Steps: start the preview in portrait → rotate to landscape → HOME → return → Stop. The launcher on this ROM is portrait-only, so pressing HOME delivers a landscape → portrait configuration change to the paused
Also on #425: start → rotate to landscape and back with the preview live → capture → stop. The preview restarted on each resize, the capture after the rotations resolved, and stop was clean. Pause/detach reviewI read
I didn't find a gap in that path. One minor thing the device logs showed: every normal
FYI, separate from this PR
This needs |
|
Thanks again for the thorough review, @abumalick. I’ve now addressed the normal-stop logging issue you identified in #425.
I verified the updated implementation on the same physical device with a normal start → first frame → stop sequence. The camera closed cleanly, and the misleading startup-failure messages were no longer emitted. The automated build, lint, and Android test suite also pass. The separate |
|
Thanks @ryaa, the
I ran On the same Redmi Note 8 (Android 16), with the example app built from
From my side, #425 is ready to merge. Once it's in |
|
Thanks, @abumalick, for the thorough review and additional device testing. I’ve now merged #425 into You can proceed with rebasing #423 onto the updated Once your changes are completed, reviewed, and merged, we can plan to release v8.0.2 with all these fixes. Thanks again for helping get these Android lifecycle fixes landed! |
…its state stop() posts its whole body to the UI thread, so the fragment transaction can commit after the host activity has saved its state. commit() then throws "IllegalStateException: Can not perform this action after onSaveInstanceState" from the plugin's own UI-thread runnable, where no try/catch around the JS call can catch it, and the app dies. The transaction only removes the preview fragment, so there is nothing worth preserving across a process death; commitAllowingStateLoss() is the correct commit here.
6f2e573 to
6d14887
Compare
|
Rebased onto Re-tested on the Redmi Note 8 (Android 16) with the example app built from the rebased branch:
Ready for your review, @ryaa. |
ryaa
left a comment
There was a problem hiding this comment.
PR Review Summary — 2026-09-25
Reviewed PR #423 at 6d14887, following the merge of #425. The remaining change is focused on preventing an Android crash when stop() removes the preview fragment after the activity has saved its state. Using commitAllowingStateLoss() for this removal addresses that path without duplicating the preview lifecycle fixes already merged in #425.
Validation
- Tested on a physical Samsung SM-S731N: start → live preview → photo → stop; rotation followed by capture and stop; background/return followed by stop; and another start/capture/stop cycle. All passed without a crash.
- To reproduce the timing issue reliably, temporarily delayed the fragment-removal commit in
stop()by five seconds on the main thread, then backgrounded the app before the delayed commit ran. Logging checked whether the activity's fragment state had been saved and whether the removal transaction actually executed. - With the original
commit()call, this test reproduced the process-fatalIllegalStateException: Can not perform this action after onSaveInstanceState. - Repeating the same test with this PR’s
commitAllowingStateLoss()change confirmed that the preview fragment was successfully removed after the activity had saved its state, without a crash. This verifies that the fix handles the reproduced failure condition. - Removed all temporary diagnostic code and retested the unmodified PR successfully.
- Reported automated validation: 76 passing unit tests and a successful Android clean build/test run. These checks supplement the device testing; they do not directly exercise the FragmentManager timing race.
No blocking findings remain for this change. The separate startCamera() saved-state path remains outside this PR's scope.
We can then proceed with planning v8.0.2 to include this fix alongside #425.
Fixes an Android crash that fires when
CameraPreview.stop()runs while the app is being backgrounded.Found while chasing the top crash in a Capacitor app that keeps a
CameraPreviewon a photo-capture screen.This PR originally also fixed
Camera is being used after Camera.release()inPreview.java. #425 rewrote the preview session lifecycle and fixes that crash (and supersedes #315), so those changes were dropped when rebasing ontomaster. What remains is the one-linestop()fix.IllegalStateException: Can not perform this action after onSaveInstanceStatestop()posts its entire body to the UI thread, so the fragment transaction can commit after the activity has saved its state:Because it is thrown on the plugin's own posted runnable, no
try/catcharound the JSCameraPreview.stop()call can catch it — the app dies.The trap this creates is worth spelling out, because it is the natural thing to try: the obvious app-side workaround is to listen for
appStateChangeand callstop()when the app is backgrounded, so the preview never survives into the background. That makes this crash more likely, not less — it movesstop()to exactly the moment the activity is saving its state. On the device below, a build doing that crashed on the first attempt (2/2), while the same build without the listener did not reproduce it in 4 attempts.The transaction only removes the preview fragment, so there is no state worth preserving across a process death.
commitAllowingStateLoss()is the appropriate commit.Testing
Redmi Note 8, Android 16, example app built from this branch (rebased on
masterat 971ab69):./gradlew testDebugUnitTest: 76 tests, 0 failures.Notes
startCamera()has the samecommit()pattern. It needsstart()to race with backgrounding, which is much rarer, so it is deliberately left out of this PR.