Monitoring Wi-Fi Connectivity Status in a post nougat world — Part 1

Exploring API changes, restrictions, and recommendation

Hussaini Hanging Bridge, Hunza (Pakistan) by Syed Mehdi Bukhari

We wanted to understand when the user enters or leaves concerned locations or Point of Interests (POI). It is easier said than done and we explored several solutions. Listening and keeping track of a Wi-Fi connection or disconnection status was one of the ways we wanted to try and test.

Getting it done on Android sounded easy because we knew there is a good old ConnectivityManager API to make it happen for us. As soon as we started implementing, it became more evident that things have quite changed since Android 7.0 (aka Nougat).

The fact remains, it is very difficult to keep track of all the changes in Android API with each new version update and many times you only find out once you have to deal with it.
Oh boy!

We wanted to achieve these two goals:

  • The app should be able to monitor Wi-Fi connection status as real-time as possible (will cover this later)
  • The app should be able to fetch information about SSID (service set identifier) which is simply the technical term for a network name

Additionally, we wanted this feature to work in all possible scenarios:

  • Even when the app is in background or foreground
  • Even when the app is in killed state
  • Even when the phone is restarted
We went through quite a struggle to sort things out for ourselves and sharing our learnings and experiences here so that maybe it can save some time for you.

Approach #1: Broadcast Receiver with CONNECTIVITY_ACTION

Android system sends broadcasts when various system events occur which are referred to as “System Broadcasts”.

The good old way to monitor for changes in connectivity is by registering a receiver for the implicit CONNECTIVITY_ACTION broadcast in the manifest.

<receiver android:name=".NetworkChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>

Problem with this approach? Since many apps register to receive this broadcast, a single network switch can cause them all to wake up and process the broadcast at once.

Hence, Android 7.0 (API level 24) added this restriction that apps targeting 7.0+ will not receive CONNECTIVITY_ACTION implicit broadcasts anymore if they register to receive them in their manifest, and processes that depend on this broadcast will not start.

The recommended approach is to register explicitly through the context-registered receiver.

val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
}
registerReceiver(br, filter)

What does it mean for us?

Context-registered receivers receive broadcasts as long as their registering context is valid. So, even if you register with the Application context, you receive broadcasts as long as the app is running but not beyond that. Additionally, listening for CONNECTIVITY_CHANGE broadcasts doesn’t guarantee that the device has an active network connection, only that the connection was recently changed.

This wasn’t satisfying our criteria because there’s a high probability that the app will be in the killed state but we still want to monitor connectivity change.

Even if we consider it, it is not worth it because CONNECTIVITY_ACTION is already deprecated in Android 9.0 (API level 28)

We had no choice but to look for alternate options.


Approach #2: Foreground Service

The biggest challenge for us was to find a solution which allows maximum real-time monitoring while not killing battery life or causing bad user-experience.

If you need to get total control of real-time monitoring, Foreground Service is our best option. You can register a broadcast receiver for CONNECTIVITY_CHANGE in that service to listen continuously for the connection changes. Foreground Service belongs to the second most important process group.

A visible process is doing work that the user is currently aware of, so killing it would have a noticeable negative impact on the user experience. These processes are considered extremely important and will not be killed unless doing so is required to keep all foreground processes running.

This approach involves implementing a Service that is running as a foreground service, through Service.startForeground() which tells the system to treat this service as something the user is aware of, or essentially visible to them.

Pros: There are very fewer chances of it being killed and can continue monitoring even if the main app is killed.

Cons: User will always see the Foreground Notification and could lead to poor user experience.

But if you end up choosing this approach, here are some points to keep in mind:

  • Some manufacturers may give you a hard time to keep your service running as they might incorporate severe background limits for battery optimization.
  • You can never be sure that your service is not killed at any time due to memory pressure or low battery. The only thing you can do to mitigate the risk is to persist everything in the service in e.g. a Room database and start the service with START_STICKY.

As a general guideline, you should do as little as possible in the background (or foreground) service and keep your service smaller so that it is less likely to be killed.

What does it mean for us?

We didn’t favor this approach because our use case couldn’t afford to let users know that they are being actively tracked. Also, there was a risk of draining the user’s battery.


Approach #3: Schedule network jobs on unmetered connections

Later, as part of the Android 8.0 (API level 26) Background Execution Limits, apps that target the API level 26 or higher can no longer register broadcast receivers for implicit (except these) broadcasts in their manifest.

Android is strongly discouraging use of implicit intents like CONNECTIVITY_CHANGEbut at the same time, it provides several solutions to mitigate the need for implicit broadcast.

Schedule Network Operations

Android recommends to schedule network operations when specified conditions, such as a connection to an unmetered network, are met. This was something good to know but it required extensive testing to be really sure if it will serve our purpose.

To handle scheduling on Android, we have two options which include JobScheduler and WorkManager. In our case, we were confident about using WorkManager as it allows guaranteed completion, regardless of whether the app process is around or not. WorkManager is part of Android Jetpack and resumes operation after a Doze-Standby mode which is a big plus. There are two kinds of work scheduling request which can be triggered in WorkManager and referred to as OneTimeWorkRequest and PeriodicWorkRequest. The purpose of both of them should be quite clear from the name. We can’t use OneTimeWorkRequest in this scenario because once conditions are met and it reads connectivity status, it won’t run again. On the contrary, we want the app to keep listening to connectivity change unless told not to hence PeriodicWorkRequest was the appropriate choice.

This is a code snippet which shows how to schedule a kind of job we need.

PeriodicWorkRequest
.Builder(WifiWorker::class.java, wifiWorkerRepeatInternal,
TimeUnit.MINUTES, wifiWorkerFlexInterval, TimeUnit.MINUTES)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiredNetworkType(NetworkType.UNMETERED)
.build())
.build()
.also {
workManager.enqueueUniquePeriodicWork(wifiWorkerTag, ExistingPeriodicWorkPolicy.REPLACE, it)
}

There are three components which require some explanation:

  • Custom Worker Class
  • Constraints
  • Interval

Custom Worker Class

This will be your custom class extending from Worker which will basically perform the work you want to perform when specified conditions are met. The Worker will run whatever is inside doWork() synchronously on a background thread provided by WorkManager.

Constraints

NetworkType.CONNECTED: Trigger only when connection state of device changes

NetworkType.UNMETERED: Trigger only when the device connects to unmetered network aka Wi-Fi

This allows us to narrow down our filters to just Wi-Fi and not any other network and only when there is a chance in connectivity status.

Interval

We really had to think it through and test several variations to find what will work for us. In this case, Interval basically defines how much delay you can afford to have. You can read here about the differences between repeatInterval and flexInterval as it might be a bit out of the scope of this article.

Example showing when Repeating Interval is 8 hours and flex interval is 15 min

For us, we ended up using 30 minutes for repeatInterval and 25 minutes for flexInterval which means the job will trigger after 5 minutes in each cycle. This duration, during which the job gets executed is referred to as a flex period. It means that at the beginning of the flex period, WifiWorkerclass registers to listen to Wi-Fi Connectivity changes and keep monitoring.

What can go wrong? A lot actually! In an ideal case, the app is able to listen to all connection and disconnection states in the background during the complete flex period. Once one cycle finishes, the next job triggers again after 30 minutes and it’s all unicorns and sunshine. But many times, Doze mode can defer our WorkManager job or the Wi-Fi connectivity listener isn’t active anymore during the whole flex period. This is not in the developer’s control as these Doze mode restrictions are for a good reason.

Nevertheless, for our use-case this 30 min cycle allowed us to cover most of the cases with good enough confidence. So, even if we miss real-time timestamp of when the change happened, we knew that the next job will surely be able to check and update backend with Wi-Fi status.

One could argue that 30 min is a bit aggressive approach but it depends on case to case and we were able to live with this level of “realtime-ness”

If you were wondering to decrease the cycle duration, it’s important to keep in mind that repeatInterval can’t be less than 15 minutes and flexInterval can’t be less than 5 minutes.

What does it mean for us?

So far, we have established that scheduling network operation is our best approach considering our acceptance criteria and official recommendations. We went forward with this one and worked out fine for us.


In the second part of this article, we will explore how we can handle the monitoring of different Wi-Fi network states inside our Custom Worker class.

If you liked the article, come say hi on Twitter.