Opening hook
The silence in the lecture hall was heavy, the kind that only exists right before a professor delivers a final exam prompt. I was sitting in the third row, mentally reviewing my notes, when my phone decided it was the perfect moment to announce a incoming call with a high-pitched, insistent ringtone. Every head turned. The professor paused, sighed, and waited for me to scramble. I fumbled with the volume rocker, face burning red, feeling that specific, sinking frustration of having failed a simple, repetitive task that I should have automated months ago.
The problem
That moment wasn't unique to me, and it certainly wasn't the first time it happened. We live in an era of constant connectivity, but our devices are surprisingly bad at understanding context. I needed a way to ensure my phone wouldn't disturb my environment during prayer, classes, or meetings, but the native Android 'Do Not Disturb' settings were too binary. They were either on or off, requiring manual intervention every single time I changed locations or entered a scheduled event.
I looked at existing solutions, but they were either bloated with telemetry, required invasive permissions, or simply failed to respect the device's sleep cycles. I wanted something that functioned as a background utility—a silent partner that managed sound profiles based on location or time without draining the battery. The friction wasn't just in the manual toggling; it was in the cognitive load of having to remember to flip a switch before walking into a room. I realized that if I wanted a tool that actually respected my privacy and my battery life, I had to stop looking for apps and start writing the logic myself.
The technical decision / implementation
When I started building Muffle, my initial instinct was to poll the device location every few minutes using a LocationManager update loop. I quickly realized that this was a recipe for disaster. Polling the GPS radio is the fastest way to kill a battery, and it rarely provides the precision needed for geofencing. Instead, I shifted my architecture to use the GeofencingClient within the Google Play Services library. This API is designed specifically to handle boundary-crossing events at the system level, allowing the OS to batch these checks and wake up the app only when necessary.
However, relying on the GeofencingClient isn't a silver bullet. The challenge is handling the transition events reliably, especially when the device is in Doze mode. I implemented a BroadcastReceiver that triggers an IntentService (and later, a JobIntentService to maintain compatibility with modern Android API levels) to handle the AudioManager state changes. The key here was ensuring the transition happens even if the app is process-killed.
kotlin
val geofencingRequest = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofence(geofence)
}.build()
geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)
.addOnSuccessListener { /* Geofence added / }
.addOnFailureListener { / Handle failure */ }
By leveraging the Geofence object's setLoiteringDelay, I prevented the sound profile from flickering if the user was just walking past the boundary. I also had to explicitly handle the ACTION_BOOT_COMPLETED intent. If the phone reboots, the GeofencingClient doesn't automatically persist all state in the way you might expect unless you re-register the geofences. I built a boot-receiver that re-initializes the geofencing service upon startup, ensuring that the 'mute' state persists across power cycles. This approach keeps the heavy lifting in the OS kernel rather than my own code, which is the only way to maintain background reliability without triggering the battery-optimization alerts that Android's modern architecture is so aggressive about.
What surprised you / what you'd do differently
The most humbling part of this project was discovering how inconsistent GPS hardware is across manufacturers. I spent a week debugging why my geofences wouldn't trigger on a specific mid-range device, only to find that the OEM's custom battery management was aggressively killing my PendingIntent before it could even execute the AudioManager change. I initially assumed that if I followed the documentation, the system would treat my service as a priority. I was wrong.
I also learned that Geofencing is not a substitute for high-precision location. If I were starting over, I would build a multi-layered verification system. Relying solely on the GeofencingClient is fine for large areas, but for small, tight boundaries like a specific office suite, it struggles with drift. I would have implemented a secondary check: if the user triggers a geofence, I would perform a low-power network-based location check to confirm they are actually inside the building before muting the phone. This would prevent the 'false positives' that occur when you walk near the edge of a geofence but don't actually enter. Additionally, I would have spent less time worrying about the AudioManager and more time focusing on the NotificationManager policies. Modern Android (API 23+) has a very specific way of handling 'Do Not Disturb' access that requires a deep understanding of NotificationManager.Policy. I spent too many hours fighting the system because I didn't realize that standard AudioManager calls were being overridden by restrictive DND settings.
Practical takeaway
If there is one thing I want you to take away from this, it is to stop fighting the Android OS and start playing by its rules. Every time I tried to force a background task to run continuously, the OS fought back, killed my process, and drained the user's battery. The secret to a stable background app isn't forcing it to stay awake; it is writing it so that it can sleep and be woken up by the system exactly when it needs to be. Whether you are using GeofencingClient, WorkManager, or AlarmManager, you must design for the 'event-driven' nature of Android.
Always assume your app will be killed by the system. If you aren't saving your state to a local database (like Room) and re-registering your triggers after a boot event, your app will eventually stop working for your users. Automation is powerful, but only if it's reliable enough to be forgotten. If you want to see how I've handled these edge cases in production, you can check out Muffle at https://play.google.com/store/apps/details?id=com.muffle.app. It's a work in progress, just like all our code, but it's a testament to the fact that with enough persistence, you can turn a annoying technical limitation into a stable, useful utility for thousands of people.
Top comments (0)