It happened during a quiet afternoon in the mosque. The imam was midway through a soft, reflective portion of the sermon when a sharp, melodic ringtone cut through the silence like a knife. Every head turned. The person whose phone it was scrambled to silence it, visibly flustered, but the damage was done. The atmosphere of concentration was shattered. I sat there with my own phone in my pocket, perfectly silent, because I had spent the last three weeks obsessing over why my previous automation attempts kept failing or draining my battery to zero by noon.
We have all been there. Whether it is a lecture hall, a medical appointment, or a board meeting, the social friction caused by a sudden, avoidable noise is universal. Existing solutions often fall into two camps: they are either battery-hungry monsters that keep the GPS radio pinned at all times, or they are inconsistent, failing to trigger the moment you step into the room. I wanted something that felt like it wasn't even there—a system that sat quietly in the background, consuming practically zero CPU cycles, and yet fired the exact second I crossed a physical threshold. Achieving this on modern Android, with its increasingly restrictive background execution policies, turned into a masterclass in resource management.
My initial approach was naive. I thought I could simply register a LocationListener and check the distance against my target coordinates inside a Service. I quickly realized that this is the fastest way to get a "Battery usage too high" warning from the OS. In Android 14, keeping the GPS radio active is a death sentence for any app's retention rate. If the system sees your app holding a wake lock or polling for location while the screen is off, it will kill your process without hesitation. I had to shift my entire mental model from "active polling" to "event-driven passive listening."
I pivoted to the GeofencingClient API, which is part of Google Play Services. Instead of managing the GPS hardware myself, I offload the heavy lifting to the system. The system maintains a low-power geofencing engine that only wakes up my application when a specific transition (entering or exiting) occurs. The architecture is straightforward: I define a Geofence object with a PendingIntent, and the system handles the hardware-level monitoring. When the boundary is crossed, the system sends an intent to my BroadcastReceiver, which then triggers the sound profile change.
kotlin
val geofence = Geofence.Builder()
.setRequestId("work_office")
.setCircularRegion(lat, lon, 100f)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.build()
By using PendingIntent with a BroadcastReceiver, I avoid keeping a service alive in the background. The receiver wakes up, performs the light task—adjusting the AudioManager—and then immediately terminates. The state management is handled by a local Room database, ensuring that even if the system kills the app process after the receiver finishes, the next trigger will simply pull the correct configuration from the database. It is efficient, robust, and respects the user's hardware constraints.
What surprised me most during this development was the "flapping" issue. When you place a geofence around a small area, like a specific office building or a prayer hall, the GPS signal can drift. If you are standing near the boundary, your phone might report that you have exited and entered the zone several times in the span of a few minutes. If your logic isn't prepared for this, your phone will start toggling between silent and normal modes rapidly, causing the notification sound to chime or the screen to wake up repeatedly. This was a nightmare to debug because it only happened in specific locations with poor signal reception.
I initially thought about adding a delay inside my BroadcastReceiver, but that would keep the process alive longer than necessary, which I was trying to avoid. Instead, I had to implement a debounce logic inside the database update layer. Before the app executes a sound profile change, it checks the timestamp of the last transition. If the last change occurred within the last two minutes, the app ignores the new signal. It sounds simple, but it required me to build a proper state machine rather than just a linear trigger. If I were starting over, I would have integrated this debounce logic into the Geofence setup itself using setLoiteringDelay(). The documentation mentions this, but I ignored it because I thought it only applied to entering. It turns out that setting a loitering delay is the most effective way to prevent false positives when you are just lingering near a boundary. I had to learn the hard way that the system's built-in filtering is always more efficient than anything I can write in user-space code.
Another lesson was the importance of the Priority field in my database. In my early versions, if two geofences overlapped—for example, one for my office and one for a nearby building—the sound settings would fight each other. The logic would oscillate between silent and vibrate. I had to build a priority-based ranking system where only the highest-priority rule can dictate the sound state at any given time. This taught me that automation is not just about triggers; it is about state conflict resolution. When you have multiple rules running, you must treat the sound profile as a single shared resource that requires a clear locking or priority mechanism.
As developers, we often obsess over the "happy path"—the code that works when the GPS is accurate and the user follows the expected routine. But true reliability in mobile development comes from handling the messy reality of hardware limitations. We need to respect the battery, anticipate the signal drift, and build systems that can fail gracefully. If your background task takes more than a few milliseconds, you are probably doing too much. Always look for ways to push the work back onto the system level, utilizing the APIs that allow the OS to manage resources on your behalf.
Automation should be invisible. It should be a utility that you set once and forget completely, like the auto-brightness feature on your phone. If you are building tools for Android, focus on the user's intent rather than the implementation mechanics. By leveraging the GeofencingClient and implementing a robust state machine to handle transitions, I was able to build Muffle, an app that manages sound profiles without the user ever needing to open it again. You can see how I approached these challenges at https://play.google.com/store/apps/details?id=com.muffle.app, where I have applied these principles to ensure the app remains silent and efficient, just like the phone should be.
Top comments (0)