| // Copyright 2012 The Chromium Authors. All rights reserved. |
| // 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.annotation.TargetApi; |
| 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 org.chromium.base.ApplicationState; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.BuildConfig; |
| import org.chromium.base.BuildInfo; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.compat.ApiHelperForM; |
| |
| 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; |
| // 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; |
| |
| public NetworkState(boolean connected, int type, int subtype, String networkIdentifier, |
| boolean isPrivateDnsActive) { |
| mConnected = connected; |
| mType = type; |
| mSubtype = subtype; |
| mNetworkIdentifier = networkIdentifier == null ? "" : networkIdentifier; |
| mIsPrivateDnsActive = isPrivateDnsActive; |
| } |
| |
| public boolean isConnected() { |
| return mConnected; |
| } |
| |
| public int getNetworkType() { |
| return mType; |
| } |
| |
| 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 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: |
| // 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; |
| } |
| } |
| |
| /** Queries the ConnectivityManager for information about the current connection. */ |
| static class ConnectivityManagerDelegate { |
| private final ConnectivityManager mConnectivityManager; |
| |
| 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. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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 (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |
| // https://crbug.com/677365 primarily affects only Lollipop and higher versions. |
| return null; |
| } |
| |
| 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 = ApiHelperForM.getNetworkInfo(mConnectivityManager, network); |
| } else { |
| networkInfo = mConnectivityManager.getActiveNetworkInfo(); |
| } |
| networkInfo = processActiveNetworkInfo(networkInfo); |
| if (networkInfo == null) { |
| return new NetworkState(false, -1, -1, null, false); |
| } |
| if (network != null) { |
| return new NetworkState(true, networkInfo.getType(), networkInfo.getSubtype(), |
| String.valueOf(networkToNetId(network)), |
| BuildInfo.isAtLeastP() |
| && AndroidNetworkLibrary.isPrivateDnsActive( |
| mConnectivityManager.getLinkProperties(network))); |
| } |
| 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(), |
| networkInfo.getExtraInfo(), false); |
| } |
| // Fetch WiFi SSID directly from WifiManagerDelegate if not in NetworkInfo. |
| return new NetworkState(true, networkInfo.getType(), networkInfo.getSubtype(), |
| wifiManagerDelegate.getWifiSsid(), false); |
| } |
| return new NetworkState( |
| true, networkInfo.getType(), networkInfo.getSubtype(), null, false); |
| } |
| |
| // Fetches NetworkInfo and records UMA for NullPointerExceptions. |
| private NetworkInfo getNetworkInfo(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; |
| } |
| } |
| } |
| |
| /** |
| * Returns connection type for |network|. |
| * Only callable on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| @ConnectionType |
| int getConnectionType(Network network) { |
| NetworkInfo networkInfo = getNetworkInfo(network); |
| if (networkInfo != null && networkInfo.getType() == TYPE_VPN) { |
| // When a VPN is in place the underlying network type can be queried via |
| // getActiveNeworkInfo() thanks to |
| // https://android.googlesource.com/platform/frameworks/base/+/d6a7980d |
| networkInfo = mConnectivityManager.getActiveNetworkInfo(); |
| } |
| 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. |
| * Only callable on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| @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. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| @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(); |
| try { |
| // 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). |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| @VisibleForTesting |
| protected NetworkCapabilities getNetworkCapabilities(Network network) { |
| return mConnectivityManager.getNetworkCapabilities(network); |
| } |
| |
| /** |
| * Registers networkCallback to receive notifications about networks |
| * that satisfy networkRequest. |
| * Only callable on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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) { |
| 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. |
| */ |
| @TargetApi(Build.VERSION_CODES.P) |
| void registerDefaultNetworkCallback(NetworkCallback networkCallback, Handler handler) { |
| mConnectivityManager.registerDefaultNetworkCallback(networkCallback, handler); |
| } |
| |
| /** |
| * Unregisters networkCallback from receiving notifications. |
| * Only callable on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| void unregisterNetworkCallback(NetworkCallback networkCallback) { |
| mConnectivityManager.unregisterNetworkCallback(networkCallback); |
| } |
| |
| /** |
| * Returns the current default {@link Network}, or {@code null} if disconnected. |
| * Only callable on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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 = getNetworkInfo(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)) { |
| // 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. |
| assert defaultNetwork == null; |
| 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. |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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); |
| } |
| } |
| |
| // 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. |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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) { |
| final NetworkCapabilities capabilities = |
| mConnectivityManagerDelegate.getNetworkCapabilities(network); |
| if (ignoreConnectedNetwork(network, capabilities)) { |
| return; |
| } |
| final boolean makeVpnDefault = capabilities.hasTransport(TRANSPORT_VPN); |
| 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) { |
| 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) { |
| 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) { |
| 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 final DefaultNetworkCallback 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 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}). |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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()); |
| } |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| mNetworkCallback = new MyNetworkCallback(); |
| mNetworkRequest = new NetworkRequest.Builder() |
| .addCapability(NET_CAPABILITY_INTERNET) |
| // Need to hear about VPNs too. |
| .removeCapability(NET_CAPABILITY_NOT_VPN) |
| .build(); |
| } else { |
| mNetworkCallback = null; |
| mNetworkRequest = null; |
| } |
| mDefaultNetworkCallback = BuildInfo.isAtLeastP() ? 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.DCHECK_IS_ON && !onThread()) { |
| throw new IllegalStateException( |
| "Must be called on NetworkChangeNotifierAutoDetect thread."); |
| } |
| } |
| |
| private void runOnThread(Runnable r) { |
| if (onThread()) { |
| r.run(); |
| } else { |
| mHandler.post(r); |
| } |
| } |
| |
| /** |
| * Allows overriding the ConnectivityManagerDelegate for tests. |
| */ |
| void setConnectivityManagerDelegateForTests(ConnectivityManagerDelegate delegate) { |
| mConnectivityManagerDelegate = delegate; |
| } |
| |
| /** |
| * Allows overriding the WifiManagerDelegate for tests. |
| */ |
| void setWifiManagerDelegateForTests(WifiManagerDelegate delegate) { |
| mWifiManagerDelegate = delegate; |
| } |
| |
| @VisibleForTesting |
| RegistrationPolicy getRegistrationPolicy() { |
| return mRegistrationPolicy; |
| } |
| |
| /** |
| * Returns whether the object has registered to receive network connectivity intents. |
| */ |
| @VisibleForTesting |
| boolean isReceiverRegisteredForTesting() { |
| return mRegistered; |
| } |
| |
| public void destroy() { |
| assertOnThread(); |
| mRegistrationPolicy.destroy(); |
| unregister(); |
| } |
| |
| /** |
| * Registers a BroadcastReceiver in the given context. |
| */ |
| public void register() { |
| assertOnThread(); |
| if (mRegistered) return; |
| |
| if (mShouldSignalObserver) { |
| connectionTypeChanged(); |
| } |
| if (mDefaultNetworkCallback != null) { |
| mConnectivityManagerDelegate.registerDefaultNetworkCallback( |
| mDefaultNetworkCallback, mHandler); |
| } else { |
| // 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.getApplicationContext().registerReceiver(this, mIntentFilter) |
| != null; |
| } |
| mRegistered = true; |
| |
| if (mNetworkCallback != null) { |
| mNetworkCallback.initializeVpnInPlace(); |
| try { |
| mConnectivityManagerDelegate.registerNetworkCallback( |
| mNetworkRequest, mNetworkCallback, mHandler); |
| } catch (IllegalArgumentException 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. |
| * Only callable on Lollipop and newer releases. |
| * @param ignoreNetwork ignore this network as if it is not connected. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 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 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 on Lollipop and newer releases and when auto-detection has |
| * been enabled. |
| */ |
| public long[] getNetworksAndTypes() { |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |
| return new long[0]; |
| } |
| 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 NetID of device's current default connected network used for |
| * communication. |
| * Only implemented on Lollipop and newer releases, returns NetId.INVALID |
| * when not implemented. |
| */ |
| public long getDefaultNetId() { |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { |
| return NetId.INVALID; |
| } |
| Network network = mConnectivityManagerDelegate.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: |
| // 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; |
| 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() { |
| // Once execution begins on the correct thread, make sure unregister() hasn't |
| // been called in the mean time. Ignore the broadcast if unregister() was called. |
| if (!mRegistered) { |
| return; |
| } |
| if (mIgnoreNextBroadcast) { |
| mIgnoreNextBroadcast = false; |
| return; |
| } |
| connectionTypeChanged(); |
| } |
| }); |
| } |
| |
| private void connectionTypeChanged() { |
| NetworkState networkState = getCurrentNetworkState(); |
| if (networkState.getConnectionType() != mNetworkState.getConnectionType() |
| || !networkState.getNetworkIdentifier().equals(mNetworkState.getNetworkIdentifier()) |
| || networkState.isPrivateDnsActive() != mNetworkState.isPrivateDnsActive()) { |
| mObserver.onConnectionTypeChanged(networkState.getConnectionType()); |
| } |
| if (networkState.getConnectionType() != mNetworkState.getConnectionType() |
| || networkState.getConnectionSubtype() != mNetworkState.getConnectionSubtype()) { |
| mObserver.onConnectionSubtypeChanged(networkState.getConnectionSubtype()); |
| } |
| mNetworkState = networkState; |
| } |
| |
| // TODO(crbug.com/635567): Fix this properly. |
| @SuppressLint({"NewApi", "ParcelCreator"}) |
| 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. Only available on Lollipop and newer releases. |
| */ |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| @VisibleForTesting |
| 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()); |
| } |
| } |
| } |