ConnectivityManager / Kotlin / NetworkCallback / WifiManager

SSID Connector – Compatibility Wrapper

Recently a friend and former colleague Robert Sproats approached me regarding some issues he was having. He asked if I was aware of best practices for connecting to a specific WiFi SSID on modern and legacy devices. Android Q deprecated the WifiManager APIs for this and they no longer work for Q and later devices. There are some newer APIs to do the same thing. I haven’t personally seen any best practice for handling both mechanisms documented. This three part series covers my approach to doing this using modern Android development techniques.

In this second article in the series we’ll look at the specific problem that prompted this series: How to perform the connection to and disconnection from a specific Wifi SSID in a backwards compatible way. Deprecated APIs will often continue to work after they’re deprecated. But that is not case here. We’ll need to use one technique on Q and later devices and another for earlier devices. To achieve this we’ll use a pattern that I’ve used before, and is a very useful compatibility wrapper.

Sealed class

We saw in the previous article that the WifiConnector class is a sealed class, but it was not obvious why. We hide the implementation details within concrete subclasses of the sealed class:

sealed class WifiConnector constructor(
    protected val wifiManager: WifiManager,
    protected val connectivityManager: ConnectivityManager
) {
    .
    .
    .
    abstract suspend fun connect(ssid: String, passphrase: String)
    abstract suspend fun disconnect()

    companion object {
        operator fun invoke(
            wifiManager: WifiManager,
            connectivityManager: ConnectivityManager
        ): WifiConnector {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                QWifiConnector(wifiManager, connectivityManager)
            } else {
                LegacyWifiConnector(wifiManager, connectivityManager)
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.Q)
    private class QWifiConnector(
        wifiManager: WifiManager,
        connectivityManager: ConnectivityManager
    ) : WifiConnector(wifiManager, connectivityManager) {
        .
        .
        .
    }

    @Suppress("Deprecation")
    private class LegacyWifiConnector(
        wifiManager: WifiManager,
        connectivityManager: ConnectivityManager
    ) : WifiConnector(wifiManager, connectivityManager) {
        .
        .
        .
    }
}

Previously we looked at the connectivityFlow in the base class, and we add a couple of abstract methods and an invoke() operator in a companion object. Also we add the concrete implementations for both Q and legacy implementations. I have left out the detail for now to keep the focus on how we hide the implementation details.

The invoke() operator is quite interesting here because it behaves like a constructor. WifiConnector cannot be instantiated directly because a sealed class is implicitly an abstract one. The two concrete implementations both have private visibility, so a consumer cannot directly instantiate them either. That only way to obtain an instance of WifiConnector is to use its the invoke() operator.

A consumer only knows about WifiConnector and is agnostic of both QWifiConnector and LegacyWifiConnector.

That is the basic pattern that we use here. We don’t need to be able to see the concrete subclasses to understand that a consumer knows nothing about them either. Moreover the logic for determining which to use is contained within the invoke() method. So a consumer of this does not even know that there are different underlying implementations. Neither does it have to worry about API level checking.

Hopefully it is fairly obvious why I like this pattern!

LegacyWifiConnector

Let’s look at the legacy implementation:

sealed class WifiConnector constructor(
    protected val wifiManager: WifiManager,
    protected val connectivityManager: ConnectivityManager
) {
    .
    .
    .
    @Suppress("Deprecation")
    private class LegacyWifiConnector(
        wifiManager: WifiManager,
        connectivityManager: ConnectivityManager
    ) : WifiConnector(wifiManager, connectivityManager) {

        private var netId: Int = -1

        override suspend fun connect(ssid: String, passphrase: String) {
            val wifiConfig = WifiConfiguration().apply {
                SSID = "\"$ssid\""
                preSharedKey = "\"$passphrase\""
            }
            netId = wifiManager.addNetwork(wifiConfig)
            wifiManager.disconnect()
            wifiManager.enableNetwork(netId, true)
            wifiManager.reconnect()
        }

        override suspend fun disconnect() {
            if (netId != -1) {
                wifiManager.disconnect()
                wifiManager.removeNetwork(netId)
                netId = -1
                wifiManager.reconnect()
            }
        }
    }
}

Before we go any further, I must mention that this is a simplified implementation. It makes assumptions such as WPA2 pre-shared key is used which may not always be the case. Production code will need to be a bit more flexible than this. But I wanted to keep the focus on the compatibility wrapper here, hence the simplification.

The old way of connecting to a specific SSID was to create a WifiConfiguration instance containing the network details, then add this using WifiManager to obtain a network ID. then disconnect from the current Wifi network before enabling the network ID that we just obtained. Finally call reconnect() to force a connection to this network.

To disconnect, we first disconnect from the current network. Then we remove the network ID before calling reconnect() which will reconnect to the default Wifi.

QWifiConnector

Let’s now take a look at the version for Android Q and later:

sealed class WifiConnector constructor(
    protected val wifiManager: WifiManager,
    protected val connectivityManager: ConnectivityManager
) {
    .
    .
    .
    @TargetApi(Build.VERSION_CODES.Q)
    private class QWifiConnector(
        wifiManager: WifiManager,
        connectivityManager: ConnectivityManager
    ) : WifiConnector(wifiManager, connectivityManager) {
        private val callback = object : ConnectivityManager.NetworkCallback() {}

        override suspend fun connect(ssid: String, passphrase: String) {
            val networkSpecifier: NetworkSpecifier = WifiNetworkSpecifier.Builder()
                .setSsid(ssid)
                .setWpa2Passphrase(passphrase)
                .build()
            val networkRequest: NetworkRequest = NetworkRequest.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .setNetworkSpecifier(networkSpecifier)
                .build()
            connectivityManager.requestNetwork(networkRequest, callback)
        }

        override suspend fun disconnect() {
            connectivityManager.unregisterNetworkCallback(callback)
        }
    }
    .
    .
    .
}

There are similar assumptions made here as the legacy implementation to keep the code simple.

The connection and disconnection techniques are not dissimilar to the legacy version, so I won’t bother with a line-by-line explanation. The important thing to note is that here everything is done via ConnectivityManager whereas the legacy version used WifiManager. Having completely independent subclasses of WifiConnector allows us to keep each one completely focused on its own implementation. As a result, each of the concrete subclasses is relatively simple.

Abstraction Point

The abstraction point is what I like about this pattern. We could write a single connect() method in WifiConnector which would contain the API level logic to select the correct API to use. Then we would also need to duplicate that logic in the disconnect() method. By leveraging a sealed class we isolate the specifics within each concrete subclass. There is a single if statement in the create() method which does all of the API level logic.

Conclusion

We have a complete WifiConnector implementation. The real power of this is how easy it is to consume.

I usually like to publish working code with each article but in this case we only have a producer and not the consumer code. Everything we have covered so far is actually in the code snippets for this article and the previous article, so that will suffice for now. I will publish the full source with the next article, I promise.

In the final article in this series we’ll look at how to use this within a modern Android app.

© 2020, Mark Allison. All rights reserved.

Copyright © 2020 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. This is one great article. As always, though.
    I’ve found two typos, if you don’t mind:
    “… is contained within the create() method.” ->perhaps “… is contained within the inoke() method.” implied
    “Having completely independent subclasses of WifiManager…” -> “Having completely independent subclasses of WifiConnector”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.