DEV Community

Haseeb
Haseeb

Posted on

Architecting reliable geofencing for Android without killing the battery

It happened during a quiet Friday Jumu'ah prayer. The sermon was in full swing, the room was pin-drop silent, and then it started—the aggressive, rhythmic vibrations of a smartphone echoing against the wooden floorboards. Every head in the room turned toward the sound. I felt the heat rise to my face because it was my phone, despite me being the guy who usually prides himself on having a 'smart' setup. I had forgotten to toggle the silent mode after leaving my office, and that simple oversight turned a moment of peace into a source of public embarrassment.

That specific friction is what drove me to build Muffle. We all live in a cycle of manual toggling: silent before meetings, vibrate during class, back to normal at home, and then forgetting to revert the settings until we miss an important call three hours later. While Android has some built-in 'Do Not Disturb' rules, they often feel too rigid or lack the context-aware triggers that actually match our messy, unpredictable lives. I wanted something that felt invisible, something that handled the sound profile based on where I was, not just what time it was on the clock.

Building a geofencing system that actually works—without turning the user's phone into a space heater—is a challenge that separates toy apps from production-ready tools. The Android Geofencing API is the standard tool for this, but it is notoriously finicky. If you just naively register geofences for every single user routine, you hit the system-imposed limit of 100 geofences per app, which sounds like plenty until you realize that Google Play Services might decide to throttle your requests if you aren't careful with your PendingIntent usage or location accuracy requests.

For Muffle, I decided to offload the heavy lifting to the FusedLocationProviderClient. The core architectural decision was to decouple the monitoring service from the UI entirely. I implemented a Foreground Service that manages the geofence registration using GeofencingRequest and LocationServices.getGeofencingClient(context). The key was setting the LoiteringDelay correctly. If you set it too short, you get 'flickering' as the user walks near the boundary of their office or home. If you set it too long, the user is already deep into their meeting before the phone silences. I eventually settled on a 30-second delay for geofence transitions to ensure the device has a stable signal lock before firing the BroadcastReceiver that triggers the AudioManager changes.

kotlin
val geofence = Geofence.Builder()
.setRequestId(routineId)
.setCircularRegion(lat, lon, radius)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.setLoiteringDelay(30000)
.build()

By keeping the BroadcastReceiver as a lightweight bridge that simply calls a WorkManager task, I ensured that even if the OS decides to kill the foreground service to reclaim memory, the state change still triggers. This is the difference between an app that works 90% of the time and one that users can actually trust. Trust is the only currency that matters in utility apps, because the moment the phone rings in a quiet room, the app loses its reason for existing.

What surprised me most during development was how aggressive the modern Android power management layers are toward background tasks. My initial assumption was that a simple Service with a WakeLock would be sufficient for monitoring location. I was wrong. On many OEM devices—looking at you, certain Chinese manufacturers—the battery optimization settings are so restrictive that they effectively kill background processes the second the screen turns off. My geofences would simply stop firing.

I learned the hard way that you cannot fight the OS. Instead of trying to keep a persistent, high-consumption location listener alive, I had to pivot to a 'Batching' strategy. I started using setExpedited on my WorkManager requests to ensure that the OS treats these background sound-toggles as high-priority tasks. I also had to implement a manual 'Reboot Receiver'. You might think that registered geofences persist through a reboot, and they technically do, but the PendingIntent associated with them often gets lost in the ether when the system cleans up the stale process state. My current architecture forces a re-sync of all active routines upon the ACTION_BOOT_COMPLETED broadcast. If I were starting over, I would have built the state persistence layer much earlier. I spent two weeks chasing a bug where routines would simply disappear after a software update, only to realize that I wasn't properly handling the onReceive lifecycle of the boot event in the AndroidManifest.xml.

Another non-obvious lesson was the impact of GPS vs. Network location. For a tool like Muffle, relying strictly on GPS is a mistake. It drains battery and fails indoors. I had to use PRIORITY_BALANCED_POWER_ACCURACY rather than PRIORITY_HIGH_ACCURACY. Most users don't need to know they are in their office building within a three-meter margin; they just need to know they are within the general perimeter. By loosening the accuracy requirements, I cut battery usage by nearly 40% while keeping the routine triggers reliable enough for daily use.

If you are building an Android utility, stop trying to be 'clever' with system resources. The Android team has spent years building a robust set of tools like WorkManager and FusedLocationProviderClient for a reason. If you find yourself fighting the OS, you are likely using the wrong API. Don't build a custom background loop when the system provides a perfectly good event-driven architecture that handles the power state for you. The goal isn't to be the most active app on the device; the goal is to be the most reliable one.

Focus on the edge cases. What happens when the device loses network connectivity? What happens when the user has multiple overlapping routines? What happens when the user updates their calendar? If you build for the 'happy path' where the phone always has a signal and the user never restarts their device, you are building a prototype, not a product. Muffle was my attempt to bridge that gap between a simple idea and a utility that actually survives the reality of a busy, notification-filled life. You can see how I approached these problems and the final result at https://play.google.com/store/apps/details?id=com.muffle.app.

Top comments (0)