The mid-afternoon sun was hitting the mosque floor, and the room was entirely silent, save for the rhythmic breathing of those in prayer. I had double-checked my phone before entering. Or so I thought. Halfway through the second rak'ah, a high-pitched, insistent notification tone shattered the stillness. My heart sank. Every head turned in my direction. I stood there, mortified, knowing that my phone was effectively a social liability. It wasn't the first time; it was just the most public, and consequently, the most humiliating one yet.
That recurring frustration wasn't just about bad timing; it was about the cognitive tax of constant vigilance. We are expected to be available 24/7, yet we are also expected to navigate complex social contexts where connectivity is a distraction. Most existing solutions either force the user to manually toggle settings, which is prone to human error, or they rely on cloud-based tracking that treats user privacy as an afterthought. I didn't want to upload my location history to a server just to keep my phone quiet in a library. I wanted a set-and-forget mechanism that lived entirely on-device.
When I started building Muffle, the problem wasn't just 'make the phone silent.' It was creating a system that could handle multiple, overlapping, and context-aware rules without leaking location data or draining the battery. I needed a geofencing engine that functioned reliably in the background, even when the OS decided to throttle or kill processes to save power. Most developers reach for third-party cloud services for this, but that felt like a bridge too far for a tool designed to bring peace of mind. I committed to a 100% offline architecture.
To manage this, I bypassed simple broadcast receivers for location and instead implemented the GeofencingClient API. The challenge, however, wasn't just drawing a circle on a map; it was managing the state transitions when multiple geofences overlapped. I had to architect a priority system where a 'Meeting' rule could override a 'Prayer' rule if they shared a geographical coordinate. I decided to store all routine data in a local Room database, ensuring that even if the app process was terminated, the PendingIntent fired by the geofencing service would wake the app and trigger the AudioManager to modify the device's ringer mode.
kotlin
val geofencingRequest = GeofencingRequest.Builder().apply {
setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
addGeofences(geofenceList)
}.build()
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
By keeping the logic contained within an Android ForegroundService, I ensured that the AudioManager operations were executed with high priority. I avoided WorkManager for immediate volume changes because the latency was too unpredictable; instead, I used the ForegroundService to keep the app in a state where it could respond to Geofence transitions within milliseconds. The tradeoff here was increased battery consumption, which I mitigated by using PRIORITY_BALANCED_POWER_ACCURACY for the location requests, striking a balance between precision and system health.
What truly surprised me during development was how hostile the modern Android ecosystem is to background tasks. I assumed that a properly registered ForegroundService would always be allowed to execute its logic. I was wrong. On certain OEM devices, particularly those with aggressive battery management, the background service was being killed regardless of notification presence. I spent three days debugging why my geofence triggers were not firing, only to realize that the WakeLock I implemented was being silently ignored by the OS due to custom power-saving layers. I had to implement a persistence mechanism that survives reboots by listening for the ACTION_BOOT_COMPLETED broadcast, which would then re-register all geofences from the local database.
Another non-obvious hurdle was the 'Jumu'ah' prayer context. I initially thought I could just use standard GPS geofencing. However, I soon realized that relying solely on GPS was flawed; if a user enters a mosque, their signal might be obstructed by thick walls, causing the geofence to trigger late or not at all. I had to build a fallback system using the Adhan library to calculate prayer times in parallel with the geofencing engine. This hybrid approach—time-based and location-based—was significantly more robust than relying on a single data source. If I were starting over, I would decouple the rule execution engine from the triggers earlier. I initially tightly coupled the triggers to the AudioManager calls, which made unit testing a nightmare. Separating them into a 'Trigger Event' bus would have saved me weeks of refactoring.
For any developer working on automation, the biggest lesson is to prioritize the user's perception of reliability over 'smart' features. If your app misses a single silence event, the user will uninstall it immediately. It’s better to have a simple, predictable trigger that works 99% of the time than a complex AI-based system that is erratic. Always design for the 'worst-case' environment: low GPS signal, aggressive battery-saving background killers, and users who turn off notifications.
Transparency is part of this reliability. When you keep data local, you aren't just protecting privacy; you are building trust. Users understand that if the app doesn't have an internet permission requirement, it physically cannot exfiltrate their location. This architectural constraint becomes a feature in itself. I built Muffle to solve my own need for a quiet life, and by leaning into the local-first approach, the app became a tool that respects the user's space as much as it manages their volume. You can see how I approached these constraints at https://play.google.com/store/apps/details?id=com.muffle.app, where I’ve balanced automated triggers with a completely offline, privacy-first architecture.
Top comments (0)