Skip to content

Instantly share code, notes, and snippets.

@Farbklex
Created February 18, 2020 13:27
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Farbklex/f84029889444ee9c52a331a7e2bd10d2 to your computer and use it in GitHub Desktop.
Save Farbklex/f84029889444ee9c52a331a7e2bd10d2 to your computer and use it in GitHub Desktop.
Android: Check if the device has internet access on devices with API level >= 23.
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)
}
}
@Farbklex
Copy link
Author

I'm not quite sure if we need both, NET_CAPABILITY_INTERNETand NET_CAPABILITY_VALIDATED or if NET_CAPABILITY_VALIDATED alone would be enough.
But I assume both are required.

@Murtowski
Copy link

Murtowski commented Jun 11, 2021

capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) returns false although I see that FLAG in capabilities
class
Screenshot 2021-06-11 at 12 44 43
Have You faced that issue too? Test on OnePlus 7t
BR

@Tobi823
Copy link

Tobi823 commented Sep 10, 2021

Sadly NET_CAPABILITY_INTERNET and NET_CAPABILITY_VALIDATED are unreliable when using local VPNs like:

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

@Farbklex
Copy link
Author

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

Have you found a solution for this problem? Is there a way to check for internet access even when being tunneled through VPN?

@Tobi823
Copy link

Tobi823 commented Sep 30, 2021

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
        }
    }
}

@Tobi823
Copy link

Tobi823 commented Sep 30, 2021

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 return Result.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, ...
    }
    ...
}

@Tobi823
Copy link

Tobi823 commented Oct 31, 2021

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.

@Tobi823
Copy link

Tobi823 commented Oct 31, 2021

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.

@lozog
Copy link

lozog commented Nov 13, 2021

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)

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.

@Grabber
Copy link

Grabber commented May 26, 2022

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;
}

@ArjixWasTaken
Copy link

ArjixWasTaken commented Apr 6, 2023

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

@iabhijeet08
Copy link

iabhijeet08 commented Feb 5, 2024

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;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment