-
-
Save Farbklex/f84029889444ee9c52a331a7e2bd10d2 to your computer and use it in GitHub Desktop.
package me.a_hoffmann.gists | |
import android.content.Context | |
import android.net.ConnectivityManager | |
import android.net.NetworkCapabilities | |
/** | |
* Checks if the device has an internet connection. | |
* NOTE: Works only on android API level 23 and above! | |
*/ | |
class ConnectivityChecker(private val context: Context){ | |
/** | |
* Check if the device has an internet connection. | |
* | |
* @return True if the device is connected to a network which also gives it access to the internet. | |
* False otherwise. | |
*/ | |
fun isInternetAvailable(): Boolean { | |
val connectivityManager = context.getSystemService( | |
Context.CONNECTIVITY_SERVICE) as ConnectivityManager? | |
?: return false | |
val activeNetwork = connectivityManager.activeNetwork ?: return false | |
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false | |
// If we check only for "NET_CAPABILITY_INTERNET", we get "true" if we are connected to a wifi | |
// which has no access to the internet. "NET_CAPABILITY_VALIDATED" also verifies that we | |
// are online | |
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) | |
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) | |
} | |
} |
Sadly NET_CAPABILITY_INTERNET
and NET_CAPABILITY_VALIDATED
are unreliable when using local VPNs like:
- NetGuard (https://f-droid.org/en/packages/eu.faircode.netguard/)
- AdGuard (https://adguard.com/en/adguard-android/overview.html)
On my Android 11 emulator + enabled VPN from either these two apps + enabled flight mode (= no internet connection): capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
or capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
returns true (but should be false).
Sadly
NET_CAPABILITY_INTERNET
andNET_CAPABILITY_VALIDATED
are unreliable when using local VPNs like:* NetGuard (https://f-droid.org/en/packages/eu.faircode.netguard/) * AdGuard (https://adguard.com/en/adguard-android/overview.html)
On my Android 11 emulator + enabled VPN from either these two apps + enabled flight mode (= no internet connection):
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
orcapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
returns true (but should be false).
Have you found a solution for this problem? Is there a way to check for internet access even when being tunneled through VPN?
Have you found a solution for this problem?
I did not found a perfect solution, but a good workaround: I additionally check if DNS addresses can be resolved.
This should be fine in most cases, because when a VPN is active and no internet is available, my app normally crashes due to a DNS error. Example stacktrace:
...
Caused by: java.net.UnknownHostException: Unable to resolve host "firefox-ci-tc.services.mozilla.com": No address associated with hostname
...
Caused by: android.system.GaiException: android_getaddrinfo failed: EAI_NODATA (No address associated with hostname)
...
My solution should be more privacy friendly than pinging a Google service. But it is not perfect because it depends on how the VPN app is handling DNS requests. But it seems that the most VPN apps will not cache DNS requests and will contact the DNS server "in the internet".
But be careful not to resolve the DNS address in the main thread. Otherwise an android.os.NetworkOnMainThreadException may be thrown.
My code:
lifecycleScope.launch(Dispatchers.Main) {
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (isInternetAvailable(cm)) {
...
}
...
}
@MainThread
private suspend fun isInternetAvailable(cm: ConnectivityManager): Boolean {
val networkConnected = if (SDK_INT >= Build.VERSION_CODES.M) {
isNetworkConnected(cm)
} else {
isNetworkConnectedOldWay(cm)
}
return networkConnected && isDnsResolvable()
}
@RequiresApi(Build.VERSION_CODES.M)
private fun isNetworkConnected(cm: ConnectivityManager): Boolean {
val activeNetwork = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
@Suppress("DEPRECATION")
private fun isNetworkConnectedOldWay(cm: ConnectivityManager): Boolean {
return cm.activeNetworkInfo?.isConnected == true
}
@Suppress("BlockingMethodInNonBlockingContext")
@MainThread
private suspend fun isDnsResolvable(): Boolean {
return withContext(Dispatchers.IO) {
try {
val address = InetAddress.getByName("api.github.com")
address.hostAddress.isNotEmpty()
} catch (e: UnknownHostException) {
false
}
}
}
Another idea:
If you are using AndroidX WorkManager, you can use Result.retry()
and runAttemptCount
(https://developer.android.com/reference/androidx/work/WorkerParameters#getRunAttemptCount() - Gets the current run attempt count for this work. Note that for periodic work, this value gets reset between periods.
):
- if
runAttemptCount
is low enough, ignore network exception and returnResult.retry()
. On default this job will be executed again after a exponentially increasing waiting time (30s, 1m, 2m, ... up to 5 hours) - if
runAttemptCount
is too high, throw an exception / show error message
@MainThread
override suspend fun doWork(): Result {
try {
doSomething()
//network exceptions are capsuled as ApiConsumerException
} catch (e: ApiConsumerException) {
if (runAttemptCount >= 8) {
showErrorNotification(context, e)
return Result.success()
//with Result.success(), my "background job" will be executed in normal intervals (e.g. every 8 hours)
//with Result.error(), my "background job" will be canceld
}
return Result.retry() //retry later in 30s, 1m, 2m, ...
}
...
}
FYI: One user with a Motorola Moto G (5) Plus (SDK 27) has internet access, but capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
returns false
.
The AndroidX WorkManager library (which I use) still uses the deprecated API ConnectivityManager#getActiveNetworkInfo()
https://android.googlesource.com/platform/frameworks/support.git/+/refs/heads/androidx-main/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/NetworkStateTracker.java#135:
@SuppressWarnings("WeakerAccess") /* synthetic access */
NetworkState getActiveNetworkState() {
// Use getActiveNetworkInfo() instead of getNetworkInfo(network) because it can detect VPNs.
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
boolean isConnected = info != null && info.isConnected();
boolean isValidated = isActiveNetworkValidated();
boolean isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(mConnectivityManager);
boolean isNotRoaming = info != null && !info.isRoaming();
return new NetworkState(isConnected, isValidated, isMetered, isNotRoaming);
}
@VisibleForTesting
boolean isActiveNetworkValidated() {
if (Build.VERSION.SDK_INT < 23) {
return false; // NET_CAPABILITY_VALIDATED not available until API 23. Used on API 26+.
}
try {
Network network = mConnectivityManager.getActiveNetwork();
NetworkCapabilities capabilities = mConnectivityManager.getNetworkCapabilities(network);
return capabilities != null
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
} catch (SecurityException exception) {
// b/163342798
Logger.get().error(TAG, "Unable to validate active network", exception);
return false;
}
}
This reminds me of a Samsung user with Android 5. His smartphone should have supported the SessionInstaller-API (introduced in API 21 = Android 5) but in reality the API on his smartphone didn't work. Maybe I should stick to old APIs until they are deprecated.
I will use the deprecated API ConnectivityManager#getActiveNetworkInfo() for API < 29. For API >= 29, I will use capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
because it's the only non-deprecated API without callbacks.
And I no longer check if an address can be resolved using DNS. There is a risk that the IP address is resolved by the internal DNS cache.
Sadly
NET_CAPABILITY_INTERNET
andNET_CAPABILITY_VALIDATED
are unreliable when using local VPNs like:* NetGuard (https://f-droid.org/en/packages/eu.faircode.netguard/) * AdGuard (https://adguard.com/en/adguard-android/overview.html)
I have noticed this behaviour, but can't find many other references to it online! Frustrating.
I am also using WorkManager
, though the behaviour I have observed is that it behaves the same as checking for NET_CAPABILITY_INTERNET
and NET_CAPABILITY_VALIDATED
(i.e. that the VPN makes it think there's an internet connection even if there isn't one). So I put another internet check inside the Work
using the deprecated ConnectivityManager#getActiveNetworkInfo()
and retry()
if none is found. Seems to work, but frustrating that I needed to do some many work arounds and use deprecated APIs to get it to work.
private boolean hasNetworkCapability(NetworkCapabilities networkCapabilities) {
boolean hasCapability = false;
/*
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) // API >= 21
&& networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) // API >= 23
&& (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) // API >= 28
|| networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)); // API >= 28
} else if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) // API >= 21
&& networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); // API >= 23
} else if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) {
hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); // API >= 21
}
return hasCapability;
*/
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
hasCapability = hasCapability || networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
}
}
return hasCapability;
}
Here is what I wrote as a util
suspend fun isDnsResolvable(): Boolean {
return withContext(Dispatchers.IO) {
try {
/*
www.msftconnecttest.com is what windows uses to determine if a network is connected to the internet
to be fair, windows requests for http://www.msftconnecttest.com/connecttest.txt and checks the content
but a simple DNS lookup is enough for us, me thinks :^)
*/
val address = InetAddress.getByName("www.msftconnecttest.com")
!address.hostAddress.isNullOrEmpty()
} catch (e: UnknownHostException) {
false
}
}
}
suspend fun hasNetwork(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false
return activeNetwork.hasCapability(NET_CAPABILITY_VALIDATED) && isDnsResolvable()
}
I recommend using kotlinx.coroutines.flow.flow
to check at an interval. (The network listener is not reliable)
var connectedToInternet = false;
flow {
while (true) {
if (connectedToInternet) {
delay(4.seconds)
} else {
delay(1.seconds)
}
connectedToInternet = hasNetwork(this@MainActivity)
delay(1.seconds)
emit(Unit)
}
}.launchIn(scope)
Taken from here.
You might want to use this for notifying the user about the network connectivity just like YouTube and TikTok do
private boolean hasNetworkCapability(NetworkCapabilities networkCapabilities) { boolean hasCapability = false; /* if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) // API >= 21 && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) // API >= 23 && (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) // API >= 28 || networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)); // API >= 28
don't we need both network not suspended as well as foreground network is available for use by apps?
} else if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.M) { hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) // API >= 21 && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); // API >= 23 } else if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); // API >= 21 } return hasCapability; */ if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { hasCapability = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { hasCapability = hasCapability || networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); } } return hasCapability; }
I'm not quite sure if we need both,
NET_CAPABILITY_INTERNET
andNET_CAPABILITY_VALIDATED
or ifNET_CAPABILITY_VALIDATED
alone would be enough.But I assume both are required.