DEV Community

David Njoroge
David Njoroge

Posted on

The Invisible War: Radios, Wakelocks, and the Evolution of Android Constraints

To a Kotlin developer, a network call is a suspend fun. To the hardware, it is a violent transition of state that consumes massive amounts of current. Understanding this is the difference between an app that is "efficient" and one that is "uninstalled."

1. The Physical Layer: The Radio State Machine (RRC)

The cellular radio is the most expensive component to keep active. It operates under the Radio Resource Control (RRC) protocol, which moves through three primary states.

DCH (Dedicated Channel)

  • The State: The radio is at full power, maintaining a dedicated high-speed link.
  • The Cost: ~200mA to 400mA.
  • The Complexity: This is the "high-performance" mode. The system enters this state the moment your code initiates a request.

FACH (Forward Access Channel)

  • The State: A lower-power intermediate state where the radio can send small control packets but doesn't have a dedicated high-speed pipe.
  • The Cost: ~40mA to 100mA.
  • The Trap: The "Tail Effect." When your data transfer finishes, the radio doesn't go to sleep. It stays in DCH for ~5 seconds, then drops to FACH for ~15 seconds. If your Kotlin code "pings" a server every 15 seconds, you effectively keep the radio in a high-power state indefinitely.

IDLE

  • The State: The radio is functionally off, only listening for "paging" signals.
  • The Cost: <2mA.
  • The Goal: This is where we want the device to be 95% of the time.

2. The Wakelock Battle: From King to Peasant

A Wakelock is a software handle that prevents the CPU from entering its deep-sleep C-states.

  • The Old Way (Android 1.0 - 5.1): Developers used PowerManager.PARTIAL_WAKE_LOCK. You could hold this forever. If your code was buggy, the phone stayed at 1GHz in the user's pocket until it died.
  • The Turning Point (Android 6.0 Marshmallow): Google introduced Doze Mode. For the first time, the system began stripping wakelocks. Even if your code held a wakelock, if the phone was stationary, the system would ignore you.
  • The Modern Era (Android 12 - 15): The system now uses Wakelock Quotas. If your app is in the "Rare" or "Restricted" standby bucket, the system provides a very small window (e.g., 10 minutes per day) for background execution. Once that's gone, your wakeLock.acquire() call succeeds in the code, but the kernel ignores it.

3. The Chronological Evolution of Constraints

How did we get here? Each Android version added a new layer of "complication" for developers:

Android Version Feature Technical Impact on Developers
4.4 (KitKat) Alarm Batching AlarmManager no longer fires exactly at the time requested; the system bundles them to save radio "tails."
6.0 (M) Doze Mode Introduced the "Stationary State." Network and Wakelocks are cut off entirely during sleep.
8.0 (O) Background Limits Killed background services. If the app isn't visible, you can't start a service. You must use JobScheduler.
9.0 (P) Standby Buckets Introduced AI categorization. Your app's ability to run code now depends on how often the user clicks your icon.
12/13 (S/T) Expedited Jobs New restriction on Foreground Services. You must now "earn" the right to run immediately using WorkManager's setExpedited(true).
14/15 (U/V) FGS Types Mandatory declaration of why you are running (e.g., location, health). If the code doesn't match the type, the OS kills the process.

4. The OEM Factor: The "Wild West" of Hardware

This is the part that frustrates developers the most. Google’s "Pure Android" (AOSP) rules are just the baseline. Major manufacturers (OEMs) add their own aggressive killers.

Samsung (One UI)

Samsung uses a feature called "App Power Management." It tracks apps that haven't been opened in 3 days and puts them into "Deep Sleep." In this state, the app cannot receive notifications, run jobs, or sync data. For a Kotlin developer, your WorkManager tasks simply disappear.

Xiaomi (MIUI / HyperOS)

Xiaomi is notoriously aggressive. Their "Battery Saver" defaults to "Restrict Background Activity" for almost all non-social media apps. They often ignore the Android standard Ignore Battery Optimizations intent, requiring users to manually dig through 5 layers of settings to allow an app to work.

Huawei & OnePlus

Historically, these OEMs used "Re-launch Killers." If a user swiped your app away from the "Recents" screen, the system would issue a force-stop, which clears all scheduled alarms and jobs. Only a manual click by the user could "revive" the app.


5. Navigating with Kotlin: The Modern Solution

As a Kotlin developer, you cannot fight these versions and OEM changes with old tools. You must use a "Declarative" approach.

A. The WorkManager + Coroutine Pattern

Do not use Thread or raw CoroutineScope. Use CoroutineWorker. It is the only API that is "aware" of Doze, Standby Buckets, and OEM restrictions.

class PowerAwareSync(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        // WorkManager handles the Wakelock and Radio state transitions here.
        // It waits for the "Maintenance Window" automatically.
        return try {
            val result = apiService.sync() 
            Result.success()
        } catch (e: Exception) {
            Result.retry() // Respects the system's "Backoff" power policy
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

B. Use "High Priority" FCM for "Waking Up"

If you need to bypass power saving (e.g., for a VoIP call or an emergency alert), you cannot do it from the device. You must send a High Priority FCM message from your server. This triggers a brief "Unrestricted" window where your Kotlin code is allowed to start a Foreground Service even on Android 14+.

C. Testing for the Real World

To see how your code behaves on different versions, use ADB to force the states:

  • Force Doze: adb shell dumpsys deviceidle force-idle
  • Test Standby Buckets: adb shell am set-standby-bucket <package> rare

Summary

The technical complication of power saving is that the OS no longer trusts the developer. Every Android update and every OEM tweak is a new wall. To navigate this, you must stop trying to "force" code to run and start "requesting" the system to run it when the Radio and CPU are already awake.

Top comments (0)