DEV Community

Architecting a 100% Offline Geofencing Engine for Android

The Problem: Social Friction and Privacy Trade-offs

It happened during a quiet afternoon at the local library. I was deep into a debugging session when my phone suddenly decided to blast a notification sound at full volume. The silence of the room shattered, and twenty people turned their heads in unison. I fumbled to silence it, but the delay was enough. That specific, sinking feeling of social friction-the kind that happens when your device refuses to respect the environment you are in-stayed with me long after I left that room.

We all have these moments. You are in a meeting, a lecture, or a place of worship, and you simply forget to toggle that silent switch. Or worse, you silence it for the event and then completely forget to turn it back on, missing urgent calls for the rest of the day.

The existing solutions on the Play Store were either bloated with telemetry trackers or relied entirely on cloud-based triggering, which felt like a massive privacy trade-off. I wanted a way to manage sound profiles based on location without my device constantly pinging a server to figure out where I was.

The friction isn't just about the silence; it is about the mental load. If I have to manually check my phone to see if it is in the right mode, the automation has already failed its primary purpose.

Design Goals

I needed a system that could handle geofencing, time-based triggers, and even calculated prayer times, all while remaining strictly offline. No analytics, no server-side lookups, and definitely no battery drain that would make the user regret installing the app in the first place.

Technical Approach: GeofencingClient with Custom Logic

To build this, I leaned heavily on the GeofencingClient provided by Google Play Services, but I had to wrap it in a custom logic layer to prevent it from eating the battery.

The primary challenge with geofencing on Android is the balance between accuracy and wake-locks. If you set the responsiveness too tight, the GPS radio stays active for too long, draining the battery in a few hours. If you make it too loose, the user walks five minutes into their meeting before the silent mode triggers.

I opted for a hybrid approach using PendingIntent to wake the receiver only when the boundary transition occurred. The critical technical decision was to avoid keeping a foreground service running for the GPS itself. Instead, I register geofences as ephemeral triggers. When the system detects an entry or exit event, it broadcasts to my BroadcastReceiver, which then checks the AudioManager to set the RINGER_MODE accordingly.

val geofencingRequest = GeofencingRequest.Builder().apply {
    addGeofences(geofenceList)
    setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
}.build()

val intent = PendingIntent.getBroadcast(
    context,
    0,
    Intent(context, GeofenceBroadcastReceiver::class.java),
    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

geofencingClient.addGeofences(geofencingRequest, intent)

By keeping the state logic inside an offline database-using Room-the app doesn't have to query the network to see if a routine should be active. It just compares the current system time and location against the local SQLite store.

This decoupling of the trigger (the geofence) from the action (the sound profile) allowed me to create a priority system. If a user is in a location that overlaps with a specific time-based routine, the app evaluates the priority integer assigned to each routine. The highest priority wins, ensuring that a 'Meeting' routine always overrides a general 'Work' routine, regardless of when they were triggered.

Lessons Learned: Manufacturer Fragmentation

What surprised me most during development was how much the LocationManager behaves differently across manufacturers. I assumed that if I requested a geofence, the OS would handle it uniformly. I was wrong.

On some devices, especially those with aggressive battery management like some older Huawei or Xiaomi models, the system would kill the GeofencingClient background tasks if the user hadn't opened the app in a few days. My initial implementation relied on standard background execution, which failed miserably because the OS assumed the app was idle and zapped the registered geofences to save power.

To fix this, I had to implement a 'Resurrection' logic:

  • Every time the phone reboots, I use a BOOT_COMPLETED receiver to re-register all geofences from the local database.
  • I also added a check that runs on a PeriodicWorkRequest via WorkManager to ensure that if the geofences were dropped by the OS, they get re-added.

This was a hard lesson in Android lifecycle management: never trust the OS to keep your background tasks alive indefinitely. You have to be proactive about re-establishing your state after any system event.

Indoor Accuracy: The Hysteresis Buffer

Another assumption I got wrong was that GPS is always the most accurate trigger. In indoor environments, GPS signal degradation is significant. If a user is in a basement office, the geofence might 'flicker' as the GPS coordinates drift, causing the phone to repeatedly toggle between silent and normal modes.

I solved this by implementing a 'hysteresis' buffer. The app now requires a transition to be sustained for at least 30 seconds before it fires the AudioManager change. This prevents rapid toggling and saves the user from the annoyance of a phone that cannot decide what mode to stay in.

Future Directions

If I were starting over, I would move away from the standard GeofencingClient for everything. For highly localized triggers, like a specific desk in an office, I would experiment with Bluetooth LE beacon scanning. The GPS approach is excellent for general areas, but it is imprecise for small-scale indoor environments. However, for a general-purpose utility, the current implementation provides the best balance of battery efficiency and performance without requiring the user to carry extra hardware.

Key Takeaways for Android Developers

For any Android developer working on background tasks, the biggest takeaway is this: minimize your wake-locks. Android is designed to punish apps that keep the CPU awake. Use the WorkManager for persistent tasks and trust the system's scheduling where possible. If you need to perform an action based on location, do the heavy lifting in a background thread or a Worker, and only touch the UI or system settings when absolutely necessary. Most developers try to do too much inside the BroadcastReceiver, which leads to ANRs and battery drain complaints.

Conclusion

Building Muffle has taught me that users value reliability over a massive feature list. They want an app that 'just works' and stays out of their way. By focusing on local-first data and battery-conscious triggers, I managed to build something that feels like an extension of the Android OS rather than an intrusive background process.

If you are struggling with similar issues or just want to see how I handled the prayer time calculations alongside the location logic, you can take a look at the implementation details here: https://play.google.com/store/apps/details?id=com.muffle.app. It is a work in progress, but it's a solution that finally lets me sit in meetings without that lingering fear of a loud, misplaced ringtone.

Comments

No comments yet. Start the discussion.