It happened during a Friday afternoon sermon. The mosque was silent, the imam was mid-sentence, and then, a familiar, high-pitched ringtone cut through the stillness like a knife. It was my phone. My face burned as I scrambled to silence it, realizing I had walked through the door completely forgetting that my phone was still in normal mode. That moment of shared, collective embarrassment—and the subsequent realization that I do this multiple times a week—is exactly why I started building Muffle.
The problem
We live in a world of constant notifications, but our phones rarely understand the context of our physical location or schedule. Manually toggling 'Do Not Disturb' or switching to vibrate is a classic 'if-I-remember' task, which, by definition, means it fails when we are most distracted. Whether it is an important board meeting, a medical appointment, or a quiet study session, the friction of manual management is a recurring failure point.
I looked for existing solutions, but most were either overly bloated, required intrusive permissions for cloud-based tracking, or simply failed to respect the device's battery life. I didn't want a background process that drained the battery by polling GPS every thirty seconds. I needed something that felt native, invisible, and, most importantly, reliable. I wanted an automation engine that could handle context-aware triggers—like GPS geofencing and prayer times—without turning my phone into a space heater. Building this required a deep dive into how Android handles location services, and it forced me to rethink my initial assumptions about background execution.
The technical decision / implementation
When I first set out to build the geofencing engine for Muffle, my initial instinct was to create a background service that would poll the LocationManager at set intervals. I quickly realized this was a recipe for disaster. Polling GPS is an energy-intensive operation that wakes up the processor and hits the cellular radio, which is the fastest way to kill a user's battery and get an app force-closed by the Android OS.
Instead, I pivoted to the GeofencingClient API from Google Play Services. This API is designed to offload the heavy lifting to the system. You define a Geofence object with a latitude, longitude, and radius, and you register it with the system. Once registered, you stop worrying about it. The system monitors your location in a highly optimized way—often using a combination of cellular towers and Wi-Fi access points rather than raw GPS—and triggers a PendingIntent only when the boundary is crossed.
kotlin
val geofence = Geofence.Builder()
.setRequestId("office_zone")
.setCircularRegion(lat, lon, radius)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
geofencingClient.addGeofences(geofenceRequest, pendingIntent)
This architectural choice meant that Muffle doesn't need to be 'running' in the traditional sense. The system handles the state transitions, and my app simply wakes up to process the Intent when the threshold is crossed. To ensure this persists through device reboots, I implemented a BroadcastReceiver listening for ACTION_BOOT_COMPLETED. This re-registers the geofences upon startup, ensuring the rules are always active without requiring the user to open the app. The priority system was another layer; since I am modifying AudioManager states, I had to handle potential conflicts where multiple triggers might overlap, using a simple priority queue to ensure the 'strictest' sound profile (like Silent over Vibrate) always takes precedence.
What surprised you / what you'd do differently
The biggest surprise was not the technical complexity of the APIs, but the sheer unpredictability of Android's 'doze' mode and manufacturer-specific battery optimizations. I assumed that if I followed the documentation, my PendingIntent would always fire immediately upon entering a geofenced area. In practice, I found that on certain devices—particularly those from manufacturers with aggressive battery management policies—the transition could be delayed by several minutes.
I initially thought I could solve this by increasing the responsiveness setting, but that just burned more battery for little gain. I learned the hard way that geofencing on Android is not an exact science. It is a probabilistic approximation. If I were starting over today, I would architect the system to be less binary. Instead of relying solely on a geofence trigger, I would implement a 'fuzzy' logic layer that checks the proximity to a location while also cross-referencing the time.
Another lesson learned was the importance of the ForegroundService. I initially tried to handle everything through the BroadcastReceiver, but the OS frequently killed the process before it could finish updating the volume settings. Moving the logic to a ForegroundService with a persistent notification was the only way to ensure the sound profile actually changed in time. I also underestimated the difficulty of handling 'Jumu'ah' prayer times, which required a custom calculation engine since they don't follow the exact same logic as daily prayers. The library I integrated, Adhan, was robust, but integrating it with my existing GPS-based geofencing required careful synchronization to avoid redundant background tasks.
Practical takeaway
If you are building an app that relies on location or background triggers, stop trying to fight the Android OS. Don't write your own polling loops. Use the system's provided APIs like GeofencingClient and WorkManager. These tools are designed to batch work, leverage hardware-level sensors, and respect the battery-saving constraints that keep users from uninstalling your app.
My primary advice is to design for failure. Your triggers will be late, your service will be killed, and the user will move between zones faster than the GPS can track. Build your logic to be idempotent—ensure that running the same sound command five times in a row doesn't break anything. If your app relies on device state, treat the state as a suggestion rather than a constant. You have to account for the reality that the user's phone is a shared resource between your code and a dozen other power-hungry background processes. It is a balancing act, and the best apps are the ones that manage that balance without the user ever noticing they are doing it. I built Muffle at https://play.google.com/store/apps/details?id=com.muffle.app to solve a specific pain point in my own life, and the process taught me that sometimes, the best feature you can add to an app is simply getting out of the way of the user's battery.
Top comments (0)