It happened during a quiet Friday sermon at my local mosque. The room was silent, the atmosphere thick with collective focus, when suddenly, a rhythmic, high-pitched ringtone shattered the peace. I looked around, feeling the collective sigh of a hundred people shifting in their seats. It wasn’t my phone—I had remembered to silence mine—but it served as a brutal reminder of the friction we face daily. We live in a world of smart devices, yet we are still manually managing their sound states like it is 2008.
I realized that most people, myself included, simply forget to toggle their phone settings. Whether it is a college lecture, a medical appointment, or a board meeting, the human element of remembering to flip a hardware switch or dive into a quick-settings menu is inherently flawed. If the phone knows where it is, it should know how to behave. My goal for Muffle was to automate this process entirely, but I hit an immediate wall: I did not want to rely on the standard Google Play Services Geofencing API for my location logic. I wanted an offline, privacy-first, and battery-conscious solution that didn't ship the user's location data to external servers or rely on a massive, opaque library.
Building a geofencing engine from scratch without relying on com.google.android.gms.location requires a deep understanding of the Android LocationManager and the lifecycle of background services. I decided to implement a hybrid polling and proximity strategy. Instead of constant GPS pings, which are battery killers, I utilized requestLocationUpdates with a passive provider and a coarse location threshold. The core logic hinges on a simple distance formula: the Haversine equation. By calculating the great-circle distance between the user’s current coordinates and the defined routine boundary, I could trigger sound changes without needing a continuous, power-hungry GPS lock.
Here is a simplified look at the distance calculation I use in the background service:
kotlin
fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val earthRadius = 6371e3 // meters
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
return earthRadius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
}
I implemented this within a ForegroundService to ensure the OS does not aggressively kill the process under memory pressure. By pairing this with a BroadcastReceiver that listens for significant movement via the LocationManager.GPS_PROVIDER and LocationManager.NETWORK_PROVIDER, I created a trigger that only performs the heavy math when the device actually changes cell towers or moves a substantial distance. This drastically reduces CPU wake-ups compared to a naive polling approach. The trade-off is higher complexity in handling edge cases where the device loses signal, but the battery performance gain is significant.
What surprised me most during development was the volatility of the NETWORK_PROVIDER. I initially assumed that using cell tower triangulation would be a reliable, low-power supplement to GPS. I was wrong. In urban areas, cell tower shifting can occur frequently even when the user is sitting perfectly still. This led to 'false positives' where the app thought the user had exited a geofence simply because the device switched from a nearby tower to one slightly further away. I had to implement a 'debounce' mechanism, requiring the device to be outside the boundary for at least three consecutive polling intervals before the state actually changes.
Another lesson was the behavior of Doze Mode. Even with a ForegroundService, Android’s power management policies are extremely strict. I found that if I relied solely on my own timers, the system would eventually throttle my checks to once every few minutes. I had to integrate AlarmManager with setExactAndAllowWhileIdle to guarantee that my background checks would run at the correct time, even when the device was sitting on a desk with the screen off. If I were starting over, I would have spent more time building a more sophisticated location caching system to handle tunnels and basements where GPS signals drop to zero, rather than relying on a last-known-location fallback which can be stale by several kilometers in some scenarios.
For any developer working on location-based apps, the biggest takeaway is this: do not trust the hardware to be precise at all times. Always build for failure. Your location data will be noisy, your signal will drop, and the OS will fight your background processes at every turn. You must treat location as a suggestion rather than a fact. Implementing your own logic is incredibly rewarding because it forces you to understand the battery cost of every single sensor request. You stop seeing 'location' as an abstract API and start seeing it as a finite resource that you are borrowing from the user.
Automation should be invisible. If the user has to open the app to verify if the sound profile was changed, the system has failed. By building Muffle this way, I’ve ensured that the user’s privacy remains intact while the app stays quiet in the background, only waking up when it absolutely needs to make a decision. You can explore the implementation details and how I handle sound state transitions at https://play.google.com/store/apps/details?id=com.muffle.app. It is a work in progress, but it solves the exact problem I set out to fix without bloating the device with unnecessary third-party tracking or heavy dependencies. Automation is not about adding features; it is about removing the need for the user to think at all.
Top comments (1)
Official Platform Update
Security protocols have been updated for all developer accounts.