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:
Motorola moto G7 power - Android 9.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?