blob: 2154ab9c0d0f7afa20222f01f4b3ea5947395999 [file] [log] [blame]
// 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());
}
}
}