Connecting to Wi-Fi Devices With Rxjava in Android
Last year’s Android 10 announcement came with many new features like Gesture navigation, Dark theme, Location control and more. If your apps haven’t yet been migrated to the newest version, go ahead and do it! There have also been many changes at the API level, so check this link before switching to Android 10.
Well, enough announcements about Android 10 since Android 11 Beta is already out. š¤
IoT (Internet of Things) is gaining popularity these days and working with Wi-Fi on Android can be challenging. Using BLE Technology is almost the same thing. When you just think that there are hundreds of different types of devices, different versions of Android, hardware, and so on, it’s already getting tougher. Testing of such use cases can be a bugger. Thanks to the open-source contributors and Google itself, we have lots of new test frameworks and stable APIs. If you are interested in BLE, there are some good articles written about it.
In this article, let’s look at the three most important parts of using Wi-FI APIs: Scanning for Wi-Fi devices, Connecting to a particular device, Communicating with a device and Disconnecting from a device using RxJava.
š Setup
To set up the app in a state where all these parts work flawlessly, by being able to scan and connect to the Wi-Fi devices without a problem, we need to have some permissions and services allowed.
We must request for location permissions, enable the Wi-Fi Adapter and enable the Location Services, otherwise might not work.
Before accessing any of the code below, we should add these checks:
-
Enabling location permissions
On Marshmallow devices, we must request for location permissionā-āManifest.permission.ACCESS_FINE_LOCATION
. For that purpose, I usually use the PermissionDispatcher library, but there many other libraries you can use (or just write your own code using the Android docs). -
Enabling Wi-Fi adapter
Here you can check if the Wi-Fi adapter is enabled and force the user to enable it before continuing to the next step. There are other options where you can enable it from the code directly, but I don’t recommend it, as you are not letting the user know that you are enabling the Wi-Fi adapter. And it no longer works on Android 10. š
- Enabling Location Services
For this purpose, I’m using Google Play Services library. It shows a popup where the user can enable these services by clicking accept. Just add this dependency in your project.
implementation 'com.google.android.gms:play-services-location:17.0.0'
The onSuccessCallback
is called only if the user clicks accept.
In Kotlin you can use it like this:
checkLocationSettings {
// the user has allowed the locating services.
}
We now have all these services and permissions enabled, and we can continue communicating with the device.
š 1. Scanning for Wi-FiĀ devices
Scanning for a device is very simple. In order to wrap the broadcast receiver so it becomes reactive, we use the Completable.create
method from RxJava.
After registering a broadcast receiver with a filter SCAN_RESULTS_AVAILABLE_ACTION
, just make sure to filter out the right SSID from the result you are looking for and emit onComplete
if the SSID is right. Otherwise emmit tryOnError
. (We use tryOnError
, just because if the Rx flow is cancelled before the scanning is finished, we don’t get the UndeliverableException
).
ā¤ļø 2. Connecting to aĀ device
Now, after scanning has been done successfully, we can connect to the device as we know that it is nearby.
For older APIs from Android 9 andĀ below:
We need to wrap the broadcast receiver as we did before with Completable.create
and register the callback with a filter for listening network changes WifiManager.NETWORK_STATE_CHANGED_ACTION
. After calling WifiManager#addNetwork
and then [WifiManager#enableNetwork
](https://developer.android.com/reference/android/net/wifi/WifiManager#enableNetwork(int,%20boolean), the broadcast receiver receives a response when the connection is being changed, so we need to check the right SSID as long as the device connects. We finish it by calling onComplete
.
For further communication with the device using REST API, socket or whatever the device supports, we need to have this Network instance accessible. We can return the Network instance like this:
For newer APIs running Android 10 andĀ later:
By implementing the WifiConnectUseCase
you will provide support for devices on Android 10 and above. Just make sure you have targedSdkVersion 29
in the app’s build.gradle
file.
This method is similar to the previous one, but instead of a broadcast receiver, it uses callbacks. The only difference here is that after calling a ConnectivityManager#requestNetwork
, it triggers a popup where the user should agree to be connected to the given Wi-Fi network.
Getting a response onAvailable
in the callback means that the app is successfully connected to the device. If we get a onUnavailable
, it usually means that the user has denied the popup.
So, before calling any of these use cases make sure you check the current Android SDK version like this:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
// use WiFiConnectUseCase.kt
} else {
// use WiFiConnectLegacyUseCase.kt
}
š” 3. Communicating with aĀ device
Let’s say the device doesn’t provide an internet connection and your phone has LTE ON. That means your phone will still be connected to the Wi-Fi device, but all the requests will go through the LTE channel even though you are connected to the right device.
You must bind the network after a successful connection to make sure communication stays on the right channel.
That is why you need to explicitly use ConnectivityManager#bindProcessToNetwork(network)
in order to make sure your communication stays on the right process.
Binding to a network:
When you are done communicating, unbind from the network by passing a null
value. This action lets the device to decide for itself which network to use as a primary internet connection (usually, the mobile-LTE network is faster to connect to). If the phone is not using LTE, it needs a couple of seconds to reconnect to the original Wi-Fi which was using before.
Accessing theĀ device
If your device supports REST API, you can simply use Retrofit to communicate with the device as you did before with any other REST API.
Usually, the API URL will be something like a raw IP, for example, https://192.168.1.20. Newer versions of Android will reject these calls and you will get a ERR_CLEARTEXT_NOT_PERMITTED error, as these calls are considered not safe. In order to fix this, follow this article.
š 4. Disconnecting from aĀ device
There are two types of disconnecting from a Wi-Fi connection, depending on whether it is the new (Android 10 and above) or the old API.
For the newer API (Android 10) pass the networkCallback you want to unregister, that you got from the WiFiConnectUseCase:
connectivityManager.unregisterNetworkCallback(networkCallback)
For older APIs (below Android 10), just pass thenetworkIdyou want to remove, that you got from the WiFiConnectLegacyUseCase:
wifiManager.removeNetwork(networkId)
Here is the code, which supports both cases:
Extra: Using it in a ViewModel
Using these use cases into a ViewModel is pretty straight forward. Hopefully, you are already familiar with the use of ViewModels and MVVM architecture.
šŖ Conclusion
Working on Wi-Fi devices can give us some hard time and it takes a lot of patience. Thankfully, the APIs these days are more descriptive, easy to use and the Android developer’s documentation is well written too.
Please feel free to leave a comment below. If you have any issues with the code, I’d be happy to help. And, if you are not using RxJava in your case, there is a great article on how to connect to devices using Android 10.
Cheers! š»