| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net; |
| |
| import static android.net.ConnectivityManager.TYPE_VPN; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; |
| import static android.net.NetworkCapabilities.TRANSPORT_VPN; |
| |
| import android.Manifest.permission; |
| import android.annotation.SuppressLint; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.net.ConnectivityManager; |
| import android.net.ConnectivityManager.NetworkCallback; |
| import android.net.LinkProperties; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkInfo; |
| import android.net.NetworkRequest; |
| import android.net.wifi.WifiInfo; |
| import android.net.wifi.WifiManager; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.telephony.TelephonyManager; |
| |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.base.ApplicationState; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.ResettersForTesting; |
| import org.chromium.base.StrictModeContext; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.compat.ApiHelperForM; |
| import org.chromium.base.compat.ApiHelperForO; |
| import org.chromium.base.compat.ApiHelperForP; |
| import org.chromium.build.BuildConfig; |
| |
| import java.io.IOException; |
| import java.net.Socket; |
| import java.util.Arrays; |
| |
| import javax.annotation.concurrent.GuardedBy; |
| |
| /** |
| * Used by the NetworkChangeNotifier to listens to platform changes in connectivity. |
| * Note that use of this class requires that the app have the platform |
| * ACCESS_NETWORK_STATE permission. |
| */ |
| // TODO(crbug.com/635567): Fix this properly. |
| @SuppressLint("NewApi") |
| public class NetworkChangeNotifierAutoDetect extends BroadcastReceiver { |
| /** Immutable class representing the state of a device's network. */ |
| public static class NetworkState { |
| private final boolean mConnected; |
| private final int mType; |
| private final int mSubtype; |
| private final boolean mIsMetered; |
| // WIFI SSID of the connection on pre-Marshmallow, NetID starting with Marshmallow. Always |
| // non-null (i.e. instead of null it'll be an empty string) to facilitate .equals(). |
| private final String mNetworkIdentifier; |
| // Indicates if this network is using DNS-over-TLS. |
| private final boolean mIsPrivateDnsActive; |
| // Indicates the DNS-over-TLS server in use, if specified. |
| private final String mPrivateDnsServerName; |
| |
| public NetworkState( |
| boolean connected, |
| int type, |
| int subtype, |
| boolean isMetered, |
| String networkIdentifier, |
| boolean isPrivateDnsActive, |
| String privateDnsServerName) { |
| mConnected = connected; |
| mType = type; |
| mSubtype = subtype; |
| mIsMetered = isMetered; |
| mNetworkIdentifier = networkIdentifier == null ? "" : networkIdentifier; |
| mIsPrivateDnsActive = isPrivateDnsActive; |
| mPrivateDnsServerName = privateDnsServerName == null ? "" : privateDnsServerName; |
| } |
| |
| public boolean isConnected() { |
| return mConnected; |
| } |
| |
| public int getNetworkType() { |
| return mType; |
| } |
| |
| public boolean isMetered() { |
| return mIsMetered; |
| } |
| |
| public int getNetworkSubType() { |
| return mSubtype; |
| } |
| |
| // Always non-null to facilitate .equals(). |
| public String getNetworkIdentifier() { |
| return mNetworkIdentifier; |
| } |
| |
| /** Returns the connection type for the given NetworkState. */ |
| @ConnectionType |
| public int getConnectionType() { |
| if (!isConnected()) { |
| return ConnectionType.CONNECTION_NONE; |
| } |
| return convertToConnectionType(getNetworkType(), getNetworkSubType()); |
| } |
| |
| /** Returns the connection cost for the given NetworkState. */ |
| @ConnectionCost |
| public int getConnectionCost() { |
| if (isMetered()) { |
| return ConnectionCost.METERED; |
| } |
| return ConnectionCost.UNMETERED; |
| } |
| |
| /** Returns the connection subtype for the given NetworkState. */ |
| public int getConnectionSubtype() { |
| if (!isConnected()) { |
| return ConnectionSubtype.SUBTYPE_NONE; |
| } |
| |
| switch (getNetworkType()) { |
| case ConnectivityManager.TYPE_ETHERNET: |
| case ConnectivityManager.TYPE_WIFI: |
| case ConnectivityManager.TYPE_WIMAX: |
| case ConnectivityManager.TYPE_BLUETOOTH: |
| return ConnectionSubtype.SUBTYPE_UNKNOWN; |
| case ConnectivityManager.TYPE_MOBILE: |
| case ConnectivityManager.TYPE_MOBILE_DUN: |
| case ConnectivityManager.TYPE_MOBILE_HIPRI: |
| // Use information from TelephonyManager to classify the connection. |
| switch (getNetworkSubType()) { |
| case TelephonyManager.NETWORK_TYPE_GPRS: |
| return ConnectionSubtype.SUBTYPE_GPRS; |
| case TelephonyManager.NETWORK_TYPE_EDGE: |
| return ConnectionSubtype.SUBTYPE_EDGE; |
| case TelephonyManager.NETWORK_TYPE_CDMA: |
| return ConnectionSubtype.SUBTYPE_CDMA; |
| case TelephonyManager.NETWORK_TYPE_1xRTT: |
| return ConnectionSubtype.SUBTYPE_1XRTT; |
| case TelephonyManager.NETWORK_TYPE_IDEN: |
| return ConnectionSubtype.SUBTYPE_IDEN; |
| case TelephonyManager.NETWORK_TYPE_UMTS: |
| return ConnectionSubtype.SUBTYPE_UMTS; |
| case TelephonyManager.NETWORK_TYPE_EVDO_0: |
| return ConnectionSubtype.SUBTYPE_EVDO_REV_0; |
| case TelephonyManager.NETWORK_TYPE_EVDO_A: |
| return ConnectionSubtype.SUBTYPE_EVDO_REV_A; |
| case TelephonyManager.NETWORK_TYPE_HSDPA: |
| return ConnectionSubtype.SUBTYPE_HSDPA; |
| case TelephonyManager.NETWORK_TYPE_HSUPA: |
| return ConnectionSubtype.SUBTYPE_HSUPA; |
| case TelephonyManager.NETWORK_TYPE_HSPA: |
| return ConnectionSubtype.SUBTYPE_HSPA; |
| case TelephonyManager.NETWORK_TYPE_EVDO_B: |
| return ConnectionSubtype.SUBTYPE_EVDO_REV_B; |
| case TelephonyManager.NETWORK_TYPE_EHRPD: |
| return ConnectionSubtype.SUBTYPE_EHRPD; |
| case TelephonyManager.NETWORK_TYPE_HSPAP: |
| return ConnectionSubtype.SUBTYPE_HSPAP; |
| case TelephonyManager.NETWORK_TYPE_LTE: |
| return ConnectionSubtype.SUBTYPE_LTE; |
| default: |
| return ConnectionSubtype.SUBTYPE_UNKNOWN; |
| } |
| default: |
| return ConnectionSubtype.SUBTYPE_UNKNOWN; |
| } |
| } |
| |
| /** Returns boolean indicating if this network uses DNS-over-TLS. */ |
| public boolean isPrivateDnsActive() { |
| return mIsPrivateDnsActive; |
| } |
| |
| /** Returns the DNS-over-TLS server in use, if specified. */ |
| public String getPrivateDnsServerName() { |
| return mPrivateDnsServerName; |
| } |
| } |
| |
| /** Queries the ConnectivityManager for information about the current connection. */ |
| @VisibleForTesting |
| public static class ConnectivityManagerDelegate { |
| private final ConnectivityManager mConnectivityManager; |
| |
| public ConnectivityManagerDelegate(Context context) { |
| mConnectivityManager = |
| (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); |
| } |
| |
| // For testing. |
| ConnectivityManagerDelegate() { |
| // All the methods below should be overridden. |
| mConnectivityManager = null; |
| } |
| |
| /** |
| * @param networkInfo The NetworkInfo for the active network. |
| * @return the info of the network that is available to this app. |
| */ |
| private NetworkInfo processActiveNetworkInfo(NetworkInfo networkInfo) { |
| if (networkInfo == null) { |
| return null; |
| } |
| |
| if (networkInfo.isConnected()) { |
| return networkInfo; |
| } |
| |
| // If |networkInfo| is BLOCKED, but the app is in the foreground, then it's likely that |
| // Android hasn't finished updating the network access permissions as BLOCKED is only |
| // meant for apps in the background. See https://crbug.com/677365 for more details. |
| |
| if (networkInfo.getDetailedState() != NetworkInfo.DetailedState.BLOCKED) { |
| // Network state is not blocked which implies that network access is |
| // unavailable (not just blocked to this app). |
| return null; |
| } |
| |
| if (ApplicationStatus.getStateForApplication() |
| != ApplicationState.HAS_RUNNING_ACTIVITIES) { |
| // The app is not in the foreground. |
| return null; |
| } |
| return networkInfo; |
| } |
| |
| /** |
| * Returns connection type and status information about the current |
| * default network. |
| */ |
| NetworkState getNetworkState(WifiManagerDelegate wifiManagerDelegate) { |
| Network network = null; |
| NetworkInfo networkInfo; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| network = getDefaultNetwork(); |
| networkInfo = getNetworkInfo(network); |
| } else { |
| networkInfo = mConnectivityManager.getActiveNetworkInfo(); |
| } |
| networkInfo = processActiveNetworkInfo(networkInfo); |
| if (networkInfo == null) { |
| return new NetworkState(false, -1, -1, false, null, false, ""); |
| } |
| |
| if (network != null) { |
| final NetworkCapabilities capabilities = getNetworkCapabilities(network); |
| boolean isMetered = |
| (capabilities != null |
| && !capabilities.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_NOT_METERED)); |
| DnsStatus dnsStatus = AndroidNetworkLibrary.getDnsStatus(network); |
| if (dnsStatus == null) { |
| return new NetworkState( |
| true, |
| networkInfo.getType(), |
| networkInfo.getSubtype(), |
| isMetered, |
| String.valueOf(networkToNetId(network)), |
| false, |
| ""); |
| } else { |
| return new NetworkState( |
| true, |
| networkInfo.getType(), |
| networkInfo.getSubtype(), |
| isMetered, |
| String.valueOf(networkToNetId(network)), |
| dnsStatus.getPrivateDnsActive(), |
| dnsStatus.getPrivateDnsServerName()); |
| } |
| } |
| assert Build.VERSION.SDK_INT < Build.VERSION_CODES.M; |
| // If Wifi, then fetch SSID also |
| if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { |
| // Since Android 4.2 the SSID can be retrieved from NetworkInfo.getExtraInfo(). |
| if (networkInfo.getExtraInfo() != null && !"".equals(networkInfo.getExtraInfo())) { |
| return new NetworkState( |
| true, |
| networkInfo.getType(), |
| networkInfo.getSubtype(), |
| false, |
| networkInfo.getExtraInfo(), |
| false, |
| ""); |
| } |
| // Fetch WiFi SSID directly from WifiManagerDelegate if not in NetworkInfo. |
| return new NetworkState( |
| true, |
| networkInfo.getType(), |
| networkInfo.getSubtype(), |
| false, |
| wifiManagerDelegate.getWifiSsid(), |
| false, |
| ""); |
| } |
| return new NetworkState( |
| true, networkInfo.getType(), networkInfo.getSubtype(), false, null, false, ""); |
| } |
| |
| /** |
| * Fetches NetworkInfo for |network|. Does not account for underlying VPNs; see |
| * getNetworkInfo(Network) for a method that does. |
| */ |
| NetworkInfo getRawNetworkInfo(Network network) { |
| try { |
| return mConnectivityManager.getNetworkInfo(network); |
| } catch (NullPointerException firstException) { |
| // Rarely this unexpectedly throws. Retry or just return {@code null} if it fails. |
| try { |
| return mConnectivityManager.getNetworkInfo(network); |
| } catch (NullPointerException secondException) { |
| return null; |
| } |
| } |
| } |
| |
| /** Fetches NetworkInfo for |network|. */ |
| NetworkInfo getNetworkInfo(Network network) { |
| NetworkInfo networkInfo = getRawNetworkInfo(network); |
| if (networkInfo != null && networkInfo.getType() == TYPE_VPN) { |
| // When a VPN is in place the underlying network type can be queried via |
| // getActiveNetworkInfo() thanks to |
| // https://android.googlesource.com/platform/frameworks/base/+/d6a7980d |
| networkInfo = mConnectivityManager.getActiveNetworkInfo(); |
| } |
| return networkInfo; |
| } |
| |
| /** Returns connection type for |network|. */ |
| @ConnectionType |
| int getConnectionType(Network network) { |
| NetworkInfo networkInfo = getNetworkInfo(network); |
| if (networkInfo != null && networkInfo.isConnected()) { |
| return convertToConnectionType(networkInfo.getType(), networkInfo.getSubtype()); |
| } |
| return ConnectionType.CONNECTION_NONE; |
| } |
| |
| /** |
| * Returns all connected networks. This may include networks that aren't useful |
| * to Chrome (e.g. MMS, IMS, FOTA etc) or aren't accessible to Chrome (e.g. a VPN for |
| * another user); use {@link getAllNetworks} for a filtered list. |
| */ |
| @VisibleForTesting |
| protected Network[] getAllNetworksUnfiltered() { |
| Network[] networks = mConnectivityManager.getAllNetworks(); |
| // Very rarely this API inexplicably returns {@code null}, crbug.com/721116. |
| return networks == null ? new Network[0] : networks; |
| } |
| |
| /** |
| * Returns {@code true} if {@code network} applies to (and hence is accessible) to the |
| * current user. |
| */ |
| @VisibleForTesting |
| protected boolean vpnAccessible(Network network) { |
| // Determine if the VPN applies to the current user by seeing if a socket can be bound |
| // to the VPN. |
| Socket s = new Socket(); |
| // Disable detectUntaggedSockets StrictMode policy to avoid false positives, as |s| |
| // isn't used to send or receive traffic. https://crbug.com/946531 |
| try (StrictModeContext ignored = StrictModeContext.allowAllVmPolicies()) { |
| // Avoid using network.getSocketFactory().createSocket() because it leaks. |
| // https://crbug.com/805424 |
| network.bindSocket(s); |
| } catch (IOException e) { |
| // Failed to bind so this VPN isn't for the current user to use. |
| return false; |
| } finally { |
| try { |
| s.close(); |
| } catch (IOException e) { |
| // Not worth taking action on a failed close. |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Return the NetworkCapabilities for {@code network}, or {@code null} if they cannot |
| * be retrieved (e.g. {@code network} has disconnected). |
| */ |
| @VisibleForTesting |
| protected NetworkCapabilities getNetworkCapabilities(Network network) { |
| final int retryCount = 2; |
| for (int i = 0; i < retryCount; ++i) { |
| // This try-catch is a workaround for https://crbug.com/1218536. We ignore |
| // the exception intentionally. |
| try { |
| return mConnectivityManager.getNetworkCapabilities(network); |
| } catch (SecurityException e) { |
| // Do nothing. |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Registers networkCallback to receive notifications about networks |
| * that satisfy networkRequest. |
| */ |
| void registerNetworkCallback( |
| NetworkRequest networkRequest, NetworkCallback networkCallback, Handler handler) { |
| // Starting with Oreo specifying a Handler is allowed. Use this to avoid thread-hops. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { |
| // Samsung Android O devices aggressively trigger StrictMode violations. |
| // See https://crbug.com/1450175 for detail. |
| mConnectivityManager.registerNetworkCallback( |
| networkRequest, networkCallback, handler); |
| } |
| } else { |
| mConnectivityManager.registerNetworkCallback(networkRequest, networkCallback); |
| } |
| } |
| |
| /** |
| * Registers networkCallback to receive notifications about default network. |
| * Only callable on P and newer releases. |
| */ |
| @RequiresApi(Build.VERSION_CODES.P) |
| void registerDefaultNetworkCallback(NetworkCallback networkCallback, Handler handler) { |
| ApiHelperForO.registerDefaultNetworkCallback( |
| mConnectivityManager, networkCallback, handler); |
| } |
| |
| /** Unregisters networkCallback from receiving notifications. */ |
| void unregisterNetworkCallback(NetworkCallback networkCallback) { |
| mConnectivityManager.unregisterNetworkCallback(networkCallback); |
| } |
| |
| /** |
| * Returns the current default {@link Network}, or {@code null} if disconnected. |
| */ |
| @VisibleForTesting |
| public Network getDefaultNetwork() { |
| Network defaultNetwork = null; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| defaultNetwork = ApiHelperForM.getActiveNetwork(mConnectivityManager); |
| // getActiveNetwork() returning null cannot be trusted to indicate disconnected |
| // as it suffers from https://crbug.com/677365. |
| if (defaultNetwork != null) { |
| return defaultNetwork; |
| } |
| } |
| // Android Lollipop had no API to get the default network; only an |
| // API to return the NetworkInfo for the default network. To |
| // determine the default network one can find the network with |
| // type matching that of the default network. |
| final NetworkInfo defaultNetworkInfo = mConnectivityManager.getActiveNetworkInfo(); |
| if (defaultNetworkInfo == null) { |
| return null; |
| } |
| final Network[] networks = getAllNetworksFiltered(this, null); |
| for (Network network : networks) { |
| final NetworkInfo networkInfo = getRawNetworkInfo(network); |
| if (networkInfo != null |
| && (networkInfo.getType() == defaultNetworkInfo.getType() |
| // getActiveNetworkInfo() will not return TYPE_VPN types due to |
| // https://android.googlesource.com/platform/frameworks/base/+/d6a7980d |
| // so networkInfo.getType() can't be matched against |
| // defaultNetworkInfo.getType() but networkInfo.getType() should |
| // be TYPE_VPN. In the case of a VPN, getAllNetworks() will have |
| // returned just this VPN if it applies. |
| || networkInfo.getType() == TYPE_VPN)) { |
| // Android 10+ devices occasionally return multiple networks |
| // of the same type that are stuck in the CONNECTING state. |
| // Now that Java asserts are enabled, ignore these zombie |
| // networks here to avoid hitting the assert below. crbug.com/1361170 |
| if (defaultNetwork != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { |
| // If `network` is CONNECTING, ignore it. |
| if (networkInfo.getDetailedState() |
| == NetworkInfo.DetailedState.CONNECTING) { |
| continue; |
| } |
| // If `defaultNetwork` is CONNECTING, ignore it. |
| NetworkInfo prevDefaultNetworkInfo = getRawNetworkInfo(defaultNetwork); |
| if (prevDefaultNetworkInfo != null |
| && prevDefaultNetworkInfo.getDetailedState() |
| == NetworkInfo.DetailedState.CONNECTING) { |
| defaultNetwork = null; |
| } |
| } |
| if (defaultNetwork != null) { |
| // TODO(https://crbug.com/1361170): Investigate why there are multiple |
| // connected networks. |
| Log.e( |
| TAG, |
| "There should not be multiple connected " |
| + "networks of the same type. At least as of Android " |
| + "Marshmallow this is not supported. If this becomes " |
| + "supported this assertion may trigger."); |
| } |
| defaultNetwork = network; |
| } |
| } |
| return defaultNetwork; |
| } |
| } |
| |
| /** Queries the WifiManager for SSID of the current Wifi connection. */ |
| static class WifiManagerDelegate { |
| private final Context mContext; |
| // Lock all members below. |
| private final Object mLock = new Object(); |
| |
| // Has mHasWifiPermission been calculated. |
| @GuardedBy("mLock") |
| private boolean mHasWifiPermissionComputed; |
| |
| // Only valid when mHasWifiPermissionComputed is set. |
| @GuardedBy("mLock") |
| private boolean mHasWifiPermission; |
| |
| // Only valid when mHasWifiPermission is set. |
| @GuardedBy("mLock") |
| private WifiManager mWifiManager; |
| |
| WifiManagerDelegate(Context context) { |
| // Getting SSID requires more permissions in later Android releases. |
| assert Build.VERSION.SDK_INT < Build.VERSION_CODES.M; |
| mContext = context; |
| } |
| |
| // For testing. |
| WifiManagerDelegate() { |
| // All the methods below should be overridden. |
| mContext = null; |
| } |
| |
| // Lazily determine if app has ACCESS_WIFI_STATE permission. |
| @GuardedBy("mLock") |
| @SuppressLint("WifiManagerPotentialLeak") |
| private boolean hasPermissionLocked() { |
| if (mHasWifiPermissionComputed) { |
| return mHasWifiPermission; |
| } |
| mHasWifiPermission = |
| mContext.getPackageManager() |
| .checkPermission( |
| permission.ACCESS_WIFI_STATE, mContext.getPackageName()) |
| == PackageManager.PERMISSION_GRANTED; |
| // TODO(crbug.com/635567): Fix lint properly. |
| mWifiManager = |
| mHasWifiPermission |
| ? (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE) |
| : null; |
| mHasWifiPermissionComputed = true; |
| return mHasWifiPermission; |
| } |
| |
| String getWifiSsid() { |
| // Synchronized because this method can be called on multiple threads (e.g. mLooper |
| // from a private caller, and another thread calling a public API like |
| // getCurrentNetworkState) and is otherwise racy. |
| synchronized (mLock) { |
| // If app has permission it's faster to query WifiManager directly. |
| if (hasPermissionLocked()) { |
| WifiInfo wifiInfo = getWifiInfoLocked(); |
| if (wifiInfo != null) { |
| return wifiInfo.getSSID(); |
| } |
| return ""; |
| } |
| } |
| return AndroidNetworkLibrary.getWifiSSID(); |
| } |
| |
| // Fetches WifiInfo and records UMA for NullPointerExceptions. |
| @GuardedBy("mLock") |
| private WifiInfo getWifiInfoLocked() { |
| try { |
| return mWifiManager.getConnectionInfo(); |
| } catch (NullPointerException firstException) { |
| // Rarely this unexpectedly throws. Retry or just return {@code null} if it fails. |
| try { |
| return mWifiManager.getConnectionInfo(); |
| } catch (NullPointerException secondException) { |
| return null; |
| } |
| } |
| } |
| } |
| |
| // NetworkCallback used for listening for changes to the default network. |
| private class DefaultNetworkCallback extends NetworkCallback { |
| // If registered, notify connectionTypeChanged() to look for changes. |
| @Override |
| public void onAvailable(Network network) { |
| if (mRegistered) { |
| connectionTypeChanged(); |
| } |
| } |
| |
| @Override |
| public void onLost(final Network network) { |
| onAvailable(null); |
| } |
| |
| // LinkProperties changes include enabling/disabling DNS-over-TLS. |
| @Override |
| public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { |
| onAvailable(null); |
| } |
| } |
| |
| // NetworkCallback used for listening for changes to the default network. |
| // This version has two major bug fixes over the above DefaultNetworkCallback: |
| // 1. Avoids avoids calling synchronous ConnectivityManager methods which is prohibited inside |
| // NetworkCallbacks see "Do NOT call" here: |
| // |
| // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network) |
| // 2. Catches onCapabilitiesChanged() which includes cellular connections transitioning to and |
| // from SUSPENDED states. Failing to catch this could leave the NetworkChangeNotifier in |
| // an incorrect disconnected state, see crbug.com/1120144. |
| @RequiresApi(Build.VERSION_CODES.P) |
| private class AndroidRDefaultNetworkCallback extends NetworkCallback { |
| LinkProperties mLinkProperties; |
| NetworkCapabilities mNetworkCapabilities; |
| |
| @Override |
| public void onAvailable(Network network) { |
| // Clear accumulated state and wait for new state to be received. |
| // Android guarantees we receive onLinkPropertiesChanged and |
| // onNetworkCapabilities calls after onAvailable: |
| // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onCapabilitiesChanged(android.net.Network,%20android.net.NetworkCapabilities) |
| // so the call to connectionTypeChangedTo() is done when we have received the |
| // LinkProperties and NetworkCapabilities. |
| mLinkProperties = null; |
| mNetworkCapabilities = null; |
| } |
| |
| @Override |
| public void onLost(final Network network) { |
| mLinkProperties = null; |
| mNetworkCapabilities = null; |
| if (mRegistered) { |
| connectionTypeChangedTo(new NetworkState(false, -1, -1, false, null, false, "")); |
| } |
| } |
| |
| // LinkProperties changes include enabling/disabling DNS-over-TLS. |
| @Override |
| public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { |
| mLinkProperties = linkProperties; |
| if (mRegistered && mLinkProperties != null && mNetworkCapabilities != null) { |
| connectionTypeChangedTo(getNetworkState(network)); |
| } |
| } |
| |
| // CapabilitiesChanged includes cellular connections switching in and out of SUSPENDED. |
| @Override |
| public void onCapabilitiesChanged( |
| Network network, NetworkCapabilities networkCapabilities) { |
| mNetworkCapabilities = networkCapabilities; |
| if (mRegistered && mLinkProperties != null && mNetworkCapabilities != null) { |
| connectionTypeChangedTo(getNetworkState(network)); |
| } |
| } |
| |
| // Calculate current NetworkState. Unlike getNetworkState(), this method avoids calling |
| // synchronous ConnectivityManager methods which is prohibited inside NetworkCallbacks see |
| // "Do NOT call" here: |
| // https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network) |
| private NetworkState getNetworkState(Network network) { |
| // Initialize to unknown values then extract more accurate info |
| int type = -1; |
| int subtype = -1; |
| if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) |
| || mNetworkCapabilities.hasTransport( |
| NetworkCapabilities.TRANSPORT_WIFI_AWARE)) { |
| type = ConnectivityManager.TYPE_WIFI; |
| } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { |
| type = ConnectivityManager.TYPE_MOBILE; |
| // To get the subtype we need to make a synchronous ConnectivityManager call |
| // unfortunately. It's recommended to use TelephonyManager.getDataNetworkType() |
| // but that requires an additional permission. Worst case this might be inaccurate |
| // but getting the correct subtype is much much less important than getting the |
| // correct type. Incorrect type could make Chrome behave like it's offline, |
| // incorrect subtype will just make cellular bandwidth estimates incorrect. |
| NetworkInfo networkInfo = mConnectivityManagerDelegate.getRawNetworkInfo(network); |
| if (networkInfo != null) { |
| subtype = networkInfo.getSubtype(); |
| } |
| } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { |
| type = ConnectivityManager.TYPE_ETHERNET; |
| } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)) { |
| type = ConnectivityManager.TYPE_BLUETOOTH; |
| } else if (mNetworkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { |
| // Use ConnectivityManagerDelegate.getNetworkInfo(network) to find underlying |
| // network which has a more useful transport type. crbug.com/1208022 |
| NetworkInfo networkInfo = mConnectivityManagerDelegate.getNetworkInfo(network); |
| type = networkInfo != null ? networkInfo.getType() : ConnectivityManager.TYPE_VPN; |
| } |
| boolean isMetered = |
| !mNetworkCapabilities.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_NOT_METERED); |
| return new NetworkState( |
| true, |
| type, |
| subtype, |
| isMetered, |
| String.valueOf(networkToNetId(network)), |
| ApiHelperForP.isPrivateDnsActive(mLinkProperties), |
| ApiHelperForP.getPrivateDnsServerName(mLinkProperties)); |
| } |
| } |
| |
| // This class gets called back by ConnectivityManager whenever networks come |
| // and go. It gets called back on a special handler thread |
| // ConnectivityManager creates for making the callbacks. The callbacks in |
| // turn post to mLooper where mObserver lives. |
| private class MyNetworkCallback extends NetworkCallback { |
| // If non-null, this indicates a VPN is in place for the current user, and no other |
| // networks are accessible. |
| private Network mVpnInPlace; |
| |
| // Initialize mVpnInPlace. |
| void initializeVpnInPlace() { |
| final Network[] networks = getAllNetworksFiltered(mConnectivityManagerDelegate, null); |
| mVpnInPlace = null; |
| // If the filtered list of networks contains just a VPN, then that VPN is in place. |
| if (networks.length == 1) { |
| final NetworkCapabilities capabilities = |
| mConnectivityManagerDelegate.getNetworkCapabilities(networks[0]); |
| if (capabilities != null && capabilities.hasTransport(TRANSPORT_VPN)) { |
| mVpnInPlace = networks[0]; |
| } |
| } |
| } |
| |
| /** |
| * Should changes to network {@code network} be ignored due to a VPN being in place |
| * and blocking direct access to {@code network}? |
| * @param network Network to possibly consider ignoring changes to. |
| */ |
| private boolean ignoreNetworkDueToVpn(Network network) { |
| return mVpnInPlace != null && !mVpnInPlace.equals(network); |
| } |
| |
| /** |
| * Should changes to connected network {@code network} be ignored? |
| * @param network Network to possibly consider ignoring changes to. |
| * @param capabilities {@code NetworkCapabilities} for {@code network} if known, otherwise |
| * {@code null}. |
| * @return {@code true} when either: {@code network} is an inaccessible VPN, or has already |
| * disconnected. |
| */ |
| private boolean ignoreConnectedInaccessibleVpn( |
| Network network, NetworkCapabilities capabilities) { |
| // Fetch capabilities if not provided. |
| if (capabilities == null) { |
| capabilities = mConnectivityManagerDelegate.getNetworkCapabilities(network); |
| } |
| // Ignore inaccessible VPNs as they don't apply to Chrome. |
| return capabilities == null |
| || capabilities.hasTransport(TRANSPORT_VPN) |
| && !mConnectivityManagerDelegate.vpnAccessible(network); |
| } |
| |
| /** |
| * Should changes to connected network {@code network} be ignored? |
| * @param network Network to possible consider ignoring changes to. |
| * @param capabilities {@code NetworkCapabilities} for {@code network} if known, otherwise |
| * {@code null}. |
| */ |
| private boolean ignoreConnectedNetwork(Network network, NetworkCapabilities capabilities) { |
| return ignoreNetworkDueToVpn(network) |
| || ignoreConnectedInaccessibleVpn(network, capabilities); |
| } |
| |
| @Override |
| public void onAvailable(Network network) { |
| try (TraceEvent e = TraceEvent.scoped("NetworkChangeNotifierCallback::onAvailable")) { |
| final NetworkCapabilities capabilities = |
| mConnectivityManagerDelegate.getNetworkCapabilities(network); |
| if (ignoreConnectedNetwork(network, capabilities)) { |
| return; |
| } |
| final boolean makeVpnDefault = |
| capabilities.hasTransport(TRANSPORT_VPN) |
| && |
| // Only make the VPN the default if it isn't already. |
| (mVpnInPlace == null || !network.equals(mVpnInPlace)); |
| if (makeVpnDefault) { |
| mVpnInPlace = network; |
| } |
| final long netId = networkToNetId(network); |
| @ConnectionType |
| final int connectionType = mConnectivityManagerDelegate.getConnectionType(network); |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| mObserver.onNetworkConnect(netId, connectionType); |
| if (makeVpnDefault) { |
| // Make VPN the default network. |
| mObserver.onConnectionTypeChanged(connectionType); |
| // Purge all other networks as they're inaccessible to Chrome |
| // now. |
| mObserver.purgeActiveNetworkList(new long[] {netId}); |
| } |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onCapabilitiesChanged( |
| Network network, NetworkCapabilities networkCapabilities) { |
| try (TraceEvent e = |
| TraceEvent.scoped("NetworkChangeNotifierCallback::onCapabilitiesChanged")) { |
| if (ignoreConnectedNetwork(network, networkCapabilities)) { |
| return; |
| } |
| // A capabilities change may indicate the ConnectionType has changed, |
| // so forward the new ConnectionType along to observer. |
| final long netId = networkToNetId(network); |
| final int connectionType = mConnectivityManagerDelegate.getConnectionType(network); |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| mObserver.onNetworkConnect(netId, connectionType); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onLosing(Network network, int maxMsToLive) { |
| try (TraceEvent e = TraceEvent.scoped("NetworkChangeNotifierCallback::onLosing")) { |
| if (ignoreConnectedNetwork(network, null)) { |
| return; |
| } |
| final long netId = networkToNetId(network); |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| mObserver.onNetworkSoonToDisconnect(netId); |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onLost(final Network network) { |
| try (TraceEvent e = TraceEvent.scoped("NetworkChangeNotifierCallback::onLost")) { |
| if (ignoreNetworkDueToVpn(network)) { |
| return; |
| } |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| mObserver.onNetworkDisconnect(networkToNetId(network)); |
| } |
| }); |
| // If the VPN is going away, inform observer that other networks that were |
| // previously hidden by ignoreNetworkDueToVpn() are now available for use, now that |
| // this user's traffic is not forced into the VPN. |
| if (mVpnInPlace != null) { |
| assert network.equals(mVpnInPlace); |
| mVpnInPlace = null; |
| for (Network newNetwork : |
| getAllNetworksFiltered(mConnectivityManagerDelegate, network)) { |
| onAvailable(newNetwork); |
| } |
| @ConnectionType |
| final int newConnectionType = getCurrentNetworkState().getConnectionType(); |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| mObserver.onConnectionTypeChanged(newConnectionType); |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Abstract class for providing a policy regarding when the NetworkChangeNotifier |
| * should listen for network changes. |
| */ |
| public abstract static class RegistrationPolicy { |
| private NetworkChangeNotifierAutoDetect mNotifier; |
| |
| /** Start listening for network changes. */ |
| protected final void register() { |
| assert mNotifier != null; |
| mNotifier.register(); |
| } |
| |
| /** Stop listening for network changes. */ |
| protected final void unregister() { |
| assert mNotifier != null; |
| mNotifier.unregister(); |
| } |
| |
| /** |
| * Initializes the policy with the notifier, overriding subclasses should always |
| * call this method. |
| */ |
| protected void init(NetworkChangeNotifierAutoDetect notifier) { |
| mNotifier = notifier; |
| } |
| |
| protected abstract void destroy(); |
| } |
| |
| private static final String TAG = NetworkChangeNotifierAutoDetect.class.getSimpleName(); |
| private static final int UNKNOWN_LINK_SPEED = -1; |
| |
| // {@link Looper} for the thread this object lives on. |
| private final Looper mLooper; |
| // Used to post to the thread this object lives on. |
| private final Handler mHandler; |
| // {@link IntentFilter} for incoming global broadcast {@link Intent}s this object listens for. |
| private final NetworkConnectivityIntentFilter mIntentFilter; |
| // Notifications are sent to this {@link Observer}. |
| private final Observer mObserver; |
| private final RegistrationPolicy mRegistrationPolicy; |
| // Starting with Android Pie, used to detect changes in default network. |
| private NetworkCallback mDefaultNetworkCallback; |
| |
| // mConnectivityManagerDelegates and mWifiManagerDelegate are only non-final for testing. |
| private ConnectivityManagerDelegate mConnectivityManagerDelegate; |
| private WifiManagerDelegate mWifiManagerDelegate; |
| // mNetworkCallback and mNetworkRequest are only non-null in Android L and above. |
| // mNetworkCallback will be null if ConnectivityManager.registerNetworkCallback() ever fails. |
| private MyNetworkCallback mNetworkCallback; |
| private NetworkRequest mNetworkRequest; |
| private boolean mRegistered; |
| private NetworkState mNetworkState; |
| // When a BroadcastReceiver is registered for a sticky broadcast that has been sent out at |
| // least once, onReceive() will immediately be called. mIgnoreNextBroadcast is set to true |
| // when this class is registered in such a circumstance, and indicates that the next |
| // invokation of onReceive() can be ignored as the state hasn't actually changed. Immediately |
| // prior to mIgnoreNextBroadcast being set, all internal state is updated to the current device |
| // state so were this initial onReceive() call not ignored, no signals would be passed to |
| // observers anyhow as the state hasn't changed. This is simply an optimization to avoid |
| // useless work. |
| private boolean mIgnoreNextBroadcast; |
| // mSignal is set to false when it's not worth calculating if signals to Observers should |
| // be sent out because this class is being constructed and the internal state has just |
| // been updated to the current device state, so no signals are necessary. This is simply an |
| // optimization to avoid useless work. |
| private boolean mShouldSignalObserver; |
| // Indicates if ConnectivityManager.registerNetworkRequest() ever failed. When true, no |
| // network-specific callbacks (e.g. Observer.onNetwork*() ) will be issued. |
| private boolean mRegisterNetworkCallbackFailed; |
| |
| /** Observer interface by which observer is notified of network changes. */ |
| public static interface Observer { |
| /** Called when default network changes. */ |
| public void onConnectionTypeChanged(@ConnectionType int newConnectionType); |
| |
| /** Called when connection cost of default network changes. */ |
| public void onConnectionCostChanged(int newConnectionCost); |
| |
| /** Called when connection subtype of default network changes. */ |
| public void onConnectionSubtypeChanged(int newConnectionSubtype); |
| |
| /** |
| * Called when device connects to network with NetID netId. For |
| * example device associates with a WiFi access point. |
| * connectionType is the type of the network; a member of |
| * ConnectionType. Only called on Android L and above. |
| */ |
| public void onNetworkConnect(long netId, int connectionType); |
| |
| /** |
| * Called when device determines the connection to the network with |
| * NetID netId is no longer preferred, for example when a device |
| * transitions from cellular to WiFi it might deem the cellular |
| * connection no longer preferred. The device will disconnect from |
| * the network in 30s allowing network communications on that network |
| * to wrap up. Only called on Android L and above. |
| */ |
| public void onNetworkSoonToDisconnect(long netId); |
| |
| /** |
| * Called when device disconnects from network with NetID netId. |
| * Only called on Android L and above. |
| */ |
| public void onNetworkDisconnect(long netId); |
| |
| /** |
| * Called to cause a purge of cached lists of active networks, of any |
| * networks not in the accompanying list of active networks. This is |
| * issued if a period elapsed where disconnected notifications may have |
| * been missed, and acts to keep cached lists of active networks |
| * accurate. Only called on Android L and above. |
| */ |
| public void purgeActiveNetworkList(long[] activeNetIds); |
| } |
| |
| /** |
| * Constructs a NetworkChangeNotifierAutoDetect. Lives on calling thread, receives broadcast |
| * notifications on the UI thread and forwards the notifications to be processed on the calling |
| * thread. |
| * @param policy The RegistrationPolicy which determines when this class should watch |
| * for network changes (e.g. see (@link RegistrationPolicyAlwaysRegister} and |
| * {@link RegistrationPolicyApplicationStatus}). |
| */ |
| public NetworkChangeNotifierAutoDetect(Observer observer, RegistrationPolicy policy) { |
| mLooper = Looper.myLooper(); |
| mHandler = new Handler(mLooper); |
| mObserver = observer; |
| mConnectivityManagerDelegate = |
| new ConnectivityManagerDelegate(ContextUtils.getApplicationContext()); |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { |
| mWifiManagerDelegate = new WifiManagerDelegate(ContextUtils.getApplicationContext()); |
| } |
| mNetworkCallback = new MyNetworkCallback(); |
| mNetworkRequest = |
| new NetworkRequest.Builder() |
| .addCapability(NET_CAPABILITY_INTERNET) |
| // Need to hear about VPNs too. |
| .removeCapability(NET_CAPABILITY_NOT_VPN) |
| .build(); |
| |
| // Use AndroidRDefaultNetworkCallback to fix Android R issue crbug.com/1120144. |
| // This NetworkCallback could be used on O+ (where onCapabilitiesChanged and |
| // onLinkProperties callbacks are guaranteed to be called after onAvailable) |
| // but is only necessary on Android R+. For now it's only used on R+ to reduce |
| // churn. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { |
| mDefaultNetworkCallback = new AndroidRDefaultNetworkCallback(); |
| } else { |
| mDefaultNetworkCallback = |
| Build.VERSION.SDK_INT >= Build.VERSION_CODES.P |
| ? new DefaultNetworkCallback() |
| : null; |
| } |
| mNetworkState = getCurrentNetworkState(); |
| mIntentFilter = new NetworkConnectivityIntentFilter(); |
| mIgnoreNextBroadcast = false; |
| mShouldSignalObserver = false; |
| mRegistrationPolicy = policy; |
| mRegistrationPolicy.init(this); |
| mShouldSignalObserver = true; |
| } |
| |
| private boolean onThread() { |
| return mLooper == Looper.myLooper(); |
| } |
| |
| private void assertOnThread() { |
| if (BuildConfig.ENABLE_ASSERTS && !onThread()) { |
| throw new IllegalStateException( |
| "Must be called on NetworkChangeNotifierAutoDetect thread."); |
| } |
| } |
| |
| private void runOnThread(Runnable r) { |
| if (onThread()) { |
| r.run(); |
| } else { |
| // Once execution begins on the correct thread, make sure unregister() hasn't |
| // been called in the mean time. |
| mHandler.post( |
| () -> { |
| if (mRegistered) r.run(); |
| }); |
| } |
| } |
| |
| /** Allows overriding the ConnectivityManagerDelegate for tests. */ |
| void setConnectivityManagerDelegateForTests(ConnectivityManagerDelegate delegate) { |
| var oldValue = mConnectivityManagerDelegate; |
| mConnectivityManagerDelegate = delegate; |
| ResettersForTesting.register(() -> mConnectivityManagerDelegate = oldValue); |
| } |
| |
| /** Allows overriding the WifiManagerDelegate for tests. */ |
| void setWifiManagerDelegateForTests(WifiManagerDelegate delegate) { |
| var oldValue = mWifiManagerDelegate; |
| mWifiManagerDelegate = delegate; |
| ResettersForTesting.register(() -> mWifiManagerDelegate = oldValue); |
| } |
| |
| @VisibleForTesting |
| RegistrationPolicy getRegistrationPolicy() { |
| return mRegistrationPolicy; |
| } |
| |
| /** Returns whether the object has registered to receive network connectivity intents. */ |
| boolean isReceiverRegisteredForTesting() { |
| return mRegistered; |
| } |
| |
| public void destroy() { |
| assertOnThread(); |
| mRegistrationPolicy.destroy(); |
| unregister(); |
| } |
| |
| /** Registers a BroadcastReceiver in the given context. */ |
| public void register() { |
| assertOnThread(); |
| if (mRegistered) { |
| // Even when registered previously, Android may not send callbacks about change of |
| // network state when the device screen is turned on from off. Get the most up-to-date |
| // network state. See https://crbug.com/1007998 for more details. |
| connectionTypeChanged(); |
| return; |
| } |
| |
| if (mShouldSignalObserver) { |
| connectionTypeChanged(); |
| } |
| if (mDefaultNetworkCallback != null) { |
| try { |
| mConnectivityManagerDelegate.registerDefaultNetworkCallback( |
| mDefaultNetworkCallback, mHandler); |
| } catch (RuntimeException e) { |
| // If registering a default network callback failed, fallback to |
| // listening for CONNECTIVITY_ACTION broadcast. |
| mDefaultNetworkCallback = null; |
| } |
| } |
| if (mDefaultNetworkCallback == null) { |
| // When registering for a sticky broadcast, like CONNECTIVITY_ACTION, if |
| // registerReceiver returns non-null, it means the broadcast was previously issued and |
| // onReceive() will be immediately called with this previous Intent. Since this initial |
| // callback doesn't actually indicate a network change, we can ignore it by setting |
| // mIgnoreNextBroadcast. |
| mIgnoreNextBroadcast = |
| ContextUtils.registerProtectedBroadcastReceiver( |
| ContextUtils.getApplicationContext(), this, mIntentFilter) |
| != null; |
| } |
| mRegistered = true; |
| |
| if (mNetworkCallback != null) { |
| mNetworkCallback.initializeVpnInPlace(); |
| try { |
| mConnectivityManagerDelegate.registerNetworkCallback( |
| mNetworkRequest, mNetworkCallback, mHandler); |
| } catch (RuntimeException e) { |
| mRegisterNetworkCallbackFailed = true; |
| // If Android thinks this app has used up all available NetworkRequests, don't |
| // bother trying to register any more callbacks as Android will still think |
| // all available NetworkRequests are used up and fail again needlessly. |
| // Also don't bother unregistering as this call didn't actually register. |
| // See crbug.com/791025 for more info. |
| mNetworkCallback = null; |
| } |
| if (!mRegisterNetworkCallbackFailed && mShouldSignalObserver) { |
| // registerNetworkCallback() will rematch the NetworkRequest |
| // against active networks, so a cached list of active networks |
| // will be repopulated immediatly after this. However we need to |
| // purge any cached networks as they may have been disconnected |
| // while mNetworkCallback was unregistered. |
| final Network[] networks = |
| getAllNetworksFiltered(mConnectivityManagerDelegate, null); |
| // Convert Networks to NetIDs. |
| final long[] netIds = new long[networks.length]; |
| for (int i = 0; i < networks.length; i++) { |
| netIds[i] = networkToNetId(networks[i]); |
| } |
| mObserver.purgeActiveNetworkList(netIds); |
| } |
| } |
| } |
| |
| /** Unregisters a BroadcastReceiver in the given context. */ |
| public void unregister() { |
| assertOnThread(); |
| if (!mRegistered) return; |
| mRegistered = false; |
| if (mNetworkCallback != null) { |
| mConnectivityManagerDelegate.unregisterNetworkCallback(mNetworkCallback); |
| } |
| if (mDefaultNetworkCallback != null) { |
| mConnectivityManagerDelegate.unregisterNetworkCallback(mDefaultNetworkCallback); |
| } else { |
| ContextUtils.getApplicationContext().unregisterReceiver(this); |
| } |
| } |
| |
| public NetworkState getCurrentNetworkState() { |
| return mConnectivityManagerDelegate.getNetworkState(mWifiManagerDelegate); |
| } |
| |
| /** |
| * Returns all connected networks that are useful and accessible to Chrome. |
| * @param ignoreNetwork ignore this network as if it is not connected. |
| */ |
| private static Network[] getAllNetworksFiltered( |
| ConnectivityManagerDelegate connectivityManagerDelegate, Network ignoreNetwork) { |
| Network[] networks = connectivityManagerDelegate.getAllNetworksUnfiltered(); |
| // Whittle down |networks| into just the list of networks useful to us. |
| int filteredIndex = 0; |
| for (Network network : networks) { |
| if (network.equals(ignoreNetwork)) { |
| continue; |
| } |
| final NetworkCapabilities capabilities = |
| connectivityManagerDelegate.getNetworkCapabilities(network); |
| if (capabilities == null || !capabilities.hasCapability(NET_CAPABILITY_INTERNET)) { |
| continue; |
| } |
| if (capabilities.hasTransport(TRANSPORT_VPN)) { |
| // If we can access the VPN then... |
| if (connectivityManagerDelegate.vpnAccessible(network)) { |
| // ...we cannot access any other network, so return just the VPN. |
| return new Network[] {network}; |
| } else { |
| // ...otherwise ignore it as we cannot use it. |
| continue; |
| } |
| } |
| networks[filteredIndex++] = network; |
| } |
| return Arrays.copyOf(networks, filteredIndex); |
| } |
| |
| /** Returns all connected networks that are useful and accessible to Chrome. */ |
| public Network[] getNetworksForTesting() { |
| return getAllNetworksFiltered(mConnectivityManagerDelegate, null); |
| } |
| |
| /** |
| * Returns an array of all of the device's currently connected |
| * networks and ConnectionTypes, including only those that are useful and accessible to Chrome. |
| * Array elements are a repeated sequence of: |
| * NetID of network |
| * ConnectionType of network |
| * Only available when auto-detection has been enabled. |
| */ |
| public long[] getNetworksAndTypes() { |
| final Network networks[] = getAllNetworksFiltered(mConnectivityManagerDelegate, null); |
| final long networksAndTypes[] = new long[networks.length * 2]; |
| int index = 0; |
| for (Network network : networks) { |
| networksAndTypes[index++] = networkToNetId(network); |
| networksAndTypes[index++] = mConnectivityManagerDelegate.getConnectionType(network); |
| } |
| return networksAndTypes; |
| } |
| |
| /** |
| * Returns the device's current default connected network used for |
| * communication. |
| * Returns null when not implemented. |
| */ |
| public Network getDefaultNetwork() { |
| return mConnectivityManagerDelegate.getDefaultNetwork(); |
| } |
| |
| /** |
| * Returns NetID of device's current default connected network used for |
| * communication. |
| * Returns NetId.INVALID when not implemented. |
| */ |
| public long getDefaultNetId() { |
| Network network = getDefaultNetwork(); |
| return network == null ? NetId.INVALID : networkToNetId(network); |
| } |
| |
| /** |
| * Returns {@code true} if NetworkCallback failed to register, indicating that network-specific |
| * callbacks will not be issued. |
| */ |
| public boolean registerNetworkCallbackFailed() { |
| return mRegisterNetworkCallbackFailed; |
| } |
| |
| /** Returns the connection type for the given ConnectivityManager type and subtype. */ |
| @ConnectionType |
| private static int convertToConnectionType(int type, int subtype) { |
| switch (type) { |
| case ConnectivityManager.TYPE_ETHERNET: |
| return ConnectionType.CONNECTION_ETHERNET; |
| case ConnectivityManager.TYPE_WIFI: |
| return ConnectionType.CONNECTION_WIFI; |
| case ConnectivityManager.TYPE_WIMAX: |
| return ConnectionType.CONNECTION_4G; |
| case ConnectivityManager.TYPE_BLUETOOTH: |
| return ConnectionType.CONNECTION_BLUETOOTH; |
| case ConnectivityManager.TYPE_MOBILE: |
| case ConnectivityManager.TYPE_MOBILE_DUN: |
| case ConnectivityManager.TYPE_MOBILE_HIPRI: |
| // Use information from TelephonyManager to classify the connection. |
| switch (subtype) { |
| case TelephonyManager.NETWORK_TYPE_GPRS: |
| case TelephonyManager.NETWORK_TYPE_EDGE: |
| case TelephonyManager.NETWORK_TYPE_CDMA: |
| case TelephonyManager.NETWORK_TYPE_1xRTT: |
| case TelephonyManager.NETWORK_TYPE_IDEN: |
| return ConnectionType.CONNECTION_2G; |
| case TelephonyManager.NETWORK_TYPE_UMTS: |
| case TelephonyManager.NETWORK_TYPE_EVDO_0: |
| case TelephonyManager.NETWORK_TYPE_EVDO_A: |
| case TelephonyManager.NETWORK_TYPE_HSDPA: |
| case TelephonyManager.NETWORK_TYPE_HSUPA: |
| case TelephonyManager.NETWORK_TYPE_HSPA: |
| case TelephonyManager.NETWORK_TYPE_EVDO_B: |
| case TelephonyManager.NETWORK_TYPE_EHRPD: |
| case TelephonyManager.NETWORK_TYPE_HSPAP: |
| return ConnectionType.CONNECTION_3G; |
| case TelephonyManager.NETWORK_TYPE_LTE: |
| return ConnectionType.CONNECTION_4G; |
| case TelephonyManager.NETWORK_TYPE_NR: |
| return ConnectionType.CONNECTION_5G; |
| default: |
| return ConnectionType.CONNECTION_UNKNOWN; |
| } |
| default: |
| return ConnectionType.CONNECTION_UNKNOWN; |
| } |
| } |
| |
| // BroadcastReceiver |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| runOnThread( |
| new Runnable() { |
| @Override |
| public void run() { |
| if (mIgnoreNextBroadcast) { |
| mIgnoreNextBroadcast = false; |
| return; |
| } |
| connectionTypeChanged(); |
| } |
| }); |
| } |
| |
| private void connectionTypeChanged() { |
| connectionTypeChangedTo(getCurrentNetworkState()); |
| } |
| |
| private void connectionTypeChangedTo(NetworkState networkState) { |
| if (networkState.getConnectionType() != mNetworkState.getConnectionType() |
| || !networkState.getNetworkIdentifier().equals(mNetworkState.getNetworkIdentifier()) |
| || networkState.isPrivateDnsActive() != mNetworkState.isPrivateDnsActive() |
| || !networkState |
| .getPrivateDnsServerName() |
| .equals(mNetworkState.getPrivateDnsServerName())) { |
| mObserver.onConnectionTypeChanged(networkState.getConnectionType()); |
| } |
| if (networkState.getConnectionType() != mNetworkState.getConnectionType() |
| || networkState.getConnectionSubtype() != mNetworkState.getConnectionSubtype()) { |
| mObserver.onConnectionSubtypeChanged(networkState.getConnectionSubtype()); |
| } |
| if (networkState.getConnectionCost() != mNetworkState.getConnectionCost()) { |
| mObserver.onConnectionCostChanged(networkState.getConnectionCost()); |
| } |
| mNetworkState = networkState; |
| } |
| |
| private static class NetworkConnectivityIntentFilter extends IntentFilter { |
| NetworkConnectivityIntentFilter() { |
| addAction(ConnectivityManager.CONNECTIVITY_ACTION); |
| } |
| } |
| |
| /** |
| * Extracts NetID of Network on Lollipop and NetworkHandle (which is munged NetID) on |
| * Marshmallow and newer releases. |
| * TODO(crbug.com/1489183): Rename networkToNetId to something meaningful and update |
| * javadoc comment. |
| */ |
| public static long networkToNetId(Network network) { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| return ApiHelperForM.getNetworkHandle(network); |
| } else { |
| // NOTE(pauljensen): This depends on Android framework implementation details. These |
| // details cannot change because Lollipop is long since released. |
| // NetIDs are only 16-bit so use parseInt. This function returns a long because |
| // getNetworkHandle() returns a long. |
| return Integer.parseInt(network.toString()); |
| } |
| } |
| } |