Skip to content

Fix camera capture crash on devices with lower RAM - #4635

Open
xkello wants to merge 1 commit into
masterfrom
bugfix/camera-oom-kill
Open

Fix camera capture crash on devices with lower RAM#4635
xkello wants to merge 1 commit into
masterfrom
bugfix/camera-oom-kill

Conversation

@xkello

@xkello xkello commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Description

On some devices with heavy OEM camera stacks (confirmed on Motorola, 4GB RAM), tapping "add photo" -> camera reliably crashed the app. Root cause, confirmed via multiple adb logcat captures: launching the OS camera app spikes system-wide memory pressure, and Android's lowmemorykiller kills our own backgrounded process moments later - before the user can even take the photo. It's a silent OS process kill, not a Java exception (no FATAL EXCEPTION in any capture).

Fixes: #2784

What changed

  • CameraActivity.java
    • Explicit grantUriPermission()/revokeUriPermission() for the capture FileProvider URI - some OEM camera apps (confirmed: Motorola) don't reliably honor FLAG_GRANT_* flags when the URI is passed via EXTRA_OUTPUT rather than setData().
    • onSaveInstanceState()/resume branch in onCreate() - if the process is killed and recreated by Android to redeliver a pending camera result, resume from the saved targetPath/cameraFile instead of relaunching the capture or losing track of the file.
    • Null-guard on cameraFile in onActivityResult() - fails cleanly instead of a NullPointerException if state wasn't restored.
  • CameraForegroundService.java (new)
    • Started right before the camera intent, stopped as soon as a result comes back. Keeps the process in a protected priority tier (oom_score_adj) for the duration the OS camera is in front, making the kill significantly less likely in the first place.
  • AndroidManifest.xml
    • Registers CameraForegroundService (foregroundServiceType="shortService", stopWithTask="true").

Behaviour

Before: on affected devices, opening the camera from a form would kill the app outright - the project reloads from scratch and the in-progress edit is lost.

After: the process is kept in a protected priority tier for as long as the camera is open, verified directly via adb - oom_score_adj stays at 0/50 during the camera wait, versus the 700 tier every observed kill happened at. If the process is still killed under extreme memory pressure, CameraActivity now resumes and completes the capture correctly instead of crashing or silently corrupting the saved file.

Before fix: #2784 (comment)
After fix:

screen-20260804-164203.mp4

TLDR @Withalion

Camera crash on some devices = OS killing our process under memory pressure while the OEM camera app is open, not a code bug. Fix is two-layered: CameraForegroundService reduces the odds of the kill (confirmed via oom_score_adj measurement, not just crash/no-crash testing), and CameraActivity's save/resume logic makes it non-destructive if the kill happens anyway. No new user-facing UI - the foreground service's notification is intentionally not requested/shown (POST_NOTIFICATIONS not requested), since the user doesn't need to know this is happening.

@xkello xkello added this to the 2026.4.0 milestone Aug 4, 2026
@xkello
xkello requested a review from Withalion August 4, 2026 14:50
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30919175200

Coverage increased (+0.01%) to 59.147%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 2 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

2 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
mm/app/position/providers/simulatedpositionprovider.cpp 1 91.67%
mm/core/merginuserinfo.cpp 1 79.08%

Coverage Stats

Coverage Status
Relevant Lines: 15678
Covered Lines: 9273
Line Coverage: 59.15%
Coverage Strength: 98.14 hits per line

💛 - Coveralls

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

📦 Build Artifacts Ready

OS Status Build Info Workflow run
macOS Build Build failed or not found. #7129
linux Build 📬 Mergin Maps 71551 x86_64 Expires: 02/11/2026 #7155
win64 Build Build failed or not found. #6331
Android Build Build failed or not found. #8440
iOS Build Build failed or not found. #9381

@Withalion Withalion removed this from the 2026.4.0 milestone Aug 10, 2026

@Withalion Withalion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no changes for AndroidManifest. The service won't get created without it.
Please rebase on Master as well

}

takePictureIntent.putExtra("__RESULT__", "takePictureIntent__RESULT__");
startForegroundService(new Intent(this, CameraForegroundService.class));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the CameraForegroundService be started before this one gets created in AndroidUtils::callCamera?

Comment on lines +200 to +209
if (cameraFile != null) {
try {
Uri photoURI = FileProvider.getUriForFile(this,
"uk.co.lutraconsulting.fileprovider", cameraFile);
revokeUriPermission(photoURI,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (IllegalArgumentException e) {
// cameraFile isn't covered by file_paths.xml -- nothing was granted, nothing to revoke
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this as well

Comment on lines +24 to +29
* Runs in the app's default process (unlike PositionTrackingService, which deliberately
* runs in its own :trackingThread) for as long as CameraActivity is waiting on the external
* camera app, to raise this process's priority and make it much less likely to be picked by
* the OS's low-memory killer while the (often heavy) OEM camera stack is in the foreground.
* This is a mitigation, not a guarantee -- under severe enough memory pressure the process can
* still be killed, which is what CameraActivity's saved-instance-state resume handles.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Runs in the app's default process (unlike PositionTrackingService, which deliberately
* runs in its own :trackingThread) for as long as CameraActivity is waiting on the external
* camera app, to raise this process's priority and make it much less likely to be picked by
* the OS's low-memory killer while the (often heavy) OEM camera stack is in the foreground.
* This is a mitigation, not a guarantee -- under severe enough memory pressure the process can
* still be killed, which is what CameraActivity's saved-instance-state resume handles.
* Runs in the app's default process for as long as CameraActivity is waiting on the external
* camera app, to raise this process's priority and make it much less likely to be killed while the camera
* is in the foreground.
* This is a mitigation, not a guarantee -- under severe enough memory pressure the process can
* still be killed, which is what CameraActivity's saved-instance-state resume handles.

.setContentIntent(pendingIntent)
.build();

if (Build.VERSION.SDK_INT >= 35) { // Android 15 (Vanilla Ice Cream) -- FOREGROUND_SERVICE_TYPE_SHORT_SERVICE

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (Build.VERSION.SDK_INT >= 35) { // Android 15 (Vanilla Ice Cream) -- FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
if (Build.VERSION.SDK_INT >= 35) {

Comment on lines +68 to +71
// Not START_STICKY: if this service alone gets killed there is nothing useful to resume --
// it holds no state, it only exists to keep the process's priority elevated while
// CameraActivity waits on startActivityForResult().
return START_NOT_STICKY;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Not START_STICKY: if this service alone gets killed there is nothing useful to resume --
// it holds no state, it only exists to keep the process's priority elevated while
// CameraActivity waits on startActivityForResult().
return START_NOT_STICKY;
// if this service alone gets killed there is nothing useful to resume, it only exists to
// keep the process's priority elevated while CameraActivity waits on startActivityForResult().
return START_NOT_STICKY;

Comment on lines +96 to +105
// Explicitly grant URI permission to every app that can resolve this intent.
// Required because the URI is passed via EXTRA_OUTPUT rather than setData(),
// and some OEM camera apps (confirmed: Motorola) don't reliably honor the
// FLAG_GRANT_* flags in that case.
List<ResolveInfo> resolvedActivities = getPackageManager()
.queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resolvedActivities) {
grantUriPermission(resolveInfo.activityInfo.packageName, photoURI,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mixed feelings about this and it feels sloppy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Adding photo attachment from camera crashes Input

2 participants