DEV Community

Haseeb
Haseeb

Posted on

Architecting Location-Based Sound Automation Without Draining Battery

It happened during a quiet afternoon at the community center. I was sitting in the front row, mid-prayer, when a sudden, jarring ringtone tore through the silence. It was my own phone. I had completely forgotten to silence it after coming from a loud, crowded office environment. The embarrassment was immediate and visceral. Everyone turned, the moment was shattered, and I felt that familiar, sinking feeling of being the person who caused the disruption. We have all been there—whether it is a medical appointment, a lecture, or a meeting, human memory is simply not reliable enough for these constant, repetitive manual tasks.

The problem

The friction here is twofold: the act of remembering to silence the device, and the equally common failure to restore the volume afterward. I spent weeks missing notifications because I had left my phone in silent mode from a meeting hours prior. I searched for solutions, but most automation apps on the Play Store were either bloated, riddled with invasive permissions, or seemingly designed to kill the battery in a single afternoon. Using a high-frequency polling service to check GPS coordinates every few seconds is a recipe for a dead phone by 2:00 PM.

I realized that the standard approach of waking the CPU to ping the location provider is fundamentally broken for a utility meant to run constantly. I needed a way to trigger sound profiles based on geofencing that could survive reboots, run in the background, and yet remain invisible to the system's battery optimizations. This wasn't just about building a feature; it was about building a background orchestration system that actually respects the user's hardware constraints rather than fighting against them.

The technical decision / implementation

When I started building Muffle, my first instinct was to write a custom Service that polled location data. I quickly realized this was a mistake. Android's LocationManager is aggressive, and keeping a wake-lock for background location is the fastest way to get your app restricted by the system. Instead, I pivoted to the GeofencingClient within the Google Play Services library. This API is significantly more efficient because it offloads the monitoring of the geofence to the system level. The OS handles the proximity calculations and only fires an intent to my app when a specific transition (entering or exiting a boundary) occurs.

However, even with the GeofencingClient, you cannot just rely on the OS. You need a robust way to handle state transitions. I implemented a JobIntentService to handle the incoming geofence transitions. This ensures that the work is queued properly even if the device is in a low-power state. Here is a snippet of how I handle the Geofencing transition in my broadcast receiver:

kotlin
val geofencingEvent = GeofencingEvent.fromIntent(intent)
if (geofencingEvent.hasError()) return

val transition = geofencingEvent.geofenceTransition
if (transition == Geofence.GEOFENCE_TRANSITION_ENTER) {
val triggeredGeofences = geofencingEvent.triggeringGeofences
triggerRoutine(triggeredGeofences.first().requestId, SoundAction.SILENT)
}

The real challenge was the persistence of these routines. If a user restarts their phone, all active geofences are cleared from memory. I had to build a BroadcastReceiver that listens for the ACTION_BOOT_COMPLETED intent. Upon reboot, the receiver queries my local Room database to retrieve the stored routines and re-registers them with the GeofencingClient. This ensures that the user doesn't have to manually re-enable their settings every time they update their phone or run out of battery. By shifting the heavy lifting to the system's own location-detection hardware, I managed to keep the app's footprint nearly zero in terms of background CPU usage.

What surprised you / what you'd do differently

I was genuinely surprised by how erratic the GeofencingClient behaves when a user is in a building with poor signal. I assumed the API would handle indoor/outdoor transitions gracefully, but I found that in urban environments with tall buildings, the GPS "dwell time" can cause the geofence to trigger intermittently as the signal bounces. I initially had a routine that would fire multiple times as the device flickered in and out of the location radius.

To fix this, I had to implement a debounce logic in my triggerRoutine function. It now checks the current state of the phone before performing any AudioManager calls. If the phone is already in the desired state, the app does nothing. I also learned the hard way about the Do Not Disturb permission restrictions. On Android 6.0 and above, you need to request the ACCESS_NOTIFICATION_POLICY permission. Many users don't realize this, and the app would crash silently if I didn't handle the SecurityException during the attempt to set the DND mode. If I were starting over, I would build a much more robust diagnostic screen that guides the user through these specific system permissions during the onboarding flow, rather than assuming they would understand why the app needs to modify notification settings.

Another assumption I got wrong was the reliability of AlarmManager for time-based triggers. I initially tried to use setExactAndAllowWhileIdle for everything, but it's too restrictive for frequent routine changes. I switched to a hybrid approach: geofencing for location and a combination of WorkManager for routine management. WorkManager is much more reliable for tasks that need to run eventually, even if the device is undergoing battery optimizations or Doze mode. It handles the scheduling logic far better than I ever could manually, and it respects the system's resource constraints.

Practical takeaway

As developers, we often want to build "all-powerful" applications that monitor everything at all times. But the modern Android ecosystem is designed to punish apps that behave like that. The key takeaway here is to stop building your own monitoring loops and start leveraging the system APIs that already do the heavy lifting for you. Whether it is GeofencingClient for location or WorkManager for background tasks, the system knows better than your code does about how to manage power effectively.

Think about the user's intent. They don't want an app that is "active"—they want an app that is "ready." By offloading the monitoring to the OS, you not only save battery but you also make your app more stable, as you're no longer fighting the system's aggressive background limitations. If you are building something that needs to react to location or time, focus on minimizing your app's presence when it is not needed. For those interested in seeing how this architecture looks in a production environment, you can check out the source-inspired logic behind Muffle at https://play.google.com/store/apps/details?id=com.muffle.app.

Top comments (0)