Most Android camera apps quietly die a few minutes after you lock the screen. No crash, no error in logcat โ the frames just stop. I spent a year figuring out why, and it turns out you're not fighting one bug. You're fighting four power-management systems at once.
Here's the whole map.
System 1: the foreground-service lifecycle. If your camera capture lives in a normal Service, the OS can kill it for memory pressure within seconds of screen-off. The fix is a foreground service with type "camera|microphone" and a persistent, non-dismissible notification. On API 34 if you skip the type, startForeground() throws and the service dies the instant you attach the camera.
System 2: Doze. Once the phone has been still and unplugged a while, the system batches background work and can revoke wakelocks held without the right foreground-service type. You need the correct type AND a PARTIAL_WAKE_LOCK with a readable tag.
System 3: App Standby buckets. Even past Doze, the OS sorts your app active โ working set โ frequent โ rare โ restricted based on usage. A "rare" app gets camera and network capabilities curtailed. The lever is the same visible notification โ as long as the user can see it, the bucket stays warm.
System 4: OEM patches. This is the layer AOSP docs never warn you about. MIUI, EMUI, One UI, ColorOS, FunTouchOS, OxygenOS โ every one ships a vendor battery optimizer that kills background camera apps even when AOSP's own rules say you're allowed to run. You can't opt in programmatically. The best you can do is detect the manufacturer and deep-link the user to the right autostart screen.
If you only fix #1, your session lives 10 minutes on a clean Pixel and dies in 3 on a Xiaomi. Fix #1โ3 but not #4 and you ship a 1-star review storm from every Chinese OEM.
The single most important architectural decision:
๐ The Activity does NOT own the camera session.
Every Camera2 tutorial opens the camera in the Activity because it's showing you a viewfinder. But with the screen off there is no viewfinder โ so that ownership pattern is exactly wrong. Instead, a foreground Service owns the CameraDevice, the CaptureSession, the encoder, the wakelock, and the HTTP server. The Activity is a thin client that just binds for status. When the screen turns off, the Activity goes through onPause โ onStop and nothing happens to the session, because it never owned it.
Opening the camera without a SurfaceView: send output straight to surfaces that don't need a visible window โ MediaRecorder.getSurface() for the MP4, and an ImageReader surface (YUV_420_888) for the frame stream. No preview target at all. Build the SessionConfiguration with just those two and it runs for hours.
Two things the tutorials never cover for long recordings:
๐งญ Orientation with no Activity. You can't trust display.rotation from a Service (some Samsung/Xiaomi builds just return ROTATION_0), and OrientationEventListener stops firing when the screen goes off. What works: read TYPE_ROTATION_VECTOR from SensorManager once at record start, derive a stable orientation, bake it in with MediaRecorder.setOrientationHint() โ and never change it mid-session. It's a one-shot tag in the MP4 metadata, not a live track.
๐ Exposure & focus across hours. A security or baby cam records from afternoon into deep night โ lighting shifts 6โ8 stops. Default auto-exposure pushes shutter to hundreds of ms and you get motion-blurred mush. Cap exposure time at ~1/30s, let ISO ride to compensate. Then detect the scene type ONCE: static mount โ lock AF and white balance; actively-filmed โ keep continuous-video AF. Detect once, commit. Skip this and the footage looks fine for ten minutes and progressively wrong over three hours.
Surviving the OEM kill is two moves: request battery-optimization exemption (the user confirms, but a long-running screen-off camera genuinely qualifies), and detect Build.MANUFACTURER to deep-link the exact autostart Activity per vendor, wrapped in try/catch with a generic fallback.
What this bought us, measured: continuous 1080p for 8h screen-off on a Pixel 6. On a Xiaomi with autostart enabled, about the same. Without autostart, the session died at ~47 min on average โ which matched the review pattern exactly. Shipping the OEM autostart wizard cut "stopped recording overnight" emails to roughly 1/100th.
The deliberate trade-off underneath all of it: no cloud, no account, no background uploads. The recording stays on the device; LAN viewing goes through an embedded MJPEG web server. The moment you add an authenticated cloud broker you inherit the failure mode that made 378 brands of cameras watchable in a single key extraction. The point is to be structurally incapable of being that breach.
Full version with all the Kotlin:
dev.to/superfunicular/cameraโฆ
Try the app:
play.google.com/store/apps/dโฆ