DEV Community

Haseeb
Haseeb

Posted on

Engineering Geofencing: Lessons in Android Battery and Location Accuracy

It happened during a quiet, solemn moment in a community prayer hall. I was sitting in the third row, reflecting, when suddenly, a high-pitched ringtone shattered the silence. It wasn't my phone, but the ripple effect of embarrassment was immediate. Everyone looked around, shifting uncomfortably. That collective tension is something we have all felt—a moment of human error that technology should have intercepted. I looked at my own device, feeling the familiar anxiety of whether I had remembered to flip the physical silent switch. It was then that I decided to stop relying on my own memory.

We live in an era of hyper-connected devices, yet the most basic context-awareness—knowing where we are and how our phone should behave—remains manual. I found myself constantly toggling between 'Normal' and 'Silent' modes at the library, the office, and the gym. If I forgot, I was the person disrupting a meeting. If I remembered to mute it, I inevitably forgot to unmute it, missing urgent calls from family for hours. The existing solutions were either too bloated, requiring invasive cloud permissions, or they simply failed to trigger reliably when the screen was off. I needed a solution that was local, predictable, and battery-conscious.

Building Muffle started with the realization that I had to master the GeofencingClient API without draining the user's battery. The primary challenge wasn't just triggering an event; it was doing so while the device was in a deep sleep state. I initially experimented with a standard LocationManager approach, polling GPS coordinates at set intervals. That was a disaster. It kept the radio active, pinged the GPS satellites constantly, and decimated the battery life in under four hours. It was an immediate non-starter for a production-ready application.

I pivoted to the Geofencing API provided by Google Play Services, which leverages the fused location provider. This is significantly more efficient because the system handles the batching and hardware-level monitoring. By defining a GeofencingRequest with an INITIAL_TRIGGER_ENTER flag, I allowed the OS to wake my app only when the boundary was crossed, rather than forcing the app to stay awake to watch the coordinates. To handle the background service requirement, I implemented a Foreground Service with a persistent notification. This ensures that the system doesn't kill the process during memory pressure, which is critical for a utility that needs to remain silent during a two-hour meeting.

kotlin
val geofence = Geofence.Builder()
.setRequestId("office_zone")
.setCircularRegion(lat, lon, 100f)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()

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

One implementation hurdle was the interaction with AudioManager. When the geofence triggers an ENTER event, the app needs to modify the ringer mode. However, Android has strict restrictions on changing 'Do Not Disturb' settings if the app hasn't been granted the NOTIFICATION_POLICY_ACCESS permission. I had to build a robust permission-checking flow that educates the user on why this is necessary, rather than just crashing or failing silently. The transition between a location-based trigger and a manual override also required a custom Priority logic to ensure that a recurring calendar event doesn't fight with a location-based rule.

The most surprising lesson I learned involved the 'flicker' effect at the edge of a geofence. If a user works on the edge of the 100-meter radius I defined, the GPS signal can 'jitter,' causing the app to rapidly fire ENTER and EXIT events. This led to the phone toggling between vibrate and normal mode constantly, which is incredibly annoying and power-intensive. I initially thought a simple debounce timer would solve it. I implemented a 5-minute cooldown period where the app ignores subsequent triggers. However, I realized this was too rigid for users who might actually leave and re-enter a building quickly. I ended up implementing a 'dwell time' requirement—the user must be inside the zone for at least 30 seconds before the state changes. This simple logic shift cleared up 90% of the false-positive triggers I was seeing during my beta testing.

Another assumption I got wrong was that the device would always have an accurate location fix. In dense urban environments with high-rise buildings, GPS 'multipath' errors occur where the signal bounces off glass, reporting the user as being three blocks away. I realized I couldn't rely solely on the geofence. I had to integrate Calendar events as a secondary, fail-safe trigger for the same routine. By allowing the app to cross-reference the user's location with their actual schedule, I created a redundancy that made the system feel much more reliable than a pure GPS implementation.

If I were to start over, I would focus more heavily on the WorkManager API from the beginning for my routine scheduling. I relied too much on custom AlarmManager implementations early on, which are notoriously finicky across different OEM battery management policies—especially on devices from manufacturers that aggressively kill background processes. WorkManager handles these manufacturer-specific quirks much better, ensuring that even if the app is put to sleep, the next trigger will eventually fire. I also underestimated the complexity of the Adhan library integration for prayer times. Calculating accurate times based on specific latitudinal offsets requires a deep understanding of standard time zones versus local solar time, and I spent a full week just debugging timezone shifts during daylight savings transitions.

As developers, we often gravitate toward the most complex solution, thinking that 'harder' equals 'better.' My experience with Muffle showed me that the most effective architecture is the one that minimizes the number of wake-locks. Every time your code wakes the processor, you are borrowing battery life from the user. The goal should be to delegate as much logic as possible to the OS-level APIs. When you let the system manage the location batching, you aren't just saving battery; you are providing a smoother, more transparent experience that doesn't feel like a resource hog. Always prioritize the OS APIs over custom polling loops, and treat the user's battery as a finite, precious resource.

Building Muffle has been a journey into the constraints of the Android ecosystem. It forced me to think about the phone not just as a computer in a pocket, but as a context-aware companion that should respect the user's focus. If you are struggling with your own background tasks, start by looking at your wake-lock usage and see if there is an event-based API that can replace your current polling logic. You can see how I implemented these triggers and managed the sound profile states by exploring the project at https://play.google.com/store/apps/details?id=com.muffle.app. Hopefully, these lessons in geofencing and power management help you build more efficient, user-friendly Android experiences in your own projects.

Top comments (0)