DEV Community

Haseeb
Haseeb

Posted on

Architecting a Low-Power Geofencing Engine: Lessons from the Android Location API

It was the middle of a Friday afternoon prayer session at the local masjid when my phone decided it was the perfect time to play a notification sound for a trending news alert. The sharp, digital chime cut through the silence like a physical blow. Around me, heads turned. My face burned with embarrassment as I fumbled to silence the device, realizing I had once again forgotten to toggle my sound profile before entering. That moment of collective disruption felt avoidable, yet I realized I was trapped in a cycle of human error.

The problem

We live in a world of constant digital distraction, and our phones are designed to demand our attention. Whether it is a medical appointment, a class, or a prayer, there are specific environments where a ringing phone is not just an annoyance but a social failure. Manually toggling sound settings is a fragile solution because it relies entirely on memory. If I remember to silence my phone, I inevitably forget to unmute it afterward, missing urgent calls from family or colleagues.

I searched for existing solutions, but most were either overly complex automation suites that required a degree in computer science to configure or lightweight apps that hammered the battery by constant GPS polling. I wanted something that felt like a native system feature: set it once, and let the OS handle the transition. The friction wasn't just about sound; it was about the cognitive load of managing my phone's status based on my physical location. I needed an architecture that could detect geofence transitions without keeping the GPS hardware active 24/7, which is the primary battery killer on Android devices.

The technical decision / implementation

When I started building Muffle, I initially considered writing a background service that polled the user's location every few minutes. I quickly realized this was a recipe for battery disaster. Instead, I pivoted to the GeofencingClient within the Google Play Services Location API. This approach offloads the monitoring to the system rather than the app itself. The OS essentially handles the heavy lifting, batching location updates and only waking up my app when a specific transition occurs.

To implement this, I define a list of Geofence objects. Each object contains a latitude, longitude, radius, and the transition types (entry or exit). These are then wrapped in a GeofencingRequest. The key here is using PendingIntent to trigger a BroadcastReceiver. This allows the app to remain dormant while the system tracks the location in the background.

kotlin
val geofence = Geofence.Builder()
.setRequestId("work_office")
.setCircularRegion(lat, lng, 100f)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build()

val request = GeofencingRequest.Builder()
.addGeofence(geofence)
.build()

geofencingClient.addGeofences(request, geofencePendingIntent)

The decision to use a BroadcastReceiver instead of a persistent foreground service for location tracking was deliberate. While a foreground service is necessary for the final sound state management, it shouldn't be responsible for the raw location arithmetic. By offloading the geofence monitoring to the system's fused location provider, I minimized the wake-locks and CPU cycles required. When the system detects an entry or exit, it fires the PendingIntent, which then triggers my IntentService to check the current routine and toggle the AudioManager accordingly. This architecture keeps the app dormant during transit, only waking it up at the exact moment of boundary crossing. It is the difference between a phone lasting two hours versus two days.

What surprised you / what you'd do differently

One of the most humbling lessons I learned was the unreliability of GPS inside buildings. I initially set my geofence radii to 50 meters, assuming high accuracy. In reality, the signal drift in dense urban environments or large office buildings was massive. My phone would trigger an "exit" event just by me moving from the front desk to the back conference room, causing the sound profile to flip back to normal while I was still technically at work. I had to increase the radius to 150 meters to account for this horizontal accuracy variance, which introduced a new problem: the triggers were no longer precise.

Another shock was how aggressively modern Android versions—specifically from Android 12 onwards—kill background processes. Even with the correct permissions, I found that if the user had aggressive battery optimization enabled, the BroadcastReceiver would occasionally fail to fire reliably. I eventually had to implement a foreground service with a persistent notification to ensure the system treated Muffle as a high-priority task. This was frustrating because I wanted the app to feel invisible, yet the platform design actively fights against background automation for the sake of battery health. If I were starting over, I would build a hybrid system that uses cell tower triangulation as a secondary check before confirming a GPS-based location change. Relying solely on the GeofencingClient is fine for simple use cases, but for something that manages device volume, you need multi-modal verification to avoid false positives.

Practical takeaway

If you are building an Android app that relies on location, stop trying to "outsmart" the system by writing your own polling loops. Use the system APIs, but understand their limitations regarding battery optimization and signal drift. Always design for the edge case where the system kills your process; your architecture must be able to recover and sync its state upon reboot or re-initialization.

Focus on the user's specific context rather than adding more features. The beauty of automation is not in how many triggers you have, but in how reliably the tool disappears into the background until it is needed. Automation is only useful if it is invisible and consistent. If you are struggling with these same problems of phone management and manual toggling, you can see how I approached these challenges in Muffle: https://play.google.com/store/apps/details?id=com.muffle.app. We, as developers, have the capability to build tools that genuinely improve our daily social interactions. Keep the logic simple, respect the platform's constraints, and always prioritize the user's battery life over your own desire for frequent state updates.

Top comments (0)