Battery work comes down to two things, waking the device less often, and doing the unavoidable work while the CPU, the radio or the GPS is already up for something else. Everything below is a way of doing one of those, and the order matters, because you measure before you change anything.
Measure first, or you will optimise the wrong thing.
- The Power Profiler in Android Studio. Record a system trace and read the Power Rails track, which shows the on device power monitor broken out per subsystem, cellular, display, GPS, WLAN, CPU and GPU. It is device level rather than app level, so use it as an A and B test between two builds of your own.
- Battery Historian. The classic tool for turning a
batterystats dump into a timeline of what woke the device. Google no longer actively maintains it and now points at system tracing, the Power Profiler and the Macrobenchmark power metric instead, so name it as the thing you know and say what replaced it.
- Android vitals in the Play Console. It flags excessive wakeups, stuck partial wake locks and excessive background Wi-Fi scans across real installs. That is field data from users, which no local profiling run gives you.
- The battery screen in system settings. Per app usage over the last day, the same number the user sees before they uninstall you. It is crude and it is the one that gets you a one star review.
Background work is where most of the damage is.
WorkManager with constraints. Deferrable work should say what it needs, an unmetered network, charging, battery not low, device idle, and let the system pick the moment. Constrained work batches with everyone else's instead of waking the device on your own schedule.
- Expedited work for the rare urgent case.
setExpedited() is for short, user initiated things like sending a chat message or completing a payment. It runs against a quota tied to your standby bucket, so it is an exception, not a default.
- Batch and defer. Collect analytics events and flush them in one go. Prefetch on Wi-Fi and charging. Five requests sent together cost far less than five spread across an hour, because each one pays to bring the radio out of idle.
- Exact alarms are restricted now. From Android 12 an exact alarm needs
SCHEDULE_EXACT_ALARM, and from Android 14 that permission is not pre granted to new installs, so you have to check canScheduleExactAlarms() and fall back. USE_EXACT_ALARM is auto granted but reserved for alarm clock and calendar style apps. If your work is not clock accurate by nature, it belongs in WorkManager.
- Doze. Once the device is unplugged, still and screen off, network access is suspended, wake locks are ignored, jobs and syncs are frozen, and alarms are deferred to a maintenance window. Fighting it does not work,
setExactAndAllowWhileIdle() still cannot fire more than once every nine minutes.
- App Standby buckets. The system sorts your app into active, working set, frequent, rare or restricted based on how the user actually uses it, and each step down throttles jobs and alarms harder. In the restricted bucket you get roughly one alarm and one batched job window a day. The fix is not a trick, it is being an app the user opens, and never spamming notifications to farm promotion.
- Battery optimisation exemptions are a last resort. Asking the user to exempt you is allowed only when Doze genuinely breaks a core function, a safety app or a companion device connection. If FCM can do the job, Play policy says use FCM.
Location is the single most expensive API most apps touch.
- Use the fused location provider, not the raw framework API. It blends GPS, Wi-Fi, cell and the motion sensors, which is both more accurate and cheaper than driving GPS yourself.
- Pick the lowest priority that works.
PRIORITY_BALANCED_POWER_ACCURACY is the right default and rarely touches GPS. PRIORITY_HIGH_ACCURACY is for a map on screen with the user watching it. PRIORITY_LOW_POWER gives city level accuracy for almost nothing.
- Ask for the longest interval you can live with. Pass the largest value into
setIntervalMillis(), and set setMaxUpdateDelayMillis() several times larger so the system batches updates and delivers them together instead of waking you each time.
- Geofences instead of polling. If you only care about arriving somewhere, register a geofence and let the platform tell you. Set the notification responsiveness to five minutes or more, which is a large power win for a small latency cost.
- Passive location.
PRIORITY_PASSIVE piggybacks on locations other apps already requested, so it costs you nothing extra.
- Foreground service only while it is genuinely needed. A tracking session runs in a foreground service with the
location type and its FOREGROUND_SERVICE_LOCATION permission, and stops the moment the trip ends. Continuous background tracking also needs ACCESS_BACKGROUND_LOCATION, which on Android 11 and up the user has to grant from settings, so most apps should not want it.
Network is the other big radio consumer.
- Batch and compress. Fewer, larger, gzip compressed requests beat many small ones. Keep payloads small so the radio spends less time at full power.
- Never poll on a keep alive timer. A background poll every few minutes is the classic drain. Let the server tell you instead.
- Prefer FCM to wake the app. A high priority message gets you temporary network access and a wake lock even in Doze, and it costs you nothing while nothing is happening, because the OS owns the one connection for every app on the device.
- Defer big transfers to unmetered and charging. Video prefetch, model downloads and backups are exactly what
NetworkType.UNMETERED plus setRequiresCharging(true) exist for.
- Back off exponentially on failure. A tight retry loop against a dead endpoint will flatten a battery in an afternoon. Add jitter so your whole install base does not retry in lockstep.
Wake locks and sensors are the classic interview follow up.
- Avoid a partial wake lock if you possibly can. A wake lock held past the operation that needed it is the most direct battery bug there is, and Android vitals reports it by name.
WorkManager and a foreground service both keep the CPU up for you without you owning a lock.
- If you must hold one, bound it. Acquire it around the exact operation, release it in a
finally, and use a timeout so a crashed code path cannot leave it held.
- Batch sensor delivery. Pass a large
maxReportLatencyUs to registerListener() so the sensor hub buffers samples and wakes the application processor once instead of continuously.
- Unregister on the way out. Sensors, location updates, camera and Bluetooth scans all get released in
onPause() or onStop(), not in onDestroy(), because a backgrounded activity may never see onDestroy().
Rendering costs real power too, and candidates usually forget it.
- Dark theme on OLED. Black pixels are unlit pixels, so a dark surface measurably reduces display draw on OLED panels, and the display is often the largest single consumer on the device.
- Cut overdraw. Every pixel painted more than once is GPU work with no visual result. Remove redundant backgrounds and flatten the hierarchy.
- Stop animations nothing can see. Pause looping animations, video and Lottie when the view scrolls off screen or the app goes to the background.
- Drop the frame rate for static content. A page of text does not need 120Hz, and asking for a lower rate on a mostly still screen saves both GPU and display power.
Finally, react to the device getting hot.
- The thermal API.
PowerManager.getCurrentThermalStatus() and addThermalStatusListener() report THERMAL_STATUS_LIGHT through THERMAL_STATUS_SEVERE and beyond. At moderate and above, back off, lower video quality, reduce frame rate, pause background sync, because the system is already throttling the CPU and burning power for no throughput.
// Deferrable work states its conditions and lets the platform choose the time.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi only
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.build()
val prefetch = OneTimeWorkRequestBuilder<PrefetchWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(prefetch)
In the room, lead with the process rather than the list. Say you measure first with a system trace and Android vitals, find the biggest wake up source, and fix that one, because battery is dominated by a handful of causes and the rest is noise. Then name the three levers that cover most real apps, WorkManager with constraints instead of your own alarms, the cheapest location priority with the longest interval you can tolerate, and FCM instead of any background polling. That is the answer, and the depth follows from whichever one the interviewer pulls on.