DEV Community

Haseeb
Haseeb

Posted on

Building Reliable Geofencing Without a Backend Dependency

It happened during a quiet afternoon prayer at the mosque. The imam had just begun the recitation when a phone in the third row started blaring a custom ringtone at maximum volume. The person fumbled, dropped it, and in their haste to silence it, accidentally hit the volume rocker until it was silent—but they forgot to turn it back up afterward. I sat there watching the scene unfold, realizing that the standard Android volume controls were entirely reactive, not proactive. My own device was in my pocket, and I wondered if I could build something that just handled this for me without needing a cloud database or constant internet access.

We live in an era where almost every mobile application demands a persistent network connection. If the servers go down, or if the user is in a basement with poor reception, the functionality vanishes. But when you are building a tool designed to manage physical sound profiles—something that should be as fundamental as the hardware itself—relying on a network call to verify a location or a routine is a massive architectural failure. The friction lies in the latency between the intent and the execution. If my phone needs to ping a server to check if I am at the office before it silences itself, and that server request hangs due to a weak signal, the phone rings anyway. That is not just an inconvenience; it is a point of social failure that users shouldn't have to navigate.

To build Muffle, I decided early on that the application had to be entirely offline. This meant moving all logic, including complex prayer time calculations and geofencing triggers, directly onto the user's device. I chose the GeofencingClient from the Google Play Services library as my primary tool, but the real challenge wasn't just drawing a circle on a map; it was managing the transitions without a persistent background socket. I had to ensure that the IntentService or BroadcastReceiver would wake up accurately, even if the device was in deep Doze mode, and process the transition before the user walked through the door. I opted to use a combination of AlarmManager for precise time-based triggers and GeofencingClient for spatial triggers. The decision to use PendingIntent for the geofence transitions was critical; it allows the system to wake up the app, execute the necessary AudioManager call to toggle the ringer mode, and then immediately return to a sleep state, conserving battery while maintaining high reliability.

kotlin
val geofenceRequest = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeences(geofenceList)
}.build()

geofencingClient.addGeofences(geofenceRequest, geofencePendingIntent)
.addOnSuccessListener { /* Geofence added / }
.addOnFailureListener { /
Handle error */ }

By keeping this logic local, I avoided the pitfalls of latency. When the device crosses the geofence, the system fires the intent directly to my app's broadcast receiver, which triggers the AudioManager.RINGER_MODE_SILENT command. No network round-trip, no DNS lookup, no failed API calls. It is just the OS talking to the local application logic. This architecture ensures that the sound profile updates happen within milliseconds of the user entering the predefined zone, which is the only way to make an automation app feel like a native, reliable part of the operating system.

What truly surprised me during development was the volatility of the FusedLocationProviderClient. I initially assumed that if I set a geofence radius of 50 meters, the OS would trigger precisely at that boundary. I was wrong. Android’s location accuracy varies wildly depending on whether the user is indoors, relying on Wi-Fi scanning, or outdoors with a clear view of GPS satellites. My first iteration failed constantly because it relied too heavily on high-accuracy GPS, which would often drift when the user moved into a large building. I learned that for a tool like Muffle, a 50-meter radius is actually dangerous. If the device's signal drifts, the geofence doesn't trigger until the user is already deep inside the room. I had to increase the minimum radius to 150 meters and implement a logic layer that checks for 'dwell time' to avoid constant toggling if the user is hovering near the boundary line.

Another assumption I had to discard was the idea that 'silent' meant 'silent.' I discovered that many users had legitimate emergency contacts—like family members or childcare providers—who needed to reach them regardless of the profile. Adding an emergency bypass feature wasn't just a quality-of-life add-on; it was a fundamental necessity. I had to integrate with the NotificationManager.Policy to properly handle 'Priority' calls, which was a nightmare of documentation reading and trial-and-error testing across different Android versions. If I were starting over, I would have spent more time building a robust testing harness for AudioManager changes across various API levels from the start, rather than relying on my own device as the primary test bench. The differences in how Android 12, 13, and 14 handle Do Not Disturb permissions are subtle but can break the entire app if you aren't strictly checking isNotificationPolicyAccessGranted before attempting to change the phone's state.

For any developer building automation, the biggest lesson is to respect the platform's constraints rather than fighting them. Do not try to keep a foreground service running indefinitely if you can accomplish the same task with AlarmManager and BroadcastReceivers. The more you rely on the system's own scheduling tools, the more stable your app will be when the OS decides to aggressively kill background processes. If you are handling sensitive user data—like location or calendar access—keep it local. Users are far more likely to trust an app that declares it has no internet permissions than one that requires a login, a password, and a cloud sync feature just to silence a ringer. Keeping the logic offline is not just a technical challenge; it is a design philosophy that prioritizes user privacy and system stability above all else.

In my work on Muffle, I have found that the most effective tools are those that vanish into the background once they are set up. If you are interested in how I handled these local-only triggers, you can see how I implemented the geofencing and prayer time logic in the project at https://play.google.com/store/apps/details?id=com.muffle.app. Building for the edge of the device requires a shift in mindset, but it results in a much cleaner, more responsive user experience.

Top comments (0)