DEV Community

Super Funicular
Super Funicular

Posted on

Why Your "Old Phone Security Camera" Dies After 4 Hours (And How to Fix It on Modern Android)

The 90% Failure Mode Nobody Documents

If you've ever followed a "turn your old Android phone into a security camera" tutorial — including the use-case walkthrough I published a few days ago — you've probably hit the same wall. The phone records for a few hours. Maybe overnight if you're lucky. Then you check it in the morning and the recording is dead, the service is gone, and there's no error, no notification, no nothing.

Welcome to the most undocumented production problem in modern Android: keeping a foreground service alive on real OEM-skinned devices in 2026.

This post is the technical companion to the use-case article. If that one was "here's what you can do with your old phone," this one is "here's why every other tutorial that says you can do this still doesn't actually work after 24 hours, and what I had to do in Background Camera RemoteStream to fix it."

The Three-Layer Gauntlet

Every always-on Android service has to survive three independent layers of aggression before it can be considered "production-grade." Most "old phone as a camera" tutorials skip layers two and three entirely. That's why those tutorials work on your dev device for a weekend and then die.

Layer 1: AOSP — Doze and App Standby

This is the layer almost everyone documents because it's the only one Google publishes specs for.

A vanilla Pixel running stock Android will, after about an hour of inactivity, enter Doze mode. Background work gets batched, network access is suspended, and most importantly for our use case, unconfigured foreground services can be aggressively pruned.

The fix at this layer is straightforward and well-known:

// In AndroidManifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<service
    android:name=".RecordingService"
    android:foregroundServiceType="camera"
    android:exported="false" />
Enter fullscreen mode Exit fullscreen mode
// In RecordingService.kt
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val notification = buildPersistentNotification()
    startForeground(
        NOTIFICATION_ID,
        notification,
        ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
    )
    // ... start CameraX recording session ...
    return START_STICKY
}
Enter fullscreen mode Exit fullscreen mode

The critical detail most tutorials miss: FOREGROUND_SERVICE_TYPE_CAMERA is mandatory on Android 14+. If you don't set it, the system will allow the service to start but will silently restrict it as soon as the app loses visibility. You'll see your service running in adb shell dumpsys activity services for the first hour, then it's just gone.

Layer 2: OEM battery managers — the real boss fight

Stock AOSP is the easy part. The hard part is everything that ships on top of AOSP.

Every major Android OEM has its own additional battery-management layer, and these layers do not respect the same contracts that AOSP foreground services do. Specifically:

  • Xiaomi MIUI has "MIUI Optimization" which will kill foreground services it considers "non-essential," with essential being defined by an internal whitelist that you, as a third-party developer, cannot get on.
  • Samsung One UI has "Sleeping apps" and "Deep sleeping apps" which auto-add anything the user hasn't opened in 3 days. Your camera app is "anything the user hasn't opened" the moment they lock the phone and walk away — which is the entire point of the use case.
  • Huawei EMUI has "Protected apps" — any app not in the protected list gets killed when the screen turns off, foreground service or not.
  • Oppo / OnePlus ColorOS does similar with "Auto-launch" and "Background freeze."
  • Vivo Funtouch runs "iManager" which is the most aggressive of all of them — it will kill foreground services even during active recording if the screen has been off for several hours.

There is no programmatic way to whitelist your app on most of these. The user has to do it manually, in a settings screen that's buried five levels deep and is named differently on every OEM.

What you can do programmatically is detect the manufacturer and route the user to the correct battery-optimization screen with an explanatory in-app modal:

fun openBatteryOptimizationSettings(context: Context) {
    val manufacturer = Build.MANUFACTURER.lowercase()
    val intent = when {
        manufacturer.contains("xiaomi") -> Intent().apply {
            component = ComponentName(
                "com.miui.securitycenter",
                "com.miui.permcenter.autostart.AutoStartManagementActivity"
            )
        }
        manufacturer.contains("samsung") -> Intent().apply {
            component = ComponentName(
                "com.samsung.android.lool",
                "com.samsung.android.sm.battery.ui.BatteryActivity"
            )
        }
        manufacturer.contains("huawei") -> Intent().apply {
            component = ComponentName(
                "com.huawei.systemmanager",
                "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"
            )
        }
        manufacturer.contains("oppo") || manufacturer.contains("oneplus") ->
            Intent("com.coloros.safecenter.permission.startup.StartupAppListActivity")
        manufacturer.contains("vivo") -> Intent().apply {
            component = ComponentName(
                "com.iqoo.secure",
                "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity"
            )
        }
        else -> Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
            data = Uri.parse("package:${context.packageName}")
        }
    }
    try {
        context.startActivity(intent)
    } catch (e: ActivityNotFoundException) {
        // Fall back to generic battery-optimization screen
        context.startActivity(
            Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Component names change between OS versions. The Vivo and Xiaomi components in particular have shifted at least three times in the last two years. Maintain this map. There is no shortcut.

Layer 3: The user's own behavior

The third layer is the one no tutorial mentions because it's not technical. It's the user.

After they install the app, the user will:

  1. Swipe it away from recents (which on most OEM skins is treated as a force-stop).
  2. "Clean up RAM" via the OEM's bundled cleaner, which kills the service.
  3. Reboot the phone and forget to re-launch the app, because the user-facing activity is no longer their focus — they expected it to "just work."

You handle (1) and (2) with the OEM whitelisting from layer 2 (most OEM cleaners respect the auto-start whitelist). You handle (3) with RECEIVE_BOOT_COMPLETED and a tiny boot-receiver that re-arms the service:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // Only auto-restart if the user previously enabled "auto-start on boot"
            // from a first-class in-app toggle. Do not auto-start by default —
            // it's a privacy red flag and a Play Store policy issue.
            val prefs = context.getSharedPreferences("recording", MODE_PRIVATE)
            if (prefs.getBoolean("auto_start_on_boot", false)) {
                val serviceIntent = Intent(context, RecordingService::class.java)
                ContextCompat.startForegroundService(context, serviceIntent)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
<receiver
    android:name=".BootReceiver"
    android:exported="true"
    android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>
Enter fullscreen mode Exit fullscreen mode

The opt-in toggle is non-negotiable. Auto-starting a camera service on boot without explicit user consent is both a privacy concern and a Play Store policy violation.

The Embedded HTTP Server Question

The other technical question I get every time the use-case article goes around: how does the "browser-based remote view" work? Doesn't that require server infrastructure?

No — and that's actually the most architecturally interesting part. The phone is the server.

Background Camera RemoteStream embeds a Ktor server that listens on the device's LAN IP. When you open 192.168.1.105:8080 in any browser on the same WiFi, you're hitting an HTTP server running inside the Android process, served by a CIO engine, with a tiny single-page app that pulls MJPEG frames from the camera pipeline.

A simplified version:

class LanWebServer(private val port: Int = 8080) {
    private var server: NettyApplicationEngine? = null

    fun start(frameSource: () -> ByteArray) {
        server = embeddedServer(Netty, port = port) {
            install(CORS) {
                anyHost() // LAN-only, but explicit for browser fetch
            }
            routing {
                get("/") {
                    call.respondText(htmlIndex, ContentType.Text.Html)
                }
                get("/stream") {
                    call.response.header(
                        "Content-Type",
                        "multipart/x-mixed-replace; boundary=frame"
                    )
                    call.respondTextWriter {
                        while (true) {
                            val frame = frameSource()
                            write("--frame\r\n")
                            write("Content-Type: image/jpeg\r\n")
                            write("Content-Length: ${frame.size}\r\n\r\n")
                            // ... write frame bytes ...
                            flush()
                            delay(33) // ~30fps
                        }
                    }
                }
            }
        }.start(wait = false)
    }
}
Enter fullscreen mode Exit fullscreen mode

Three things to note:

  1. No external network exposure by default. The server binds to the device's LAN IP. Nothing is reachable from outside the WiFi unless the user explicitly tunnels in (Tailscale / WireGuard, recommended).
  2. No cloud relay. The browser pulls frames directly from the phone over the LAN. This is the entire reason the privacy story actually holds — there is no third party in the data path.
  3. CORS is permissive but the binding is restrictive. The combination is what makes the "no app install on the viewing device" feature work without compromising the local-first guarantee.

What This Adds Up To

The reason "old phone as security camera" feels like a half-broken hack in 2026 isn't the hardware. The hardware has been ready since 2017. The reason is that solving layers 2 and 3 above requires per-OEM, per-OS-version maintenance that the open-source / weekend-tutorial ecosystem hasn't had the bandwidth to do.

That's the gap Background Camera RemoteStream tries to close. Not by inventing anything novel, but by paying the OEM-compatibility tax that actually makes the use case ship.

If you're building anything else in this space — a custom DVR, a baby monitor, a self-hosted Ring replacement — these three layers are what you'll be fighting with. Hope the map saves someone the months it took to draw.

For the use-case-level "what can I do with this" framing, see the original article: Turn Your Old Android Phone Into a Free Security Camera (No Subscription Required).


Comments open — happy to share the specific component names that have changed in MIUI 14 and One UI 7, or to compare notes if you've shipped a similar foreground-service Android app.

Top comments (0)