DEV Community

Haseeb
Haseeb

Posted on

Architecting Battery-Efficient Geofencing for Automated Sound Profiles

It was the middle of a Friday afternoon prayer session at the local masjid. The room was heavy with silence, a collective stillness that felt almost fragile. Suddenly, the sharp, upbeat jingle of a popular pop song blasted from somewhere in the back row. A hundred heads turned toward the culprit, who was frantically fumbling with their device, face turning bright red as they tried to swipe away the notification. I knew that feeling all too well. It is that specific, sinking sensation of being the source of a completely avoidable disruption.

We have all been there. You walk into a medical appointment, a classroom, or a job interview, and the intent to silence your phone is there, but the execution fails. You get distracted, the phone stays on loud, and minutes later, you are the person interrupting the room. Existing solutions were either manual, requiring me to remember to flip a toggle, or they were bloated, privacy-invasive automation apps that felt like overkill for a simple task. I didn't want a suite of home automation tools; I just wanted my phone to respect the environment I was currently in. I realized that the technology existed—GPS and scheduling APIs—but the implementation lacked a focus on reliability without nuking the battery life.

When I started building Muffle, the biggest technical challenge was the Geofencing API. I needed a way to trigger a sound profile change when a user entered a specific coordinate boundary, but I could not afford to keep the GPS radio running 24/7. That is a recipe for a dead battery in under four hours. The standard GeofencingClient in Android is actually quite efficient because it offloads the monitoring to the system rather than the app process. The system uses a combination of network location and low-power GPS signals to determine proximity. However, the catch is the callback handling. If you try to do too much inside the BroadcastReceiver that handles the geofencing transition, the system will kill your background task or throttle it to save resources.

I decided to use a combination of GeofencingClient combined with a ForegroundService. The GeofencingClient acts as the low-power watchman. When the system detects a geofence transition, it sends an intent to my BroadcastReceiver, which then triggers a JobIntentService or a WorkManager task to handle the sound profile change. The most critical part of this architecture is separating the detection from the action. I do not store the sound state in a volatile variable; I persist every routine into a local Room database. This ensures that even if the app process is completely destroyed by the system to free up memory, the service can wake up, query the database, and re-apply the correct AudioManager settings immediately.

kotlin
val geofencingRequest = GeofencingRequest.Builder()
.addGeofence(geofence)
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.build()

geofencingClient.addGeofences(geofencingRequest, geofencePendingIntent)
.addOnSuccessListener { /* Successfully registered / }
.addOnFailureListener { /
Handle registration error */ }

By using AudioManager.setRingerMode(), I could force the phone into silent or vibrate mode, but I had to handle the NotificationManager separately for Do Not Disturb (DND) because DND permissions are restricted on newer Android versions. The complexity wasn't just in the code; it was in the state machine management. What if a user enters a geofence while a calendar event is already active? I implemented a priority-based queue where each trigger has an assigned weight. If two triggers overlap, the one with the higher priority wins. It sounds straightforward, but testing this across different OEM implementations—like Samsung’s aggressive battery management—was a nightmare. I learned quickly that relying on standard Android behavior isn't enough; you have to account for how individual manufacturers silently kill background tasks.

What surprised me most during this journey was how much I underestimated the 'restoration' phase of the app. I assumed that if I set the phone to silent when entering a location, I could just set it back to 'Normal' when leaving. But that logic breaks immediately if the user enters two overlapping geofenced areas. If I leave the first one, the app would revert to 'Normal', overriding the second active geofence that should still be keeping the phone silent. I had to pivot to a stack-based approach where the app keeps track of all currently active routines. The phone only returns to 'Normal' when the stack is empty. I also found that relying on AlarmManager for time-based triggers is notoriously inaccurate on modern Android due to 'Doze' mode. I had to use setExactAndAllowWhileIdle to ensure that prayer times and scheduled meetings trigger precisely when they are supposed to, even if the device has been sitting on a desk for hours.

If I were starting over today, I would have invested more time in handling the 'Adhan' library and time-zone offsets earlier. I assumed that GPS coordinates would be enough to calculate prayer times accurately, but the reality is that the local custom of time offsets (like adjusting for high latitudes or specific regional conventions) varies significantly between countries. I had to refactor the entire calculation engine to support user-defined offsets, which was a massive headache after the initial release. Furthermore, I would have used DataStore instead of SharedPreferences from day one for the settings. SharedPreferences are synchronous and can block the UI thread during disk I/O, which led to minor stutters when the app was updating multiple routine statuses simultaneously. Moving to DataStore solved that, but it was a significant migration that I could have avoided with better initial planning.

Building Muffle taught me that the most effective background services are the ones that do the least. The goal is not to stay alive; the goal is to be woken up at the exact right moment, perform a single, atomic operation, and go back to sleep. If your app is constantly 'running' in the background, you are doing it wrong. The system will eventually punish you by revoking your background execution privileges or, worse, the user will uninstall you when they see your app at the top of their battery usage list. My advice for anyone building similar automation tools is to focus entirely on the persistence layer. If the user reboots their phone, does your app recover its state without user intervention? If it doesn't, your user will eventually stop trusting your app to handle their phone's settings. Trust is the most important feature you can build into a productivity tool. I am still iterating on Muffle, constantly tweaking the background logic to be even lighter, and I invite you to see how I've handled these constraints at https://play.google.com/store/apps/details?id=com.muffle.app. It is a work in progress, just like any good piece of software.

Top comments (0)