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

那年仲夏 提交于 2020-08-03 02:43:22

问题


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:

  1. 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.
  2. 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:

  1. Private Retrofit: bound on the network object I receive from the wifi callback, expose api to communicate with my vehicle within the local network.
  2. 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:

  1. Why "Network without internet access" popup does not appear if connection is made programmatically?
  2. If Android known that my WiFi has no internet access, why others applications don't use mobile data network as fallback automatically?
  3. Is possible to tell android that other applications must use a certain network to open future sockets?
  4. 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?

回答1:


First, regarding your questions:

  1. This behavior is reserved to the system app.
  2. 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.
  3. Yes, see below
  4. 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:

  1. 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.

  2. 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

  3. 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.



来源:https://stackoverflow.com/questions/61773466/android-communicate-with-mobile-data-while-connected-to-wifi-without-internet-a

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!