DEV Community

Haseeb
Haseeb

Posted on

Engineering Geofencing for Android: Balancing Precision and Battery Life

It happened during a quiet afternoon at the mosque. The imam was mid-sermon, the room was silent, and the air felt heavy with focus. Suddenly, a high-pitched ringtone shattered the stillness, echoing off the stone walls. Heads turned, eyes narrowed, and the sense of peace was instantly replaced by an uncomfortable, collective tension. I checked my own pocket, relieved my phone was silent, but the incident stayed with me. We have all been there—whether in a board meeting, a classroom, or a doctor's office—praying that our phones don't betray us.

The friction isn't just about the noise; it's about the cognitive load of managing our devices. We are expected to be available 24/7, yet social etiquette demands we vanish into silence at a moment's notice. Before I built Muffle, I tried relying on manual toggles. I would mute my phone for a meeting and then, two hours later, realize I had missed three urgent calls because I forgot to unmute. I tried location-based automation apps, but they were either heavy battery drains or relied on cloud services that didn't respect my privacy. I wanted a solution that lived locally, respected my data, and didn't require me to carry a power bank just to handle basic sound profiles.

When I sat down to build Muffle, the biggest challenge was the geofencing implementation. I needed to detect when a user entered or left a specific area to trigger a sound profile change. Using the GPS LocationManager directly was out of the question; keeping the GPS radio active in the background is a recipe for battery disaster. Instead, I opted for the GeofencingClient within the Google Play Services library. This is a higher-level abstraction that delegates the heavy lifting to the OS. The OS uses a combination of Wi-Fi, Bluetooth, and cellular signals rather than relying purely on power-hungry satellite triangulation. This approach allows the system to batch location updates, significantly reducing the frequency of wake-locks that kill battery life.

However, the API alone doesn't solve the problem of background execution. To ensure Muffle actually changes the volume when a geofence triggers, I implemented a JobIntentService. This ensures that even if the app process is killed by the system to reclaim memory, the intent is handled. I had to manage the PendingIntent carefully to avoid memory leaks. The core logic inside my GeofenceBroadcastReceiver looks something like this:

kotlin
override fun onReceive(context: Context, intent: Intent) {
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) return

val transition = geofencingEvent.geofenceTransition
if (transition == Geofence.GEOFENCE_TRANSITION_ENTER) {
    audioManager.ringerMode = AudioManager.RINGER_MODE_SILENT
}
Enter fullscreen mode Exit fullscreen mode

}

This code snippet seems simple, but the real complexity lies in the GeofencingRequest. If you set the loiteringDelay too low, you get constant "flicker" events as the user moves slightly within their building. I had to tune the initialTrigger and loiteringDelay values to create a stable buffer zone. I discovered that for most indoor locations, a radius of 100 meters is the sweet spot. Anything smaller, and you risk the system failing to trigger due to GPS drift; anything larger, and the phone might go silent before you even reach your destination.

What surprised me most was the reality of the Android Doze mode. I assumed that because I was using the official Geofencing API, the system would always wake my app up instantly. I was wrong. When the phone enters deep sleep (Doze), the system aggressively limits how often it processes geofence transitions to save power. My early tests showed a 3-5 minute delay between entering a geofence and the phone actually silencing. This was infuriating. I initially thought about requesting REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, but that is a quick way to get flagged by the Play Store and ruin the user's battery life. Instead, I embraced the limitation. I added a subtle notification that tells the user the phone is entering a 'buffered' state, and I ensured the logic accounts for the delay by prioritizing the most recent transition event when the app finally wakes up.

I also learned that AudioManager.RINGER_MODE_SILENT is not the same as Do Not Disturb. If you just set the phone to silent, you miss critical calls. I had to pivot to using NotificationManager.setInterruptionFilter alongside the AudioManager settings. This was a massive architectural shift that forced me to rewrite my entire routine engine. If I were starting over, I would build the priority system first. I initially designed routines as independent islands, but when you have a calendar event overlapping with a GPS geofence, the system gets confused about whether to be silent or vibrate. A central RoutineManager that calculates the 'highest priority' state every time a trigger fires is non-negotiable.

For any developer working on background location tasks, the biggest lesson is to stop fighting the OS. Android is designed to save power above all else; if your app is the reason a phone dies by 2 PM, the user will uninstall it, regardless of how useful the features are. Use the built-in system APIs like GeofencingClient instead of building your own location polling loops. Accept the latency that comes with Doze mode and design your user experience around it rather than trying to bypass it. If you need to perform a task that must happen exactly on time, look into AlarmManager with setExactAndAllowWhileIdle, but use it sparingly.

Automation shouldn't be complex for the user, even if it is complex for the developer. By keeping the logic local and the triggers modular, you can build tools that feel like a natural extension of the phone's operating system. If you are interested in seeing how I handled the intersection of these various triggers in a real-world app, you can explore the implementation details of Muffle at https://play.google.com/store/apps/details?id=com.muffle.app. Focus on the user's friction points first, and the architecture will naturally follow.

Top comments (0)