I know that my users are connected to a wifi without Internet, so I bind the Android process to cellular:
private fun bindProcessToCellular() {
val req = NetworkRequest.Builder()
req.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
req.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
cm.requestNetwork(req.build(), object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
if (cm.bindProcessToNetwork(network)){
// YAY, process bound to cellular
}
}
})
}
Now, this works perfectly from Kotlin/Android - Internet requests are running as expected through cellular for Android/Kotlin code, -but not from my gomobile library. From within Go, requests simply times out. If I disconnect wifi, all Go-requests are successful, making me think that Go somehow resolves the wrong network.
My understanding is that gomobile runs on the Andoroid JNI interfaces, and my intuition is that the bindProcessToNetwork binds everything, including components running towards the JNI interfaces of that app.
Anyone got some ideas what might be going on here?
Possible workaround:
When user connects to this wifi (without internet) the first time, the os asks the user "This wifi doesn´t have internet, do you still want to connect?" If the user then cancels, the device is actually still connected to that wifi, but traffic is still routed through cellular, including requests created in Go..
Related
The moment I get on a wifi connection, the cellular network is completely lost even though the cellular network indicator is definitely on.
This is my network request
val request = NetworkRequest.Builder().run {
addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
build()
}
connectivityManager.registerNetworkCallback(request, callback)
I've tried looking in the connectivityManager.allNetworks list and it's no where to be found. Only the wifi network is in there.
What's even weirder is there is one other cellular network that is always there. It does not have the same ID as my cellular network. There's no connection that can be made with it. It never shows up with registerNetworkCallback. The capabilities on it always include "valid" and "internet"
What am I seeing here? Why is my cellular network lost? What is this phantom cellular network?
targetSdkVersion: 29
Device: Galaxy S10 - Android 12
I figured this out.
If you call registerNetworkCallback the above will happen, but if you call requestNetwork with TRANSPORT_CELLULAR,
connectivityManager.requestNetwork(request, callback)
Android will keep the cellular network around. I was so confused because the documentation was so lacking. Once you do that, it will ask you to add the CHANGE_NETWORK_STATE permission.
After this step, the network is available, but you won't be able to make any request with it. You have to call
connectivityManager.bindProcessToNetwork(theCellularNetwork)
to get any connection.
After this is done, the cellular network can be used in tandem with the wifi network. You can even send some traffic to one and some to the other. If you use OkHttp like I do, you just bind the client with the network's socketFactory
val client = OkHttpClient().newBuilder().run {
socketFactory(network.socketFactory)
build()
}
client.newCall(
Request.Builder().url("https://example.com").build()
).execute().let {
Log.i(TAG, "Fetched ${it.body!!.string()}")
}
The cellular network isn't lost, but your app isn't allowed to use it. Once WiFi is connected, everything is forced to use that connection. The only exception to this rule is if your phone has a feature called "Dual Acceleration", which allows the cellular connection to stay active (and obviously, the user would have to enable that feature). Alternatively, you may have a setting in your phone's Developer Options called "Cellular Data Always Active", which will do the same thing.
But needless to say, you can't rely on either of those 2 features being enabled in a production environment. So, just assume that when WiFi is connected, that's the only connection that your app can use
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.
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.
The application we are implementing allows the user's phone to be connected to a network via WiFi without having access to the internet (through that network), and using their phone's mobile data connection at the same time. The application will be hitting web services through the network it's connected to, while able to still hit other services outside it's network through the data plan. The behavior for this isn't as expected; it works on iOS but not available on Android, so I developed a way to manually route the network traffic leaving my application.
My current work around is limited to only Android version 6.0 (Api 23) and up. What I've done is manually assigning where the traffic should go via the ConnectivityManager's Network Request api:
NetworkRequest request = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
//.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) //In case it should go through WiFi instead
.build();
connectivityManager.requestNetwork(request, new ConnectivityManager.NetworkCallback(){
#Override
public void onAvailable(Network network) {
connectivityManager.bindProcessToNetwork(network);
//Handle API Request
}
#Override
public void onUnavailable() {
//Handle error logic
}
});
I've gotten this to work, albeit may not be practical, and I'm able to change which network I should send my requests through. However, with the app open and unfocused, other applications are able to access the internet through the phone's data plan. Once the app is closed, the behavior goes back to normal and the phone can hit services through the network, but not on the web (or vice versa, it's different depending on your Android phone).
What I would like to know if there is a way to revert back to the normal behavior when the app is unfocused (ie, during onPause() of my activity). Having the route set manually for other applications might not be ideal for the end user experience, nor might it pass through Google's terms.
I wrote an app that is triggering a Sony qx smartphone attachable camera over wifi. However I need to transfer the images off the phone over another local network in real time. Since the wifi card is being used for qx connection I need to be able to use ethernet over usb for transferring images off the phone. Http requests will be used to trigger the camera and send the images off the phone.
Is it possible in one android app on a phone with two network interfaces setup to specify for certain http requests to use one network interface and for others to use another network interface ? Does this need to be done through routing tables, not java?
The phone I'm using is a rooted nexus 6p.
Update:
Currently, I was able to get an Ethernet adapter working with the device (Nexus 6P). The device is connected to a local network over Ethernet. When the Wi-Fi interface is off, I can ping all devices on the local network the device is connected to over Ethernet. However, I am unable to access the web servers (Not using DNS) of any of the devices on that network (which I know they are running), i.e. Http via a browser app. The nexus 6p is connected to the network over Ethernet via a Ubiquiti Station. This seems to be a routing issue.
I can tether(usb interface) and use Wi-Fi in one app, so that leads me to believe it is possible to use Ethernet and Wi-Fi.
Update2:
After more testing, it seems to be that it is a permissions issue. Since when I ping the network the device is connected to over Ethernet without first running su in the terminal the network doesn't exist. However, when I run su then ping, I can ping the network. Thus it seems my app needs to get superuser permission before accessing Ethernet. I've granted it superuser access, but nothing has changed. I read that simply running su isn't enough from one of the comments in this post. This is because su just spawns a root shell that dies. This also explains why I couldn't access any of the web servers on this network via a browser app. Is it possible to grant my app access to the Ethernet interface when making HTTP calls like give HttpURLConnection root access, if that makes any sense (running su doesn't work)? There seems to definitely be a solution since HttpURLConnection can make calls over the USB tethering interface (Nexus 6P calls it rndis0) fine.
Update 3:
I found online here , that I can make my app a System app (thought this might grant the app eth0 access). I just moved my app to /system/app and then rebooted. However, this didn't seem to give the app anymore privileges (thus not solving the problem) , or there is something else required to make the app system than just copying it to /system/app.
Update 4:
So I was able to get Ethernet working on every app without root permissions! It seemed to be that it only works over DHCP and does not like static connections, which I was using. It works with Wi-Fi enabled, however, I cannot contact any of the devices on the Wi-Fi network when Ethernet is enabled. Is there a way around this? Does it have to do with setting two default gateways?
Since you were programming in Nexus 6P, you can try to use the new API added in ConnectivityManager to select the ethernet as your preferred network connection for your process.
Since I can't build the similar environment like yours, I am not sure if it works. It's just a suggested solution, totally not tested and verified.
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
Network etherNetwork = null;
for (Network network : connectivityManager.getAllNetworks()) {
NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network);
if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
etherNetwork = network;
}
}
Network boundNetwork = connectivityManager.getBoundNetworkForProcess();
if (boundNetwork != null) {
NetworkInfo boundNetworkInfo = connectivityManager.getNetworkInfo(boundNetwork);
if (boundNetworkInfo.getType() != ConnectivityManager.TYPE_ETHERNET) {
if (etherNetwork != null) {
connectivityManager.bindProcessToNetwork(etherNetwork);
}
}
}
Just to give a little more explanation on how this finally got solved.
Utilizing #alijandro's answer I was able to switch back and forth between Ethernet and Wi-Fi in one app. For some reason for the Ethernet to work it required the network gateway to supply DHCP address, not static. Then since the bindProcessToNetwork, used in #alijandro's answer is per-process, I decided to split communications with the QX camera into a Service that runs in a separate Process. The main Application (another process) would post images over Ethernet to a local network. I was successfully able to contact the devices on the local network via HTTP over Ethernet while simultaneously triggering the QX over Wi-Fi. Currently, I used Messenger to communicate using IPC to tell the QX triggering Service what methods to call.
Most of android tv boxes can use wifi and ethernet together. In my device, i can enable ethernet from this path ---
Settings -> More ... > Ethernet ---
But your device wont have a menu like that as i understand. So you should make an app to do that. This application needs to access some system specific resources so your device needs to be rooted or application needs to signed with system signature.
Also this topic can help you link
There is an easy way to do this that will answer the OP's original question about how to do this with a single application (not two separate app processes) using ConnectivityManager.requestNetwork().
The docs for ConnectivityManager.requestNetwork() allude to this:
... For example, an application could use this method to obtain a
connected cellular network even if the device currently has a data
connection over Ethernet. This may cause the cellular radio to consume
additional power. Or, an application could inform the system that it
wants a network supporting sending MMSes and have the system let it
know about the currently best MMS-supporting network through the
provided NetworkCallback. ...
For OP's scenario of using Wi-Fi for some traffic and ethernet for other traffic one only needs to call ConnectivityManager.requestNetwork() twice with two separate requests. One for TRANSPORT_WIFI and one for TRANSPORT_ETHERNET. The operative item here is we need a way to uniquely identify these networks. For OP's scenario, we can use transport type.
final NetworkRequest requestForWifi =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build();
final NetworkRequest requestForEthernet =
new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
final ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkCallback networkCallbackWifi = new NetworkCallback() {
#Override
void onAvailable(Network network) {
// Triggers when this network is available so you can bind to it.
}
#Override
void onLost(Network network) {
// Triggers when this network is lost.
}
};
final NetworkCallback networkCallbackEthernet = new NetworkCallback() {
#Override
void onAvailable(Network network) {
// Triggers when this network is available so you can bind to it.
}
#Override
void onLost(Network network) {
// Triggers when this network is lost.
}
};
connectivityManager.requestNetwork(requestForWifi, networkCallbackWifi);
connectivityManager.requestNetwork(requestForEthernet, networkCallbackEthernet);
Then, once the callbacks trigger, you can then in the pertinent code (e.g. OP's code for transferring images), listen for onAvailable(Network network) and use the provided Network with Network.OpenConnection() to connect to an HTTP server using that network.
This would allow you to connect to two separate Networks from the same application.