DEV Community

Haseeb
Haseeb

Posted on

Architecting a Battery-Efficient Geofencing Engine for Android

It happened during a quiet Friday prayer at the local mosque. The room was silent, the imam was mid-sermon, and I felt that familiar, sinking dread. My pocket vibrated. Then, a sharp, digital notification chime cut through the stillness like a knife. Every head in the row turned. I wanted to disappear. It wasn't the first time my phone had betrayed me, but it was the time I finally decided that relying on my own memory to toggle silent mode was a failed strategy. I needed the device to handle it for me.

We have all been there. You walk into a meeting, a lecture, or a cinema, and you are so focused on the task at hand that silencing your phone becomes a secondary thought. Then, the inevitable interruption happens. Conversely, how many times have you finished a meeting and spent the next three hours wondering why you haven't received a single notification, only to realize your phone is still stuck in 'Do Not Disturb' mode from the morning? These micro-failures in user experience aren't just annoying; they are a constant source of friction in our daily lives. I wanted to build a system that made these manual toggles obsolete.

Initially, I looked at standard background task solutions, but they were either too aggressive, draining the battery within hours, or too passive, failing to trigger precisely when the user crossed a boundary. The problem is that Android’s location APIs are designed for navigation, not for lightweight, long-running state management. If you poll the GPS every few seconds, you kill the battery. If you rely solely on cell tower triangulation, you lose the precision required for geofencing a specific building or office block. I needed a way to trigger sound profiles based on location without becoming the app that users uninstall because of high power consumption.

I settled on using the GeofencingClient within the Google Play Services library, but the implementation required a specific architectural choice regarding how the app handles PendingIntent callbacks. Instead of keeping a service active to listen for location updates, I registered the geofences with the OS-level GeofencingClient. This offloads the heavy lifting to the system. The system tracks the location; my app only wakes up when a specific boundary is crossed. This is the crucial difference between a battery-draining app and a lightweight one.

However, simply registering the geofences wasn't enough. I had to handle the BroadcastReceiver that catches these triggers. If the receiver is poorly managed, it can trigger multiple times or fail to execute when the device is in 'Doze' mode. I architected the GeofenceBroadcastReceiver to immediately hand off the work to a WorkManager task, ensuring the sound profile change is prioritized even if the system is under memory pressure. Here is a simplified look at how I structure the registration request:

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

geofencingClient.addGeofences(request, pendingIntent)
.addOnSuccessListener { /* Success logic / }
.addOnFailureListener { /
Handle registration error */ }

This approach works because it leverages the underlying Android architecture's existing location hardware management rather than forcing the device to keep the GPS radio active in the foreground. By keeping the radius tight—roughly 100 meters—I found a sweet spot between responsiveness and the frequency of false positives caused by GPS drift in high-density urban areas. One of the biggest challenges I faced was handling device reboots. If the device restarts, all GeofencingClient registrations are wiped clean by the operating system. I had to implement a BootReceiver that listens for the ACTION_BOOT_COMPLETED intent, which then triggers a re-registration flow of all stored user routines. This ensures that the automation doesn't stop working just because the phone ran out of battery or performed a system update.

What surprised me most during the development of Muffle was how erratic GPS behavior is inside large concrete buildings. I assumed that a geofence would be a clean 'in' or 'out' event. In reality, I spent weeks debugging why users were triggering 'exit' events while sitting at their desks. It turns out that when a phone loses a solid GPS lock indoors, the system might revert to network-based location, which can jump by hundreds of meters in a split second. This 'GPS jitter' was triggering false silent modes. My initial solution was to increase the geofence radius, but that made the timing too unpredictable. The real fix was implementing a 'debounce' logic in the BroadcastReceiver. I now store the timestamp of the last transition and ignore any subsequent transitions for that specific geofence for at least 60 seconds. This simple state-check saved me from the frustration of constant, accidental toggles.

Another assumption I got wrong was that the system would always provide a precise location update when entering a geofence. Sometimes, the transition event fires, but the location coordinates provided by the system are slightly stale. If I relied on those coordinates to verify the location, the logic would fail. I had to decouple the trigger from the action verification. Now, the trigger simply tells the app, 'Hey, the user entered this area,' and the app immediately checks the local database to see if the criteria are met, rather than trying to re-calculate distance on the fly. This architecture allows Muffle to remain completely offline, as no network call is needed to verify coordinates against a server.

If I were to start over, I would put more effort into the 'Priority' system earlier. Initially, I thought users would only have one routine active at a time. I was wrong. Users have overlapping schedules—a gym routine that overlaps with a work routine. Managing the state of the AudioManager when multiple triggers fire at once was a nightmare of race conditions. I ended up building a priority queue that sorts routines based on user-defined importance. If two routines conflict, the system calculates the 'winning' sound profile once and ignores the lower-priority trigger until the higher-priority one completes. Building this state machine in the local Room database allowed me to avoid complex concurrency issues that would have otherwise plagued the user experience.

For any developer working on automation or location-based services, the takeaway is simple: do not fight the Android framework. If you find yourself trying to keep a Service running in the background to track location, you are likely doing it the hard way. Use WorkManager for persistent tasks and GeofencingClient for location triggers. They are designed to let the OS optimize power usage, which is exactly what a user wants from an automation tool. You want your app to be the one that is 'set and forget,' not the one that shows up in the battery usage stats as a top offender.

Building tools that handle state for the user—like managing their phone's volume—requires a high degree of trust. If the app fails, the user looks bad in a meeting. If the app stays on 'Silent' too long, the user misses a call from their family. By focusing on robustness, offline-first data, and leveraging the system's own APIs rather than workarounds, you create an experience that feels like it belongs in the OS itself. I built Muffle with these exact principles to handle my own daily life, and it has since become a tool I rely on every single day to stay organized and quiet when I need to be. You can see how the final implementation turned out at https://play.google.com/store/apps/details?id=com.muffle.app.

Top comments (0)