Opening hook
The silence in the lecture hall was heavy, the kind that amplifies every nervous breath. I was sitting in the third row, mentally rehearsing my presentation points, when my phone vibrated against the wooden desk. It wasn't just a vibration; it was a rhythmic, buzzing thud that echoed through the quiet room like a jackhammer. Every head turned. I froze, my face flushing, frantically fumbling to silence the device. That moment of visceral, social discomfort was the exact second I decided I was tired of being at the mercy of my own hardware.
The problem
We live in an age where our phones are supposed to be smart, yet they are remarkably bad at understanding context. I found myself constantly oscillating between two extremes: either I left my phone on loud and risked a disruptive interruption in a meeting, a place of worship, or a library, or I switched it to silent and forgot to toggle it back, missing important calls from family or clients for hours on end. Manual intervention is the enemy of automation. I didn't want to add another widget or remember to tap a button before entering a building. I wanted the device to simply know where it was.
Most existing solutions for this problem fall into two camps: they are either battery-hungry monsters that poll the GPS sensor every few seconds, effectively turning your phone into a space heater, or they are unreliable hacks that lose track of location the moment the screen turns off. As a developer, I knew there had to be a way to utilize the system's underlying location services without sacrificing the user's daily battery life. The friction wasn't just in the app design; it was in the fundamental Android power management architecture.
The technical decision / implementation
When I started building Muffle, I initially considered writing a background service that would poll location data every five minutes. I quickly realized this was a recipe for disaster. Using the LocationManager with high accuracy requests effectively prevents the CPU from entering a low-power state. Instead, I pivoted to the GeofencingClient within the Google Play Services library. This API is designed specifically for this use case—it allows the system to handle the heavy lifting of location tracking at the hardware level, waking up my app only when a specific, user-defined circular boundary is breached.
To implement this, I defined a GeofencingRequest using GeofencingRequest.Builder. I had to carefully set the setInitialTrigger to INITIAL_TRIGGER_ENTER | INITIAL_TRIGGER_EXIT to ensure that if a user entered a zone while the app process was killed, the system would still register the transition. I then passed the PendingIntent to a BroadcastReceiver, which triggers my logic to adjust the AudioManager settings.
kotlin
val geofence = Geofence.Builder()
.setRequestId(routineId)
.setCircularRegion(lat, lng, radiusMeters)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.build()
geofencingClient.addGeofences(request, pendingIntent)
.addOnSuccessListener { /* Geofence registered */ }
This approach shifts the burden of location monitoring to the OS's fused location provider. Because the OS batches these location signals across multiple apps, the impact on battery life is significantly lower than manual polling. The key tradeoff here was losing the ability to define highly granular, non-circular shapes, but for the purpose of silencing a phone in a specific building or office, a circular geofence is more than sufficient. By delegating the monitoring to the system's low-power hardware geofencing engine, I ensured that Muffle remains almost invisible to the battery stats while still acting with high precision when the transition occurs.
What surprised you / what you'd do differently
What truly caught me off guard during development was how aggressively Android kills background processes, regardless of how well-optimized they are. I assumed that registering a PendingIntent for a geofence transition would be enough to wake my app reliably. I was wrong. On certain OEM devices—specifically those with very aggressive custom battery management layers—my BroadcastReceiver would sometimes fail to trigger simply because the system had put the entire app into a deep sleep state, effectively ignoring the system-level intent.
I learned the hard way that you cannot treat the Android lifecycle as a guarantee. To fix this, I had to implement a Foreground Service as a fallback heartbeat. While this seems counter-intuitive to saving battery, it ensures that the GeofencingClient remains active in the eyes of the OS task manager. If I were starting over, I would spend much more time initially investigating WorkManager for the logic execution part of the trigger. Instead of handling the sound profile change directly inside the BroadcastReceiver, I now offload that to WorkManager, which is much more resilient to the unpredictable nature of background process execution across different Android versions.
Another lesson learned was the importance of the radiusMeters parameter. I initially set it to 50 meters, assuming high precision was better. However, in dense urban environments with poor GPS signal or high "GPS jitter," a 50-meter radius caused the device to rapidly toggle between 'Silent' and 'Normal' as the GPS coordinates drifted. Increasing the default to 100 meters, combined with a simple cooldown timer, solved the flicking issue. It’s a classic example of where real-world physical constraints override theoretical code perfection.
Practical takeaway
If you are building an app that relies on location, stop trying to be the "smartest" process on the phone. Do not build your own location polling loop. Instead, lean heavily on the GeofencingClient and WorkManager. The Android system is designed to manage resources in aggregate, and fighting against that by trying to keep your process alive 24/7 is a losing battle that only leads to uninstalls when users see your app at the top of their battery usage list.
Always design for the "worst-case" device, where the OS is constantly looking for reasons to kill your background tasks. If your code can survive on a device that is aggressively trying to put it to sleep, it will run perfectly on everything else. When you build tools that respect the user's hardware constraints, you end up with a product that feels like a native extension of the OS rather than a clunky add-on. Muffle was born from these lessons, and you can see how I approached these limitations at https://play.google.com/store/apps/details?id=com.muffle.app if you are interested in the implementation.
Top comments (0)