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

Listening for changes in network status and reading Wi-Fi info

Fun at the Chenab river (Pakistan) by Syed Mehdi Bukhari

In the first part of this article, we tried to understand how API changes since Android Version 7.0 (aka Nougat) have changed the way Android developers can listen to connectivity status change. We explored three approaches and established that scheduling network operations through WorkManager make much more sense considering official guidelines.


This second part is intended to cover what happens in the Custom Worker class, WifiWorker.kt which we specified in PeriodicWorkRequest.Builder(). We’ll talk about this Worker class in a bit.

Monitoring Wi-Fi connection status

The easiest way would be to just check inside doWork(), if you are connected to the Internet and if the type of current connection is Wi-Fi. Once boolean is true, you can note down the timestamp and location of the user.

val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
val isConnected: Boolean = activeNetwork?.isConnectedOrConnecting == true
val isWiFi: Boolean = activeNetwork?.type == ConnectivityManager.TYPE_WIFI

But this is not sufficient for our acceptance criteria. We wanted to listen to changes which keep on happening with Wi-Fi so that we can be more accurate about when and where that change happened. Luckily, Android offers a more robust solution through a callback for faster and more detailed updates about the network changes.

NetworkRequest is used to define a request for a network and we explicitly restrict it to only Wi-Fi changes.

val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()

NetworkCallback is the callback that the system will call as suitable networks change state. This is the most important part of the code. Here, onAvailable() gets called when Wi-Fi connects to a network and onLost() gets called when Wi-Fi disconnects from a network.

private var networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network?) {
logger.d("networkcallback called from onLost")
//record wi-fi disconnect event
}
override fun onUnavailable() {
logger.d("networkcallback called from onUnvailable")
}
override fun onLosing(network: Network?, maxMsToLive: Int) {
logger.d("networkcallback called from onLosing")
}
override fun onAvailable(network: Network?) {
logger.d("NetworkCallback network called from onAvailable ")
//record wi-fi connect event
}
}

Then, register both NetworkRequest and NetworkCallback using registerNetworkCallback() which is part of ConnectivityManager. This will tell the system that our intent is to receive notifications about network changes whenever they occur. This is something we wanted from the beginning. Using this requires the caller to hold Manifest.permission.ACCESS_NETWORK_STATE permission.

val connectivityManager =
context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
An important point to note is that the callback will continue to be called until either the application exits or unregisterNetworkCallback() is called.

The fact remains, we can’t control when an application process gets killed. This is basically why we have periodic WorkManager job and registration happens with each new periodic cycle. This enables listening to Wi-Fi connectivity changes continuously and reliably. To be on the safe side and being efficient with resources, we try to unregister before registering again.

//NetworkCallback that has been unregistered can be registered again
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (e: Exception) {
logger.w("NetworkCallback for Wi-fi was not registered or already unregistered")
}
//other code
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)

Also, since Android has deprecatedCONNECTIVITY_ACTION in Android 9.0 (API level 28) this is also the recommended solution as per documentation.


Reading SSID Information

At the beginning of the first article, we mentioned that one of the goals of the app is also to fetch information about SSID (aka Wi-Fi name) to map it with the building location.

Android 9.0 brought some new restrictions with using getConnectionInfo()

Retrieving the SSID or BSSID requires location services to be enabled on the device and calling app should have ACCESS_FINE_LOCATION and ACCESS_WIFI_STATE permissions.

We created these Kotlin extension functions to make it easier for us.

/**
* Fetches Name of Current Wi-fi Access Point
*
* Returns blank string if received "SSID <unknown ssid>" which you get when location is turned off
*/
fun WifiManager.deviceName(): String = connectionInfo.ssid.run {
if (this.contains("<unknown ssid>")) "UNKNOWN" else this
}

This is later used inside onAvailable()

override fun onAvailable(network: Network?) {
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager

val wifiName = wifiManager.deviceName()
}

Worker class and Threading

This is the basic skeleton of any Worker class and doWork() is where you have your code for background processing.

class WifiWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
override fun doWork(): Result {
  //Rest of the code comes here
  // If there were no errors, return SUCCESS
return Result.success()
}

An important point to consider is that WorkManager runs synchronously on a pre-specified background thread. Also, the documentation says that maximum time allowed for a worker to perform background operation is 10 minutes. What it means is that either when the time expires or you return from doWork() method, the Worker is considered to have finished what it's doing and will be destroyed.

If you call an asynchronous API in doWork() and return a Result, your callback may not operate properly

In our case, NetworkCallback is an asynchronous API but we don’t need to worry because ConnectivityManager handles its operations in a separate thread via ConnectivityThread. This is why NetworkRequest can outlive the life of our worker and even the calling application until unregistered or the calling application exits.

If this was not the case, we should have used ListenableWorker and you can listen more about it here or read it here.


To conclude, we managed to implement a good enough solution for our use-case which works and allows the app to react to Wi-Fi connectivity changes. But, we do hope that maybe the Android team can provide an easier way for developers in coming Android versions.

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