DEV Community

Haseeb
Haseeb

Posted on

Engineering background geofencing without killing the battery

Opening hook

The silence in the lecture hall was absolute, the kind that feels heavy. The professor was mid-sentence, explaining a complex derivation on the whiteboard, when my pocket started buzzing. It wasn't just a vibration; it was a rhythmic, aggressive thumping against my thigh. I had forgotten to flip the silent switch after my morning gym session. As the ringing tone pierced the quiet, I scrambled to silence it, my face burning with embarrassment. It was the third time that month. I realized then that my reliance on manual toggles was a failure point I couldn't ignore.

The problem

We live in a world where our phones are expected to be context-aware, yet we still perform the mundane task of toggling sound profiles manually dozens of times a day. We mute for meetings, unmute for lunch, mute for prayer, and then repeat. The friction here is not just the act of tapping a button; it is the cognitive load of remembering to change settings in the first place. When you miss that mental cue, the consequence is social friction or personal disruption.

Existing solutions often felt like overkill or were tied to intrusive cloud services. Many automation apps I tested relied on constant GPS polling, which turned my battery into a furnace within three hours. Others required active internet connections, which was a non-starter for my privacy-first approach. I wanted something that functioned entirely locally, remained dormant until absolutely necessary, and survived the aggressive memory management policies of modern Android versions. I wanted a background service that was invisible, not just in its UI, but in its impact on the hardware.

The technical decision / implementation

To solve this, I leaned heavily on the GeofencingClient API rather than rolling my own location tracking. Many developers mistakenly believe they need to monitor LocationManager and calculate distances in a continuous loop to detect entries and exits. That is a battery death sentence. By using the GeofencingClient, I offload the heavy lifting to the Google Play Services layer, which is already optimized for batching location requests at the hardware level.

The architectural challenge was ensuring the IntentService or BroadcastReceiver would trigger reliably even when the device was in Doze mode. I registered a PendingIntent that broadcasts to a BroadcastReceiver. This receiver is the entry point for my logic. Crucially, I had to ensure that the logic within the receiver was lightweight. If the processing takes too long, the system will kill the process to save resources.

kotlin
val geofencingRequest = GeofencingRequest.Builder()
.addGeofence(geofence)
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.build()

geofencingClient.addGeofences(geofenceRequest, pendingIntent)
.run {
addOnSuccessListener { /* Geofence added / }
addOnFailureListener { /
Handle registration error */ }
}

I implemented a priority-based queue for sound states. If a location-based routine triggers an 'Enter' event, it pushes a state onto a local stack. When the 'Exit' event triggers, it pops that state and restores the previous one, or reverts to 'Normal' if no other conditions are active. This prevents the common bug where exiting one geofence accidentally defaults the phone to 'Normal' volume even if the user is still inside a separate, overlapping silent zone. By treating sound profiles as a stack of overrides rather than binary toggles, I created a system that respects overlapping context.

What surprised you / what you'd do differently

I initially assumed that the biggest challenge would be the precision of the GPS geofencing. I spent weeks tweaking radius sizes, worried that the phone would trigger too late or too early. It turns out, that was the easy part. The real nightmare was the 'Background Execution Limits' introduced in recent Android versions. I found that if my app was not in the foreground, the system would periodically strip my permissions or freeze my background service if it detected high battery usage.

What truly shocked me was the inconsistency of the AudioManager behavior across different manufacturers. On stock Android, AudioManager.RINGER_MODE_SILENT works exactly as documented. On certain heavily skinned devices from Asian markets, the OS would aggressively override my sound changes because its own 'Phone Manager' software thought my app was behaving suspiciously by changing system settings without user interaction. I had to implement a retry-loop with a backoff strategy that specifically checks if the ringer mode was successfully applied, and if not, logs the failure for the user to troubleshoot.

If I were starting over, I would move away from relying on BroadcastReceiver for state restoration. Instead, I would use WorkManager for almost everything. WorkManager is much better at guaranteeing execution even if the app is force-closed or the device reboots. My initial reliance on standard alarms meant that a phone reboot would wipe my schedule until the app was manually opened again. Integrating WorkManager with a BOOT_COMPLETED receiver was the necessary fix that I should have prioritized on day one.

Practical takeaway

If you are building an app that interacts with hardware sensors or system settings, stop trying to reinvent the wheel by keeping a service alive 24/7. Modern Android is designed to kill your process. Instead, shift your architecture toward event-driven triggers. Use GeofencingClient for location, AlarmManager for time, and WorkManager for state persistence. Your goal is to write code that only exists for the few milliseconds it takes to trigger an action.

Always design for the edge case where the system shuts you down. If your application state isn't written to a local database—I use Room for this—then you are building a fragile system that will fail the moment the user restarts their phone. By focusing on local, event-driven architecture, I was able to build Muffle as a lightweight tool that respects the user's battery life. You can see how this all comes together at https://play.google.com/store/apps/details?id=com.muffle.app, where I continue to refine these background routines to be as unobtrusive as possible.

Top comments (0)