DEV Community

Haseeb
Haseeb

Posted on

Engineering Geofencing: The Battery-Accuracy Tradeoff in Android

It was the second rakat of Maghrib prayer, the quietest moment of my evening, when a jarring, high-pitched ringtone shattered the silence of the room. It wasn't my phone, but the sound was loud enough to break the focus of everyone standing in the row. I felt that familiar, sinking pit in my stomach. We have all been there: a meeting at work, a medical checkup, or a silent lecture where our digital lives suddenly demand attention at the worst possible time. I realized then that I had forgotten to silence my device again.

That recurring frustration became the catalyst for Muffle. I wanted an automation tool that understood the context of my environment without me having to remember to toggle a switch every single time. The problem is that manual control is binary: either you remember, or you suffer the consequences. Most existing solutions relied on rigid time schedules, which rarely account for the unpredictability of life. If a meeting runs late or a prayer schedule shifts by a few minutes, a static timer fails. I needed a system that understood location and context, but building that on Android is a masterclass in compromise.

When I started building the geofencing engine for Muffle, the immediate technical hurdle was the inherent tension between location accuracy and battery life. Android’s GeofencingClient is the standard tool for this, but it is a black box. You provide a latitude, longitude, and radius, and the OS handles the monitoring. The trap for many developers is assuming that setting a high-accuracy requirement will always result in timely triggers. In reality, the system optimizes for battery longevity by batching location updates. If the device is in a low-power mode or the user is stationary for long periods, the OS might delay the transition trigger by several minutes.

I initially experimented with LocationManager and requestLocationUpdates to force higher precision, but the battery drain was catastrophic. I saw the system process wake up every few seconds, which is a death sentence for a background app. Instead, I pivoted to using the GeofencingClient combined with a PendingIntent that triggers a BroadcastReceiver. This allows the application to remain dormant until the OS detects the transition. The real challenge was handling the 'dwell' time. If a user walks along the edge of a geofence, the device might rapidly toggle between 'inside' and 'outside' states, causing the phone to switch sound profiles back and forth like a strobe light.

To solve this, I implemented a custom state-buffer in my logic layer. When a trigger fires, the app checks a timestamp threshold. If a state change occurs within sixty seconds of a previous one, the system ignores it. Here is the simplified logic I used inside the GeofenceBroadcastReceiver:

kotlin
val geofencingEvent = GeofencingEvent.fromIntent(intent)
val transition = geofencingEvent.geofenceTransition

if (System.currentTimeMillis() - lastTriggerTime > 60_000) {
if (transition == Geofence.GEOFENCE_TRANSITION_ENTER) {
applySoundProfile(Profile.SILENT)
lastTriggerTime = System.currentTimeMillis()
}
}

This simple time-gate prevents the 'jitter' effect while keeping the ForegroundService from needing constant CPU cycles. It is a classic architectural trade-off: I sacrificed the 'instant' trigger of a few seconds for the sake of long-term reliability and battery stability. I chose to prioritize the user's phone staying quiet over the ego of a perfectly timed trigger.

What surprised me most during this journey was how much the hardware manufacturers' aggressive battery management policies interfere with even standard Android APIs. I assumed that if I used the official GeofencingClient, the system would respect my request to monitor a location. I was wrong. On certain devices, the OS would kill the background service entirely if it deemed the app 'inactive,' even if the geofence was active. I spent weeks debugging why my routines weren't firing on specific hardware, only to realize that the 'Doze' mode was suppressing the background execution of my BroadcastReceiver.

I learned the hard way that you cannot rely solely on the OS to keep your app alive. I had to restructure the app to use a ForegroundService with a persistent notification. This signals to the Android OS that the app is performing a critical user-facing task—managing sound profiles—which gives it a higher priority in the process lifecycle. If I were starting over, I would have built a more robust diagnostic logging system from day one. In the early builds, when a trigger failed, I had no way of knowing if it was a GPS signal loss, a lack of permission, or an OS-level restriction. I had to manually add an internal activity log that tracked every transition attempt and failure, which ended up being the most important feature for debugging user reports.

Another assumption I had to discard was that users want complex, granular controls. I initially thought about adding speed detection to guess if someone was driving, but it just added bloat and battery overhead for a feature that barely anyone used. The lesson here is that 'good enough' is often better than 'technically perfect' when it comes to user experience. A slightly delayed silent trigger is better than a drained battery or a complex interface that requires a manual to operate.

For any developer working on background location or automation tasks, the biggest takeaway is to respect the platform's constraints rather than fighting them. Do not try to bypass the system's battery optimizations; you will lose that war every time. Instead, design your architecture around the reality that your app will be stopped, killed, and restarted by the OS at will. Use JobScheduler or WorkManager for tasks that don't need to happen at the exact millisecond, and always maintain local state so your app can resume exactly where it left off after a reboot.

Automation should be invisible, not a source of additional complexity. By focusing on the core problem—preventing those awkward social moments—I found that users care less about the 'why' of the technology and more about the 'consistency' of the result. If you are interested in how I implemented these rules locally to ensure complete privacy, you can explore the current build of Muffle at https://play.google.com/store/apps/details?id=com.muffle.app. Always build with the assumption that your code will be interrupted, and you will find yourself writing much more resilient applications.

Top comments (0)