Opening hook
The silence in the mosque was absolute, broken only by the rhythmic recitation of the imam. I was in the middle of a prayer, my head bowed, when a sudden, aggressive vibration rattled against the marble floor. It was my phone. My heart sank. I felt every eye in the room shift toward me. It was not just a notification; it was an interruption of a moment that demanded total focus. I had forgotten to silence my device again, and that single mistake turned a moment of peace into a source of public embarrassment.
The problem
We live in an age of constant connectivity, yet our devices remain remarkably unintelligent when it comes to context. I constantly found myself in meetings, classes, or prayer halls where a ringing phone was unacceptable. The standard Android "Do Not Disturb" features were either too manual or too rigid. I tried scheduling them, but my schedule is never static. If a meeting ran late or a prayer time shifted by a few minutes, the pre-set timers were useless. I needed something that understood where I was, not just what time it was.
Most existing solutions forced me to manage my phone's sound settings manually or relied on invasive, battery-draining background processes that kept the GPS radio active 24/7. I didn't want a "smart" assistant that required an active data connection or a cloud account to function. I wanted a local, offline-first tool that could silently monitor my location and adjust my AudioManager state without me needing to look at a screen. The friction wasn't just in the silencing; it was in the mental overhead of having to remember to flip a physical switch or toggle a software setting every time I walked through a door.
The technical decision / implementation
When I started building Muffle, the biggest challenge was handling location-based triggers without destroying the user's battery life. If I had simply requested high-accuracy location updates every few seconds, the app would be uninstalled within an hour. Instead, I leaned into the GeofencingClient API provided by Google Play Services, which is designed specifically for this use case. This API offloads the heavy lifting to the system, allowing the OS to batch location requests and wake up the app only when a boundary is crossed.
However, the GeofencingClient is a black box. To ensure it worked reliably, I had to be precise with the GeofencingRequest parameters. I set the LoiteringDelay carefully to prevent the sound profile from toggling if a user just passes by the building. Crucially, I implemented a BroadcastReceiver to listen for the GeofencingEvent. This allows the app to remain dormant while the system handles the proximity detection. The actual sound state change is handled via AudioManager using the setRingerMode method:
kotlin
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.isVolumeFixed) return
when (actionType) {
"SILENT" -> audioManager.ringerMode = AudioManager.RINGER_MODE_SILENT
"VIBRATE" -> audioManager.ringerMode = AudioManager.RINGER_MODE_VIBRATE
"NORMAL" -> audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
}
This approach ensures that Muffle doesn't need to hold a wakelock or keep a service running in the foreground at all times. By using PendingIntent for the geofence transition, the process only wakes up at the exact moment a boundary is breached. It’s a clean separation of concerns: the system manages the location hardware, and my app manages the policy logic. This architecture keeps the battery impact negligible while providing the automation I originally set out to build. It was a trade-off: I sacrificed the ability to have micro-second precision in exchange for a battery-friendly, reliable background process.
What surprised you / what you'd do differently
What truly caught me off guard was the inconsistency of the GeofencingClient across different manufacturers. I initially assumed that if I set a 100-meter radius, the system would trigger the event consistently. I was wrong. On devices from specific manufacturers with aggressive "battery optimization" settings, the geofencing trigger would sometimes be delayed by minutes or, in rare cases, suppressed entirely until the user woke their phone.
I learned the hard way that Android is not a monolith. I had to implement a secondary, manual verification check using LocationManager's getLastKnownLocation when a geofence event triggers, just to be sure the user is actually where the system claims they are. If I were starting over today, I would have spent more time building a robust logging system for these edge cases from day one. I spent weeks chasing "phantom" bugs that turned out to be the OS killing my BroadcastReceiver because of custom power-saving layers.
Another realization was that relying solely on GPS isn't enough. Users move indoors, where GPS signal quality drops significantly. I had to add fallback logic using network-based location providers. If I could do it again, I would prioritize building a more complex priority queue for routines. Currently, when two routines overlap, the priority system handles it, but documenting those conflicts for the user in the UI proved to be a significantly harder UX challenge than the actual backend implementation. Transparency in automation is just as important as the code that drives it.
Practical takeaway
If you are building an Android app that relies on location or background sensors, stop trying to "force" the system to keep your app alive. Don't fight the OS power management; lean into it. Use APIs like GeofencingClient or WorkManager that are intended for these deferred, system-managed tasks. Your goal should be to minimize your footprint so the user never has a reason to check their battery stats and see your app at the top of the list.
Focus on edge cases where your data might be stale. Always assume the user's GPS signal will be lost, that the phone will reboot, or that the system will kill your process to reclaim memory. If your app can survive a crash, a reboot, and a total loss of location signal, it will provide a much better experience than a "perfect" app that only works under ideal conditions. For those who want to see how this handles real-world usage, you can see how I implemented these constraints at https://play.google.com/store/apps/details?id=com.muffle.app. Building offline-first, low-impact tools requires a shift in mindset, moving away from constant connectivity and toward smart, event-driven architecture that respects the device's battery as a finite resource.
Top comments (0)