Back to articles

Performance & Security

OEM Battery Optimization Killing Your App — A Survival Guide for Samsung, Xiaomi, Huawei, and the ColorOS Family

Here’s the bug report I’ve seen variations of at least a dozen times: “The fitness app stops counting my steps after a few hours. I have to open it manually for it to resume. This only started happening on my new Samsung phone — my old Pixel was fine.” The user thinks the app is broken. The Pixel reviewers think the app is fine. The Samsung reviewers think the app is broken. The truth is the app is fine on AOSP and Samsung killed it — deliberately, by design, at the OS level — and you can’t fix it with a code change to your app.

Welcome to OEM fragmentation. Samsung, Xiaomi, Huawei, OPPO, Vivo, OnePlus, Realme, and others ship modified Android with aggressive battery optimization that kills legitimate background work. Foreground services die mid-execution, alarms get deferred for hours, push notifications arrive batched at random intervals, periodic WorkManager jobs simply don’t run. Google’s documentation describes how Android should behave; the OEM reality is meaningfully different.

This post is the field guide. We’ll walk through each major OEM’s specific battery-killing behaviors, why they exist, exactly how they manifest as bugs, what workarounds exist (with their costs), and the patterns that work across all OEMs. Examples come from a fitness tracker because step counting, heart-rate sync, and workout GPS tracking are exactly the use cases OEMs are most aggressive about killing — making fitness developers the experts on this problem.


Why OEMs Do This (and Why It Won’t Stop)

It helps to understand the motivation. The OEMs aren’t hostile; they’re responding to real user feedback.

Battery life is the single most-complained-about smartphone feature. Vanilla AOSP’s Doze and App Standby are conservative because Google has to support every app, including legitimate use cases like fitness tracking, navigation, and music. OEMs — especially in markets where flagship competition is fierce on battery life claims — ship more aggressive policies because they’ve found that most apps doing background work aren’t doing anything users care about. The OEM’s gamble: kill the long tail of background work, and 95% of users will never notice; the 5% with legitimate fitness/navigation apps will complain, but they’ll blame the app, not the OS.

That gamble has worked for a decade. Battery life ranks as a major selling point in benchmarks and reviews; the “app got killed” complaint goes to the app developer, not the OEM. There’s no incentive for them to stop. If anything, each OS version of OneUI, MIUI, EMUI, and ColorOS has gotten more aggressive over time, not less.

What this means for you: this isn’t a bug to file with the OEM, it’s a constraint to engineer around. The right framing is “my app needs to survive on hostile platforms,” not “the OEM should fix this.”


Samsung — OneUI’s “Sleeping Apps” List

Samsung is the largest non-Google Android vendor and the one most developers will encounter. OneUI’s battery optimization has several distinct mechanisms, each adding pressure:

Adaptive Battery. Samsung’s ML-driven system learns user behavior and progressively restricts apps the user doesn’t open frequently. After a few days of disuse, your fitness app moves from “Active” → “Sleeping” → “Deep Sleeping.” Each tier increases restrictions.

Sleeping Apps list. Apps in this state can’t run in the background, can’t fetch data, can’t fire alarms or schedule jobs except when manually opened. Push notifications still arrive but with significant delays.

Deep Sleeping Apps list. Even more aggressive. The app can’t run in the background period — it’s essentially as if uninstalled until the user opens it. Step counts pause, scheduled medication reminders don’t fire, sync stops cold.

Auto-add to Sleeping Apps can be turned on by the user (off by default on newer OneUI, but on by default in some regions). When on, any app the user hasn’t touched in 3 days automatically becomes a Sleeping App.

How this manifests as bugs in your fitness app:

// Step counter using a foreground service
class StepCounterService : LifecycleService(), SensorEventListener {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForeground(NOTIFICATION_ID, buildNotification())
        sensorManager.registerListener(
            this,
            stepSensor,
            SensorManager.SENSOR_DELAY_NORMAL
        )
        return START_STICKY
        // ❌ START_STICKY does not save you on Samsung
        // If the user’s app is in Deep Sleeping, the service is killed
        // and the system does NOT restart it — START_STICKY is overruled
    }

    override fun onSensorChanged(event: SensorEvent) {
        recordStep(event.values[0])
    }
}

The service starts fine. It runs while the user is actively using the phone or the app. Then the phone is idle for an hour, the user puts it in their pocket, Samsung’s policy moves your app toward Sleeping, the foreground service is silently terminated, and the step count freezes. The notification disappears. The user opens the app hours later, sees no steps recorded, files a bug report.

Mitigations:

1. Battery optimization exemption request. Show the user a one-time prompt asking them to disable battery optimization for your app. This works but is intrusive, requires user action, and the request must be justified for Play Store policy compliance — you need a legitimate use case (fitness tracking, alarm clock, accessibility tool).

// Request the user disable battery optimization for your app
private fun requestBatteryOptimizationExemption() {
    val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
    if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
        val intent = Intent(
            Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
            Uri.parse(“package:$packageName”)
        )
        startActivity(intent)
    }
}

// In manifest, declare you need this permission
<uses-permission android:name=“android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS” />
<!-- Note: Play Store reviews this declaration. Apps without legitimate
     background-work justification get rejected. -->

2. Foreground service type. Android 14+ requires explicit foreground service type. For step counting, use FOREGROUND_SERVICE_TYPE_HEALTH; for workout GPS, FOREGROUND_SERVICE_TYPE_LOCATION. Wrong type means the service can be killed without warning.

// In manifest
<service
    android:name=“.StepCounterService”
    android:foregroundServiceType=“health” />

// In code, also pass the type to startForeground
startForeground(
    NOTIFICATION_ID,
    notification,
    ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
)

3. Direct user to Samsung’s settings. Even with battery optimization exemption, OneUI’s Sleeping Apps list is separate. The user has to manually go to Settings → Apps → [your app] → Battery and select “Unrestricted.” Or remove your app from the Sleeping/Deep Sleeping lists if it’s already been added. There’s no API for this. You have to teach the user to navigate the OEM settings, ideally with screenshots or a video for the OneUI version they’re running.

The library AutoStarter attempts to deep-link into vendor-specific settings screens, but the intents change between OneUI versions and sometimes break entirely. Test on actual Samsung devices on actual recent OneUI versions; what worked six months ago may not work today.

4. The dontkillmyapp.com approach. The site dontkillmyapp.com publishes per-OEM, per-version step-by-step instructions. Many fitness apps link directly to the relevant page, telling users “please follow these instructions for your phone model.” It’s a community-maintained resource; better than nothing, far short of a real solution.


Xiaomi — MIUI’s Autostart and Background Restrictions

Xiaomi (and the related Redmi, POCO brands) ship MIUI with several layers that combine to create one of the most hostile environments for legitimate background work:

Autostart restriction (off by default). Apps cannot start themselves on boot or via Intents from other apps unless the user explicitly grants “Autostart” permission in Security app → Permissions → Autostart. Without this, your BOOT_COMPLETED receiver doesn’t fire, your scheduled WorkManager jobs may not run after reboot, deep links from notifications can fail to launch your app.

Background activity restriction. Even when running, apps in the background can be killed by MIUI’s “Battery saver” long before AOSP would. The user can manually set the app to “No restrictions,” but the default is aggressive.

Lock screen behavior. Apps can be configured per-app for whether they show on the lock screen and whether they can run while the screen is locked. Default for many apps: not allowed to run while locked. For a workout tracker recording your run with the screen off, this is fatal — the GPS service silently dies the moment the screen turns off.

Memory cleaning. MIUI aggressively cleans memory when switching between apps. If the user opens a heavy game, your fitness app’s foreground service is among the first to be killed to reclaim memory, even if the workout is in progress.

How this manifests in the fitness app:

// User starts a workout, GPS tracking begins
class WorkoutTrackingService : LifecycleService() {
    override fun onCreate() {
        super.onCreate()
        startForeground(
            NOTIFICATION_ID,
            buildNotification(),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
        )

        FusedLocationProviderClient(this).requestLocationUpdates(
            LocationRequest.Builder(2_000L).build(),
            locationCallback,
            mainLooper
        )
    }
    // On Pixel: runs reliably for hours during a workout.
    // On MIUI: depending on user settings, may be killed when:
    //   - Screen turns off (if app not allowed to run on lock screen)
    //   - User opens another app (memory cleaning)
    //   - User leaves phone idle for 10 minutes (battery restriction)
    //   - Phone reaches battery threshold (~20%, depending on settings)
}

Mitigations:

1. The MIUI permission gauntlet. A fitness app on MIUI realistically needs the user to grant: Autostart, Battery saver: No restrictions, Lock screen: Show, Background popup: Allow, Other permissions: Display pop-up windows when running in background. Six manual settings, none of which can be requested via standard Android APIs.

2. Detect MIUI and prompt. Detect that you’re running on MIUI (via Build.MANUFACTURER or system properties) and show a one-time onboarding flow specifically explaining MIUI’s restrictions. Many fitness apps do this.

fun isMiui(): Boolean =
    Build.MANUFACTURER.equals(“Xiaomi”, ignoreCase = true) ||
    Build.MANUFACTURER.equals(“Redmi”, ignoreCase = true) ||
    Build.BRAND.equals(“POCO”, ignoreCase = true) ||
    !TextUtils.isEmpty(getSystemProperty(“ro.miui.ui.version.name”))

private fun getSystemProperty(propName: String): String? {
    return try {
        @SuppressLint(“PrivateApi”)
        val systemProperties = Class.forName(“android.os.SystemProperties”)
        val get = systemProperties.getMethod(“get”, String::class.java)
        get.invoke(null, propName) as? String
    } catch (e: Exception) {
        null
    }
}

3. Test on actual MIUI devices. The behavior varies by MIUI version (12 vs. 13 vs. 14 vs. HyperOS), region (Chinese MIUI is more aggressive than Global MIUI), and individual device model. Emulators don’t reproduce MIUI behavior; you need real hardware. Firebase Test Lab does have some Xiaomi devices but coverage is incomplete.


Huawei — EMUI / HarmonyOS and the Protected Apps List

Huawei is the most complex case. Pre-2019, Huawei devices ran EMUI on Android with Google Play Services. Post-2019 trade restrictions, new Huawei devices in many markets ship without GMS — they have HMS (Huawei Mobile Services) instead, and now run HarmonyOS as the underlying OS in some configurations.

Three things make Huawei especially hostile:

1. Protected Apps list. Apps not on this list cannot run when the screen is off or after the phone is locked, period. The user must manually add your app via Settings → Battery → App launch → [your app] → Manage manually. Without this, your foreground service is killed within minutes of screen-off.

2. No GMS on newer devices. Push notifications via FCM don’t work. WorkManager’s scheduling fallbacks may behave differently. Play Integrity API isn’t available. Apps that assume GMS will mostly run, but anything depending on GMS-specific features quietly fails.

3. PowerGenie. A Huawei system process that aggressively kills background apps. Disabling it isn’t straightforward and isn’t guaranteed across versions.

For a fitness app on Huawei:

// Detect Huawei (EMUI or HarmonyOS)
fun isHuawei(): Boolean =
    Build.MANUFACTURER.equals(“HUAWEI”, ignoreCase = true) ||
    Build.MANUFACTURER.equals(“HONOR”, ignoreCase = true)
    // Honor was Huawei’s sub-brand, sold off in 2020 but inherits much
    // of EMUI’s behavior; treat similarly

// On Huawei, the standard battery optimization request goes to a different screen
// and many users report it doesn’t actually help — the Protected Apps list is
// the real lever, and there’s no Intent to launch it directly on most EMUI versions.

Mitigations:

1. HMS support. If your app targets Chinese or other markets where Huawei is dominant, integrate HMS Push Kit alongside FCM. The libraries can coexist; pick the right one at runtime based on which service is available.

fun pushTokenForDevice(): String? = when {
    isGmsAvailable() -> FirebaseMessaging.getInstance().token.await()
    isHmsAvailable() -> HmsInstanceId.getInstance(context).getToken(...)
    else -> null  // Some Chinese OEMs use unified push or vendor-specific systems
}

private fun isGmsAvailable(): Boolean =
    GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) ==
        ConnectionResult.SUCCESS

private fun isHmsAvailable(): Boolean =
    HuaweiApiAvailability.getInstance().isHuaweiMobileServicesAvailable(context) ==
        ConnectionResult.SUCCESS

2. User onboarding for Protected Apps. A Huawei-specific dialog explaining the manual setting is unavoidable. Many apps include screenshots of the EMUI flow because it’s genuinely confusing to navigate.

3. Accept the limitations. For some Huawei configurations, there is no engineering fix. Step counts will lag, GPS sessions will be cut short, sync will be intermittent. The product decision is whether to ship on Huawei at all, given the support cost.


OPPO, Vivo, OnePlus, Realme — The ColorOS Family

These OEMs (BBK Electronics group) share variants of ColorOS / OxygenOS / FuntouchOS / RealmeUI. They have similar restrictions to MIUI but with different settings names and locations.

Common patterns:

Auto-launch / Auto-start. Like MIUI, off by default. Required for boot receivers, scheduled work, push delivery reliability.

Background activity. Aggressive killing similar to MIUI. The user must allow background activity per-app.

Battery usage management. Three modes for each app: Optimized (default), Background restricted, Allow background activity. The default kills apps quickly.

OnePlus historically had a relatively clean OxygenOS but has progressively adopted ColorOS-style restrictions since the OPPO/OnePlus merger. Newer OxygenOS is essentially ColorOS with branding.

fun isColorOSFamily(): Boolean = when {
    Build.MANUFACTURER.equals(“OPPO”, ignoreCase = true) -> true
    Build.MANUFACTURER.equals(“Vivo”, ignoreCase = true) -> true
    Build.MANUFACTURER.equals(“OnePlus”, ignoreCase = true) -> true
    Build.MANUFACTURER.equals(“Realme”, ignoreCase = true) -> true
    else -> false
}

The mitigations are the same pattern: detect, prompt the user with vendor-specific instructions, and manage user expectations.


Pixel and Stock Android — The Baseline (and Even It Has Quirks)

Pixel runs closest to AOSP. Doze mode kicks in after the device is idle, screen off, and stationary for ~30 minutes; App Standby restricts apps the user hasn’t opened in days. These are documented, predictable, and respect FOREGROUND_SERVICE_TYPE_LOCATION / HEALTH properly.

Even here, gotchas exist:

Background location permission on Android 10+ is its own grant, separate from foreground location. Without it, your workout-tracking GPS service stops collecting locations once the app moves to background — even with the foreground service running. The user must explicitly grant “Allow all the time”; without it, only foreground location works.

Battery saver mode (system-level) when the user enables it manually or it triggers automatically (typically below 20% battery). Even foreground services with proper types get throttled. There’s no opt-out; you have to detect and notify the user.

Adaptive Battery and Adaptive Charging. Pixel’s ML system is gentler than Samsung’s but still progressively restricts unused apps. After 7+ days of disuse, your app may behave like an uninstalled-and-reinstalled fresh install for the next launch.

For your fitness app, Pixel is the easy case but not free. The same battery-optimization exemption flow you build for Samsung benefits Pixel users too.


The Patterns That Work Across All OEMs

Stepping back from per-OEM specifics, certain patterns are robust across the fragmentation:

1. Foreground services with explicit types — mandatory, not optional. FOREGROUND_SERVICE_TYPE_LOCATION for GPS, FOREGROUND_SERVICE_TYPE_HEALTH for sensor monitoring, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK for audio. Without the right type, the OEM scheduler treats your service as suspicious and kills it more aggressively.

2. Battery optimization exemption for genuine background work. One-time prompt during onboarding, with clear explanation of why it’s needed. Don’t request unless your app actually needs it — Play Store reviewers will reject apps requesting REQUEST_IGNORE_BATTERY_OPTIMIZATIONS without justification.

3. Persistent notification with real information. The notification for your foreground service should show useful state: current step count, workout duration, last sync time. This serves two purposes: justifies the foreground service to the user (so they don’t dismiss it), and visibly catches when the service has died (the notification disappears, the user notices).

4. Self-monitoring and recovery. Schedule a periodic WorkManager job (every 15 minutes is the floor) that checks “is my critical service running?” If not, restart it. WorkManager itself can be killed on hostile OEMs but it’s the most reliable backup; combine with AlarmManager exact alarms for critical reminders.

class ServiceWatchdogWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        // Check if our critical service is running
        if (!isStepCounterServiceRunning()) {
            // Try to restart it
            ContextCompat.startForegroundService(
                applicationContext,
                Intent(applicationContext, StepCounterService::class.java)
            )
            // Log to telemetry that watchdog had to restart
            Analytics.log(“watchdog_service_restart”, mapOf(
                “manufacturer” to Build.MANUFACTURER,
                “model” to Build.MODEL,
                “android_version” to Build.VERSION.SDK_INT
            ))
        }
        return Result.success()
    }
}

// Schedule the watchdog
val watchdog = PeriodicWorkRequestBuilder<ServiceWatchdogWorker>(15, TimeUnit.MINUTES)
    .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    “service_watchdog”,
    ExistingPeriodicWorkPolicy.KEEP,
    watchdog
)

The telemetry is critical: log every time the watchdog has to restart your service, with manufacturer / model / Android version. This builds an internal dataset of which OEM/version combinations are most hostile, informing where to invest in workarounds vs. accept limitations.

5. User-visible diagnostics. A “health check” screen in your app that tells the user when their phone’s battery optimization is fighting your app. “Your phone has restricted this app’s background activity. To get reliable step counting, follow these steps: ...” Better the user gets this diagnosis from your app than from a one-star review.

6. Server-side data backfill. When your app is killed mid-workout, the partial data is gone unless you persisted it. Periodically (every minute, every kilometer) sync workout state to a local database AND opportunistically to the server. When the app eventually restarts, it can recover state and even reconstruct the full workout from the partial sample.

7. The Health Connect API (where applicable). Google’s Health Connect provides a centralized data layer that the system maintains regardless of your app’s lifecycle. For fitness apps, writing step counts to Health Connect (and reading from sensors that other apps populate) is more resilient than maintaining your own continuous tracking. The OEM is less likely to kill the system data layer than your app’s service.


A Realistic Onboarding Flow for a Fitness App

For a production fitness app targeting global markets, the onboarding I’d ship:

1. Welcome screen (your normal product onboarding)
2. Permissions: foreground location, activity recognition, notifications
3. Battery optimization exemption (REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
4. Background location permission (separate grant on Android 10+)
5. ★ OEM-specific guidance (only shown if isHostileOem() returns true):
   - Detect Samsung / Xiaomi / Huawei / OPPO / Vivo / OnePlus
   - Show vendor-specific instructions with screenshots
   - Test the configuration: send a test alarm in 30 seconds, ask the user
     to confirm they received the notification on time
   - If test fails: deeper troubleshooting flow
6. ★ “Health check” persistent in-app reminder, accessible from settings,
   showing real-time status of permissions, battery optimization,
   foreground service health

Steps 5 and 6 are what separate apps that work for users with hostile-OEM phones from apps that don’t. The work is significant; the alternative is one-star reviews from a meaningful percentage of your global users.


When Not to Fight This

Honest framing: not every app needs to win this battle. If your app is a fitness tracker, navigation app, alarm clock, or anything else where background reliability is core to the value proposition, fighting OEM fragmentation is an essential investment. If your app is a content reader, social network, or productivity tool where the user opens it actively and you don’t need persistent background work, OEM aggressiveness mostly doesn’t affect you.

For the latter category, ride the standard AOSP behavior. Don’t request battery optimization exemption you don’t need (Play Store reviewers will reject the app). Don’t add OEM-specific onboarding flows that confuse users for no benefit. The simpler your background story, the less OEM fragmentation hurts you.

The worst pattern: cargo-culting OEM workarounds into apps that don’t need them. “The fitness apps do this so we should too.” You shouldn’t. The cost is real (engineering time, user confusion, potential Play Store policy issues) and the benefit is zero unless you genuinely need persistent background work.


The Reality Test — Your Device Lab

Before shipping any feature that depends on background work, the test matrix that catches the bugs:

  • Pixel (latest) — baseline AOSP behavior
  • Samsung mid-range (recent OneUI version, Sleeping Apps default settings) — the largest OEM
  • Xiaomi mid-range (recent MIUI / HyperOS, default settings, both Chinese and Global ROMs if relevant)
  • One Huawei device if your market includes it (with HMS, no GMS)
  • One ColorOS device (OPPO, Vivo, or OnePlus)
  • The cheapest device in your target market — mid-range with the OEM’s most aggressive default settings is where bugs manifest

Firebase Test Lab gives you remote access to a wide device fleet for automated testing. For manual scenario testing (what does the app do over a 6-hour workout when the screen turns off), real hardware in the office is unavoidable. A small device drawer with one device per major OEM is one of the best engineering investments a fitness/health/navigation team can make.

Telemetry from production tells you what to test. Crashlytics + custom events for “service was killed,” “watchdog restarted,” “notification not delivered within expected window” — segmented by manufacturer and model — will surface the worst combinations within a week of release.


Closing

OEM battery optimization is not a bug. It’s a real engineering constraint that varies by vendor, version, and region, and the only way to ship reliably across hostile OEMs is to know the patterns, detect the OEM at runtime, prompt users through the necessary settings with vendor-specific guidance, build self-monitoring with telemetry, and accept that some configurations have no clean engineering fix.

For a fitness app, this is half the engineering work after the core feature is built. For a banking or messaging app, it’s a fraction of that. Either way, knowing what each OEM does — and why your app dies on them in ways it doesn’t on a Pixel — is the difference between “works on my phone” and “works on the phones half the world actually uses.”

Next in this OEM Fragmentation cluster: push notifications across OEMs — why FCM “just works” on Pixel and fails or arrives hours late on others, and what to do about it. The mechanisms differ by vendor; the user complaint (“your app’s notifications are unreliable”) is universal. We’ll cover detection, vendor-specific push services, and the realistic SLA you can achieve.

Happy coding!

2 views · 0 comments

Comments (0)

No comments yet. Be the first to share your thoughts.