WiFiManager API in Android Q, R - android

I am writing a sample application which is scanning for available wifis and connects to them programmatically
For that propose, I'm using WiFiManager, but recently Google deprecated most of the methods that are allowing to scan for WiFis and Connect to them programmatically. And yes it seems logical from Google's side to not allow anyone to try to connect to any WiFi programmatically.
But my questions will be following
Is there still a way to Scan for available WiFi-s
Is there a way to connect to a Public WiFi
Is there a way to connect to a Private WiFi programmatically, or if no if there is a way to suggest user connect to a private WiFi, so user can enter a password
In the new API I found a way how I can suggest user connect to the WiFi
wifiManager.addNetworkSuggestions(listOf(WifiNetworkSuggestion
.Builder()
.setSsid("testWiFi")
.setWpa2Passphrase("test1234")
.build()))
but this is working really unpredictable, the first time when I run my all and call this method, on Android Q it's showing a notification and on Android R it's showing dialog asking the user if he allows connecting to the WiFi-s when app somehow connects to the WiFi but after that calling the same function for another WiFis does nothing and app is not showing any more ant notification or dialog (it this a bug? )
also I tried requestNetwoerk method
val specifier = WifiNetworkSpecifier
.Builder()
.setSsid("testWiFi")
.setBssid(MacAddress.fromString("testWiFiMac"))
.build()
val request = NetworkRequest
.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(specifier)
.build()
connectivityManager.requestNetwork(request, networkCallback)
but in this case, it's not very clear if it is possible to connect to password-protected WiFi, and if yes how to do that.
So if there are some examples or explanations, please let me know.

even if start scan is deprecated Google still recommends to use it.
The three steps here are:
Register a broadcast receiver for SCAN_RESULTS_AVAILABLE_ACTION.
Request a scan using WifiManager.startScan()
Get the scan results with WifiManager.getScanResults(). Note that if you are using these scan results before receiving the results from the broadcast, these results might be a bit older from previous scans.
Sample code might be look like this:
val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiScanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
if (success) {
scanSuccess()
} else {
scanFailure()
}
}
}
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)
val success = wifiManager.startScan()
if (!success) {
// scan failure handling
scanFailure()
}
....
private fun scanSuccess() {
val results = wifiManager.scanResults
... use new scan results ...
}
private fun scanFailure() {
// handle failure: new scan did NOT succeed
// consider using old scan results: these are the OLD results!
val results = wifiManager.scanResults
... potentially use older scan results ...
}
For devices running on Android Q or higher, the only method to properly to connect to a public or private wifi which is not intended as a peer to peer network is to use the NetworkSuggestionAPI as you already showed in your question.
The reason why you sometimes don't see the network suggestion is the following:
"If the user uses the Wi-Fi picker to explicitly disconnect from one of the network suggestions when connected to it, then that network is ignored for 24 hours. During this period, that network will not be considered for auto-connection, even if the app removes and re-adds the network suggestion corresponding to the network.", cited from the Google documentation website.
For more information about the wifi scanning look at the official documentation:
https://developer.android.com/guide/topics/connectivity/wifi-scan#kotlin
More information about the WifiNetworkSuggestion API can be found here:
https://developer.android.com/guide/topics/connectivity/wifi-suggest

Related

Wifi doesn't disconnect even after being removed from wifi network suggestion. How to disconnect user after removing wifi from network suggestion?

I am trying to connect and disconnect form a wifi programmatically in android(kotlin).
I am able to connect user to wifi using WifiNetworkSuggestion.
Now when I try to disconnect the user from the wifi(which was joined using our app WifiNetworkSuggestion), I am unable to disconnect user even after removing the user form suggestion list. Below is the code I am using :
fun disconnectFromNetwork(ssid: String, password: String) {
val wifiManager: WifiManager = this.applicationContext.getSystemService(
Context.WIFI_SERVICE
) as WifiManager
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
val suggestion = WifiNetworkSuggestion.Builder()
.setSsid(ssid)
.setWpa2Passphrase(password)
.build()
val suggestionsList: MutableList<WifiNetworkSuggestion> = ArrayList()
suggestionsList.add(suggestion)
wifiManager.removeNetworkSuggestions(suggestionsList)
}
else -> {
}
}
}
According to android documentation.
removeNetworkSuggestions:
Remove some or all of the network suggestions that were previously
provided by the app. If one of the suggestions being removed was used
to establish connection to the current network, then the device will
immediately disconnect from that network.
But the wifi connection persist even after removing the wifi from suggestion.
How to disconnect user after removing wifi from network suggestion?
This issue only seems to exist on API level 29. It works as intended for android with API level greater than 29.

How to restore Wi-Fi connection after using requestNetwork on Android

Background: I have an Android app which configures an IoT device's Wi-Fi connection:
The IoT device exposes an Access Point with a specific name
The app connects to the AP
The app sends the SSID & passphrase of the user's Wi-Fi network to the device
Both the device and the smartphone connect to the home Wi-Fi network.
I've had a working solution based on WifiManager.addNetwork & WiFiManager.enableNetwork, but these APIs are unavailable with targetSdkLevel set to 29 or above (which is a hard requirement now on Google Play).
The new API offered by Android is ConnectivityManager.requestNetwork. This works quite well during steps 1-3, but after disconnecting from the temporary network (by removing the network request with ConnectivityManager.unregisterNetworkCallback), the smartphone does not reconnect to the original Wi-Fi network - it stays disconnected from Wi-Fi until the user manually selects a Wi-Fi network in Settings. Even if there is no other data connection available.
Is there a way to force the phone to connect to its preferred Wi-Fi network after using requestNetwork?
It's not really apparent from the documentation, but on Android 10 (API level 29) and higher, you need to always call connectivityManager.unregisterNetworkCallback(networkCallback) when you don't need the Wifi connection anymore. Here is the code sample from the Android documentation:
val specifier = WifiNetworkSpecifier.Builder()
.setSsidPattern(PatternMatcher("test", PatternMatcher.PATTERN_PREFIX))
.setBssidPattern(MacAddress.fromString("10:03:23:00:00:00"), MacAddress.fromString("ff:ff:ff:00:00:00"))
.build()
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(specifier)
.build()
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
...
override fun onAvailable(network: Network?) {
// do success processing here..
}
override fun onUnavailable() {
// do failure processing here..
}
...
}
connectivityManager.requestNetwork(request, networkCallback)
...
// Release the request when done.
connectivityManager.unregisterNetworkCallback(networkCallback)
Please, take into account to always call the method from the last line.
As you mentioned, from targedSdkLevel 29 you cannot directly enable specific wifi network. But as documentation says you can add network suggestion, which "is used to provide a Wi-Fi network for consideration when auto-connecting to networks."
You can find it here

Android, communicate with mobile data while connected to wifi without internet access

I have an automotive companion app that needs to communicate with both wifi and mobile data networks.
My vehicle control unit provides a wifi network without internet access which exposes an API service that we can call from the app.
In addition to this we need to communicate with another backend reachable from the internet using phone mobile data (3G/4G).
I immediately noticed that some android phones, when connected to a wifi network without internet using android settings menu, show a system dialog informing user that current network has no internet access. Here user have two choises: keep this wifi network or disconnect and switch to another one.
Here some examples:
Samsung J7 - Android 7.0
Motorola moto G7 power - Android 9.0
Xiaomi mi 9T - Android 10
Huawei p9 lite - Android 6.0
After a short analysis I understood that if the user clicks 'NO' option, the system disconnects from the current wifi network and if another network is available connect to this.
If instead user clicks 'YES' option, we can have two differents behavior:
Phone keep connected to wifi network without internet access and all applications cannot communicate anymore with internet, because android try to use the wifi interface.
Phone keep connected to wifi network without internet access but android system rebind all existing sockets and those that will open in the future on moblie data network (if sim is available).
Then i tried same thing but using programmatic connection.
My sample code for differents android versions:
fun connectToWifi(ssid: String, key: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectPost10(ssid, key)
} else {
connectPre10(ssid, key)
}
}
#RequiresApi(Build.VERSION_CODES.O)
private fun connectPost10(ssid: String, wpa2Passphrase: String) {
val specifier = WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setWpa2Passphrase(wpa2Passphrase)
.build()
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(specifier)
.build()
val networkCallback = object: ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val networkSSID = wifiManager.connectionInfo.ssid
.trim()
.removeSurrounding("\"")
if (networkSSID == "MY_NETWORK_WITHOUT_INTERNET_SSID") {
// i'm connected here
}
}
}
connectivityManager.requestNetwork(request, networkCallback)
}
private fun connectPre10(ssid: String, key: String) {
// setup wifi configuration
val config = WifiConfiguration().apply {
SSID = "\"$ssid\""
preSharedKey = "\"$key\""
}
val networkId = wifiManager.addNetwork(config)
wifiManager.disconnect() // disconnect from current (if connected)
wifiManager.enableNetwork(networkId, true) // enable next attempt
wifiManager.reconnect()
}
Please note that in order to read the network ssid android require ACCESS_FINE_LOCATION permission and GPS must be active.
I immediately noticed that using the programmatic connection the native popup didn't show up anymore, but on many devices, after connection, mobile data connectivity was "disabled" by android.
I suppose this behavior is wanted by the system and is determined by the fact that android prefers wifi over metered connections.
I'm ok with that, but what happen in my case where wifi network has no internet access? Other applications that require connectivity stops working because these cannot reach the internet.
I need a solution that allows my application to communicate via both wifi and 4g without preventing other applications from working properly.
My min sdk api level is 23 (Marshmallow), targeting 29 (Android 10).
I managed to solve the problem by saving the networks that come from callbacks registered on the connectivity manager.
val connectivityManager by lazy {
MyApplication.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
private val wifiNetworkCallback = object : ConnectivityManager.NetworkCallback() {
// Called when the framework connects and has declared a new network ready for use.
override fun onAvailable(network: Network) {
super.onAvailable(network)
listener?.onWifiConnected(network)
}
// Called when a network disconnects or otherwise no longer satisfies this request or callback.
override fun onLost(network: Network) {
super.onLost(network)
listener?.onWifiDisconnected()
}
}
private val mobileNetworkCallback = object : ConnectivityManager.NetworkCallback() {
// Called when the framework connects and has declared a new network ready for use.
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivityManager.bindProcessToNetwork(network)
listener?.onMobileConnected(network)
}
// Called when a network disconnects or otherwise no longer satisfies this request or callback.
override fun onLost(network: Network) {
super.onLost(network)
connectivityManager.bindProcessToNetwork(null)
listener?.onMobileDisconnected()
}
}
private fun setUpWifiNetworkCallback() {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
try {
connectivityManager.unregisterNetworkCallback(wifiNetworkCallback)
} catch (e: Exception) {
Log.d(TAG, "WiFi Network Callback was not registered or already unregistered")
}
connectivityManager.requestNetwork(request, wifiNetworkCallback)
}
private fun setUpMobileNetworkCallback() {
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
try {
connectivityManager.unregisterNetworkCallback(mobileNetworkCallback)
} catch (e: Exception) {
Log.d(TAG, "Mobile Data Network Callback was not registered or already unregistered")
}
connectivityManager.requestNetwork(request, mobileNetworkCallback)
}
Mark this "connectivityManager.bindProcessToNetwork()" we'll talk about it later.
Subsequently I created two different retrofit services:
Private Retrofit: bound on the network object I receive from the wifi callback, expose api to communicate with my vehicle within the local network.
Public Retrofit: bound on the network object I received from the mobile data callback, expose api to communicate with my backend and everything else that needs the internet.
At this point in my app I am able to redirect the traffic that passes through retrofit,
but how do the libraries that I include in the project understand which network they should use?
Answer: they don't understand it, in fact they try to use the wifi network getting a timeout error.
I noticed this behavior when I added google maps to my application, the canvas showed only an empty grid.
Since it is not possible to redirect google maps traffic through the public retrofit service that I created earlier i had to look for another solution to fix this.
Here it is ConnectivityManager.bindProcessToNetwork() that you have seen before!
https://developer.android.com/reference/android/net/ConnectivityManager#bindProcessToNetwork(android.net.Network).
With this method I am able to tell the process of my application that all the sockets created in the future must use the network that came from the callback of mobile data.
Using this trick, google maps and all the other libraries of which I cannot control connectivity, they will use the data connection to communicate with the internet and therefore they will work properly.
In some phones, especially the older versions of android, there is still the problem of other applications that remain without connectivity because they try to use wifi instead of using mobile data.
As a user I would be quite frustrated if using one app all the others won't work anymore.
So I would like to understand if there is a way to meet all my requirements in such a way that both my app and the others work without problems regardless of the Android version and the phone vendor.
In summary, my questions are:
Why "Network without internet access" popup does not appear if connection is made programmatically?
If Android known that my WiFi has no internet access, why others applications don't use mobile data network as fallback automatically?
Is possible to tell android that other applications must use a certain network to open future sockets?
Each vendor has custom WiFi settings to provide enhanced internet experience (Huawei WiFi+, Samsung Adaptive WiFi, Oppo WiFi Assistant, ...). I have noticed that in some phones activating it solve other applications problem, it seems that these features have permissions to rebind the entire application ecosystem on a specific network interface. How can these features help / hinder me? Is it possible to write some code that does the same thing these features do?
First, regarding your questions:
This behavior is reserved to the system app.
Android knows there is a healthy connection to the WiFi network. It does not check further to verify that there is no connection to the outside world. It is actually not always the desired behavior btw.
Yes, see below
In some aspects yes, see below
It seems to me that what you're looking for is to alter the default routing mechanism of Android.
That is, you would like all the traffic to the server(s) inside the WiFi network be routed to the WiFi network, while all other traffic be routed via the mobile data interface. There are a couple of ways to achieve this:
If your app is part of the infotainment system of the vehicle, and can possess system privileges, or alternatively, on a rooted Android phones, you can directly alter the routing table, using ip route commands.
What you described is actually part of the functionality of a Virtual Private Network (VPN). You can implement a VPN service yourself server side and client side, based on open source solutions such as OpenVPN, in which the VPN server would be inside the wifi network. Android has prebuilt infrastructure for implementing the VPN client: https://developer.android.com/guide/topics/connectivity/vpn
You can use commercial VPN solutions. Some of them allow the configuration you're looking for, and I believe will meet the needs you described.

Connecting to specific wifi in android 10

I'm trying to connect to a specific wifi network in android 10. I already have the credentials available to me, and i'm adding the network suggestion:
val suggestion = WifiNetworkSuggestion.Builder()
.setSsid("ssid")
.setWpa2Passphrase("password")
.build()
wifiManager.removeNetworkSuggestions(mutableListOf(suggestion))
when (wifiManager.addNetworkSuggestions(mutableListOf(suggestion))) {
WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS -> {
networkAdded()
}
WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE -> {
networkDuplicate()
}
else -> {
networkAddFailed()
}
}
I'm able to see the notification (though only the first time the app requests this, which makes sense for the most part, since it's asking for permission really to be able to suggest networks). However, after I allow it, i can't seem to get connected to that network. In particular if i already have a wifi connection to some other network.
The new network does not appear in my list of saved networks, and if i find it in the wifi scan list, and click on it, it treats it like a new network.
I don't expect the app to be able to force the system to connect, but i would expect the user to be able to do it on their own via the wifi settings without having to configure it from scratch.
I've also tried using ConnectivityManager to forcibly connect, but that does not add an actual persistent wifi connection. It's only meant to force connection for a specific app while the app is in memory (at least that's my understanding of it).
I had the same issue but after I added this piece of code the device was connected to the network:
final IntentFilter intentFilter = new IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)) {
return;
} else {
// Do something with success
}
}
};
context.registerReceiver(broadcastReceiver, intentFilter);
Hope that it helps. You can find the complete code example in: Wi-Fi suggestion API

Android 10 / API 29 : how to connect the phone to a configured network?

I am working on an application in which one the user has to follow these steps :
connect the phone to wifi ;
connect the phone to a dedicated hotspot from a connected object.
When the user is connected to the dedicated hotspot of the connected object, the application does some HTTP requests in order to configure it. Then, I would like to reconnect automatically the application to the global wifi of step 1.
From API 21 to API 28 this feature is very easy to implement because I know the SSID I want to reconnect the phone too. It can be done with a few line of code:
private fun changeCurrentWifiNetworkLegacy(ssidToConnect: String) {
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
var ssidIdentifier: Int? = null
wifiManager.configuredNetworks?.forEach { config ->
Log.d("SSID", config.SSID)
if (config.SSID == "\"${ssidToConnect}\"") {
ssidIdentifier = config.networkId
}
}
ssidIdentifier?.let { id ->
wifiManager.enableNetwork(id, true)
}
}
On API 29 this simple code does not work anymore according to this article: https://developer.android.com/about/versions/10/privacy/changes#configure-wifi
According to the article, now, I should use 2 classes: WifiNetworkSpecifier and/or WifiNetworkSuggestion.
Unfortunately, I cannot find a working example using these classes in order to connect the user to a previous configured SSID.
Does someone already achieve that?
Thank you in advance for your help.
Just in case any poor soul encounters this, it's completely possible the API is just broken on your device. On my OnePlus 5, Android 10, Build 200513, this happens:
Call requestNetwork. Doesn't matter whether I use the Listener or PendingIntent version
The OS finds the network, connects, onAvailable and friends are called
OS immediately disconnects. I can see "App released request, cancelling NetworkRequest" in logcat
This is however, not true - the request was not cancelled, which Android relizes, and starts the process of connecting to the network again. Go to 2. and repeats forever.
Created an Android bug for this: https://issuetracker.google.com/issues/158344328
Additionally, you can get the OS into state when it will no longer show the "Device to use with " dialog if you don't terminate your app and the dialogs in the correct order, and only reboot helps.
Just save yourself the trouble, target Android 9 and use the WifiManager APIs (that are helpfully broken if you target 10). It even has better user experience than the new requestNetwork APIs.
You can set which network to connect to with the following code:
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(
WifiNetworkSpecifier.Builder()
.setSsid("My ssid")
.build()
)
.build()
cm.requestNetwork(networkRequest, object: ConnectivityManager.NetworkCallback() {
override fun onUnavailable() {
Log.d("TEST", "Network unavailable")
}
override fun onAvailable(network: Network) {
Log.d("TEST", "Network available")
}
})
}
This uses the ConnectivityManager's networkRequest method to request a network with a specific SSID.
This method requires the caller to hold either the Manifest.permission.CHANGE_NETWORK_STATE permission or the ability to modify system settings as determined by Settings.System.canWrite(Context).
See the NetworkCallback class for more documentation about what info you can get.
(edit: Missed adding Transport type.)
Further edit: I needed to use .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) to get it to work properly. Because in WifiNetworkSpecifier
can only be used to request a local wifi network (i.e no internet
capability)
According to the docs
This gives me a request for devices popup, but then eventually shows me the Wifi network I asked for.
I try lots of codes, including WifiManager.addNetworkSuggestions and ConnectivityManager.requestNetwork. Finally i found that, just call ConnectivityManager.unregisterNetworkCallback to restore user wifi connection. The only place that this code makes me dissatisfied, if the user saved WiFi A and WiFi B, then the system will automatically select a connection, not the one I specified.

Categories

Resources