DO NOT MERGE ANYWHERE Add CONNECTIVITY_USE_RESTRICTED_NETWORKS permission am: 849682f5a0 -s ours am: d55ff11c4b -s ours
am: 0a159e0967 -s ours
Change-Id: Ibe074076d22a736394a01d27aa9e1c7ed73458b2
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index a45e6f5..b9e9b28 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -343,6 +343,15 @@
public static final String ACTION_PROMPT_UNVALIDATED = "android.net.conn.PROMPT_UNVALIDATED";
/**
+ * Action used to display a dialog that asks the user whether to avoid a network that is no
+ * longer validated. This intent is used to start the dialog in settings via startActivity.
+ *
+ * @hide
+ */
+ public static final String ACTION_PROMPT_LOST_VALIDATION =
+ "android.net.conn.PROMPT_LOST_VALIDATION";
+
+ /**
* Invalid tethering type.
* @see #startTethering(int, OnStartTetheringCallback, boolean)
* @hide
@@ -1035,6 +1044,26 @@
}
/**
+ * Request that this callback be invoked at ConnectivityService's earliest
+ * convenience with the current satisfying network's LinkProperties.
+ * If no such network exists no callback invocation is performed.
+ *
+ * The callback must have been registered with #requestNetwork() or
+ * #registerDefaultNetworkCallback(); callbacks registered with
+ * registerNetworkCallback() are not specific to any particular Network so
+ * do not cause any updates.
+ *
+ * @hide
+ */
+ public void requestLinkProperties(NetworkCallback networkCallback) {
+ try {
+ mService.requestLinkProperties(networkCallback.networkRequest);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Get the {@link android.net.NetworkCapabilities} for the given {@link Network}. This
* will return {@code null} if the network is unknown.
* <p>This method requires the caller to hold the permission
@@ -1052,6 +1081,26 @@
}
/**
+ * Request that this callback be invoked at ConnectivityService's earliest
+ * convenience with the current satisfying network's NetworkCapabilities.
+ * If no such network exists no callback invocation is performed.
+ *
+ * The callback must have been registered with #requestNetwork() or
+ * #registerDefaultNetworkCallback(); callbacks registered with
+ * registerNetworkCallback() are not specific to any particular Network so
+ * do not cause any updates.
+ *
+ * @hide
+ */
+ public void requestNetworkCapabilities(NetworkCallback networkCallback) {
+ try {
+ mService.requestNetworkCapabilities(networkCallback.networkRequest);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gets the URL that should be used for resolving whether a captive portal is present.
* 1. This URL should respond with a 204 response to a GET request to indicate no captive
* portal is present.
@@ -3103,14 +3152,11 @@
throw new IllegalArgumentException("Invalid NetworkCallback");
}
try {
+ // CallbackHandler will release callback when receiving CALLBACK_RELEASED.
mService.releaseNetworkRequest(networkCallback.networkRequest);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
-
- synchronized (sNetworkCallback) {
- sNetworkCallback.remove(networkCallback.networkRequest);
- }
}
/**
@@ -3152,6 +3198,27 @@
}
/**
+ * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is
+ * only meaningful if the system is configured not to penalize such networks, e.g., if the
+ * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code
+ * NETWORK_AVOID_BAD_WIFI setting is unset}.
+ *
+ * <p>This method requires the caller to hold the permission
+ * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}
+ *
+ * @param network The network to accept.
+ *
+ * @hide
+ */
+ public void setAvoidUnvalidated(Network network) {
+ try {
+ mService.setAvoidUnvalidated(network);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Resets all connectivity manager settings back to factory defaults.
* @hide
*/
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 0d518cc1..4aabda9 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -156,9 +156,12 @@
void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
in PendingIntent operation);
+ void requestLinkProperties(in NetworkRequest networkRequest);
+ void requestNetworkCapabilities(in NetworkRequest networkRequest);
void releaseNetworkRequest(in NetworkRequest networkRequest);
void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
+ void setAvoidUnvalidated(in Network network);
int getRestoreDefaultNetworkDelay(int networkType);
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index 384ab1c..6e74f14 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -103,7 +103,7 @@
private boolean isIPv6ULA() {
if (address != null && address instanceof Inet6Address) {
byte[] bytes = address.getAddress();
- return ((bytes[0] & (byte)0xfc) == (byte)0xfc);
+ return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
}
return false;
}
diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java
index 5511a24..69f50a2 100644
--- a/core/java/android/net/NetworkMisc.java
+++ b/core/java/android/net/NetworkMisc.java
@@ -52,6 +52,15 @@
public boolean acceptUnvalidated;
/**
+ * Set to avoid surfacing the "Sign in to network" notification.
+ * if carrier receivers/apps are registered to handle the carrier-specific provisioning
+ * procedure, a carrier specific provisioning notification will be placed.
+ * only one notification should be displayed. This field is set based on
+ * which notification should be used for provisioning.
+ */
+ public boolean provisioningNotificationDisabled;
+
+ /**
* For mobile networks, this is the subscriber ID (such as IMSI).
*/
public String subscriberId;
@@ -65,6 +74,7 @@
explicitlySelected = nm.explicitlySelected;
acceptUnvalidated = nm.acceptUnvalidated;
subscriberId = nm.subscriberId;
+ provisioningNotificationDisabled = nm.provisioningNotificationDisabled;
}
}
@@ -79,6 +89,7 @@
out.writeInt(explicitlySelected ? 1 : 0);
out.writeInt(acceptUnvalidated ? 1 : 0);
out.writeString(subscriberId);
+ out.writeInt(provisioningNotificationDisabled ? 1 : 0);
}
public static final Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -89,6 +100,7 @@
networkMisc.explicitlySelected = in.readInt() != 0;
networkMisc.acceptUnvalidated = in.readInt() != 0;
networkMisc.subscriberId = in.readString();
+ networkMisc.provisioningNotificationDisabled = in.readInt() != 0;
return networkMisc;
}
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
index f1edcbe..4501f7b 100644
--- a/core/java/android/net/NetworkRequest.java
+++ b/core/java/android/net/NetworkRequest.java
@@ -19,6 +19,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.Objects;
+
/**
* Defines a request for a network, made through {@link NetworkRequest.Builder} and used
* to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes
@@ -47,15 +49,55 @@
public final int legacyType;
/**
+ * A NetworkRequest as used by the system can be one of three types:
+ *
+ * - LISTEN, for which the framework will issue callbacks about any
+ * and all networks that match the specified NetworkCapabilities,
+ *
+ * - REQUEST, capable of causing a specific network to be created
+ * first (e.g. a telephony DUN request), the framework will issue
+ * callbacks about the single, highest scoring current network
+ * (if any) that matches the specified NetworkCapabilities, or
+ *
+ * - TRACK_DEFAULT, a hybrid of the two designed such that the
+ * framework will issue callbacks for the single, highest scoring
+ * current network (if any) that matches the capabilities of the
+ * default Internet request (mDefaultRequest), but which cannot cause
+ * the framework to either create or retain the existence of any
+ * specific network.
+ *
+ * - The value NONE is used only by applications. When an application
+ * creates a NetworkRequest, it does not have a type; the type is set
+ * by the system depending on the method used to file the request
+ * (requestNetwork, registerNetworkCallback, etc.).
+ *
* @hide
*/
- public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId) {
+ public static enum Type {
+ NONE,
+ LISTEN,
+ TRACK_DEFAULT,
+ REQUEST
+ };
+
+ /**
+ * The type of the request. This is only used by the system and is always NONE elsewhere.
+ *
+ * @hide
+ */
+ public final Type type;
+
+ /**
+ * @hide
+ */
+ public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId, Type type) {
if (nc == null) {
throw new NullPointerException();
}
requestId = rId;
networkCapabilities = nc;
this.legacyType = legacyType;
+ this.type = type;
}
/**
@@ -65,6 +107,7 @@
networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
requestId = that.requestId;
this.legacyType = that.legacyType;
+ this.type = that.type;
}
/**
@@ -90,7 +133,7 @@
final NetworkCapabilities nc = new NetworkCapabilities(mNetworkCapabilities);
nc.maybeMarkCapabilitiesRestricted();
return new NetworkRequest(nc, ConnectivityManager.TYPE_NONE,
- ConnectivityManager.REQUEST_ID_UNSET);
+ ConnectivityManager.REQUEST_ID_UNSET, Type.NONE);
}
/**
@@ -223,6 +266,7 @@
dest.writeParcelable(networkCapabilities, flags);
dest.writeInt(legacyType);
dest.writeInt(requestId);
+ dest.writeString(type.name());
}
public static final Creator<NetworkRequest> CREATOR =
new Creator<NetworkRequest>() {
@@ -230,7 +274,8 @@
NetworkCapabilities nc = (NetworkCapabilities)in.readParcelable(null);
int legacyType = in.readInt();
int requestId = in.readInt();
- NetworkRequest result = new NetworkRequest(nc, legacyType, requestId);
+ Type type = Type.valueOf(in.readString()); // IllegalArgumentException if invalid.
+ NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
return result;
}
public NetworkRequest[] newArray(int size) {
@@ -238,8 +283,36 @@
}
};
+ /**
+ * Returns true iff. the contained NetworkRequest is of type LISTEN.
+ *
+ * @hide
+ */
+ public boolean isListen() {
+ return type == Type.LISTEN;
+ }
+
+ /**
+ * Returns true iff. the contained NetworkRequest is one that:
+ *
+ * - should be associated with at most one satisfying network
+ * at a time;
+ *
+ * - should cause a network to be kept up if it is the best network
+ * which can satisfy the NetworkRequest.
+ *
+ * For full detail of how isRequest() is used for pairing Networks with
+ * NetworkRequests read rematchNetworkAndRequests().
+ *
+ * @hide
+ */
+ public boolean isRequest() {
+ return type == Type.TRACK_DEFAULT || type == Type.REQUEST;
+ }
+
public String toString() {
- return "NetworkRequest [ id=" + requestId + ", legacyType=" + legacyType +
+ return "NetworkRequest [ " + type + " id=" + requestId +
+ (legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") +
", " + networkCapabilities.toString() + " ]";
}
@@ -248,13 +321,11 @@
NetworkRequest that = (NetworkRequest)obj;
return (that.legacyType == this.legacyType &&
that.requestId == this.requestId &&
- ((that.networkCapabilities == null && this.networkCapabilities == null) ||
- (that.networkCapabilities != null &&
- that.networkCapabilities.equals(this.networkCapabilities))));
+ that.type == this.type &&
+ Objects.equals(that.networkCapabilities, this.networkCapabilities));
}
public int hashCode() {
- return requestId + (legacyType * 1013) +
- (networkCapabilities.hashCode() * 1051);
+ return Objects.hash(requestId, legacyType, networkCapabilities, type);
}
}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 141af3d..35e3065 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -45,13 +45,20 @@
public native static void attachDhcpFilter(FileDescriptor fd) throws SocketException;
/**
- * Attaches a socket filter that accepts ICMP6 router advertisement packets to the given socket.
+ * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket.
* @param fd the socket's {@link FileDescriptor}.
* @param packetType the hardware address type, one of ARPHRD_*.
*/
public native static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException;
/**
+ * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
+ * @param fd the socket's {@link FileDescriptor}.
+ * @param ifIndex the interface index.
+ */
+ public native static void setupRaSocket(FileDescriptor fd, int ifIndex) throws SocketException;
+
+ /**
* Binds the current process to the network designated by {@code netId}. All sockets created
* in the future (and not explicitly bound via a bound {@link SocketFactory} (see
* {@link Network#getSocketFactory}) will be bound to this network. Note that if this
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 2364787..679e882 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -125,6 +125,99 @@
}
}
+static void android_net_utils_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd,
+ jint ifIndex)
+{
+ static const int kLinkLocalHopLimit = 255;
+
+ int fd = jniGetFDFromFileDescriptor(env, javaFd);
+
+ // Set an ICMPv6 filter that only passes Router Solicitations.
+ struct icmp6_filter rs_only;
+ ICMP6_FILTER_SETBLOCKALL(&rs_only);
+ ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &rs_only);
+ socklen_t len = sizeof(rs_only);
+ if (setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &rs_only, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(ICMP6_FILTER): %s", strerror(errno));
+ return;
+ }
+
+ // Most/all of the rest of these options can be set via Java code, but
+ // because we're here on account of setting an icmp6_filter go ahead
+ // and do it all natively for now.
+ //
+ // TODO: Consider moving these out to Java.
+
+ // Set the multicast hoplimit to 255 (link-local only).
+ int hops = kLinkLocalHopLimit;
+ len = sizeof(hops);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_HOPS): %s", strerror(errno));
+ return;
+ }
+
+ // Set the unicast hoplimit to 255 (link-local only).
+ hops = kLinkLocalHopLimit;
+ len = sizeof(hops);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_UNICAST_HOPS): %s", strerror(errno));
+ return;
+ }
+
+ // Explicitly disable multicast loopback.
+ int off = 0;
+ len = sizeof(off);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &off, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_LOOP): %s", strerror(errno));
+ return;
+ }
+
+ // Specify the IPv6 interface to use for outbound multicast.
+ len = sizeof(ifIndex);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifIndex, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_MULTICAST_IF): %s", strerror(errno));
+ return;
+ }
+
+ // Additional options to be considered:
+ // - IPV6_TCLASS
+ // - IPV6_RECVPKTINFO
+ // - IPV6_RECVHOPLIMIT
+
+ // Bind to [::].
+ const struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ .sin6_port = 0,
+ .sin6_flowinfo = 0,
+ .sin6_addr = IN6ADDR_ANY_INIT,
+ .sin6_scope_id = 0,
+ };
+ auto sa = reinterpret_cast<const struct sockaddr *>(&sin6);
+ len = sizeof(sin6);
+ if (bind(fd, sa, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "bind(IN6ADDR_ANY): %s", strerror(errno));
+ return;
+ }
+
+ // Join the all-routers multicast group, ff02::2%index.
+ struct ipv6_mreq all_rtrs = {
+ .ipv6mr_multiaddr = {{{0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2}}},
+ .ipv6mr_interface = ifIndex,
+ };
+ len = sizeof(all_rtrs);
+ if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &all_rtrs, len) != 0) {
+ jniThrowExceptionFmt(env, "java/net/SocketException",
+ "setsockopt(IPV6_JOIN_GROUP): %s", strerror(errno));
+ return;
+ }
+}
+
static jboolean android_net_utils_bindProcessToNetwork(JNIEnv *env, jobject thiz, jint netId)
{
return (jboolean) !setNetworkForProcess(netId);
@@ -173,6 +266,7 @@
{ "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess },
{ "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter },
{ "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
+ { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
};
int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 534b544..989892f 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -38,7 +38,6 @@
import android.annotation.Nullable;
import android.app.BroadcastOptions;
-import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -76,7 +75,9 @@
import android.net.UidRange;
import android.net.Uri;
import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.IpConnectivityLog;
import android.net.metrics.NetworkEvent;
+import android.net.util.AvoidBadWifiTracker;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -93,6 +94,7 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -124,14 +126,18 @@
import com.android.internal.util.AsyncChannel;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.MessageUtils;
+import com.android.internal.util.WakeupMessage;
import com.android.internal.util.XmlUtils;
import com.android.server.am.BatteryStatsService;
import com.android.server.connectivity.DataConnectionStats;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics;
import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import com.android.server.connectivity.PacManager;
import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.Tethering;
@@ -170,7 +176,7 @@
*/
public class ConnectivityService extends IConnectivityManager.Stub
implements PendingIntent.OnFinished {
- private static final String TAG = "ConnectivityService";
+ private static final String TAG = ConnectivityService.class.getSimpleName();
private static final boolean DBG = true;
private static final boolean VDBG = false;
@@ -190,6 +196,12 @@
// connect anyway?" dialog after the user selects a network that doesn't validate.
private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
+ // Default to 30s linger time-out. Modifiable only for testing.
+ private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
+ private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
+ @VisibleForTesting
+ protected int mLingerDelayMs; // Can't be final, or test subclass constructors can't change it.
+
// How long to delay to removal of a pending intent based request.
// See Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
private final int mReleasePendingIntentDelayMs;
@@ -238,7 +250,8 @@
private static final int DISABLED = 0;
private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames(
- new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class });
+ new Class[] { AsyncChannel.class, ConnectivityService.class, NetworkAgent.class,
+ NetworkAgentInfo.class });
private enum ReapUnvalidatedNetworks {
// Tear down networks that have no chance (e.g. even if validated) of becoming
@@ -293,7 +306,7 @@
/**
* indicates a timeout period is over - check if we had a network yet or not
- * and if not, call the timeout calback (but leave the request live until they
+ * and if not, call the timeout callback (but leave the request live until they
* cancel it.
* includes a NetworkRequestInfo
*/
@@ -354,6 +367,11 @@
private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28;
/**
+ * used to specify whether a network should not be penalized when it becomes unvalidated.
+ */
+ private static final int EVENT_SET_AVOID_UNVALIDATED = 35;
+
+ /**
* used to ask the user to confirm a connection to an unvalidated network.
* obj = network
*/
@@ -370,6 +388,16 @@
*/
private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31;
+ /**
+ * Indicates a caller has requested to have its callback invoked with
+ * the latest LinkProperties or NetworkCapabilities.
+ *
+ * arg1 = UID of caller
+ * obj = NetworkRequest
+ */
+ private static final int EVENT_REQUEST_LINKPROPERTIES = 32;
+ private static final int EVENT_REQUEST_NETCAPABILITIES = 33;
+
/** Handler thread used for both of the handlers below. */
@VisibleForTesting
protected final HandlerThread mHandlerThread;
@@ -416,6 +444,8 @@
TelephonyManager mTelephonyManager;
private KeepaliveTracker mKeepaliveTracker;
+ private NetworkNotificationManager mNotifier;
+ private LingerMonitor mLingerMonitor;
// sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
private final static int MIN_NET_ID = 100; // some reserved marks
@@ -429,6 +459,11 @@
private static final int MAX_NETWORK_REQUEST_LOGS = 20;
private final LocalLog mNetworkRequestInfoLogs = new LocalLog(MAX_NETWORK_REQUEST_LOGS);
+ // NetworkInfo blocked and unblocked String log entries
+ // TODO: consider reducing memory usage. Each log line is ~40 2B chars, for a total of ~8kB.
+ private static final int MAX_NETWORK_INFO_LOGS = 100;
+ private final LocalLog mNetworkInfoBlockingLogs = new LocalLog(MAX_NETWORK_INFO_LOGS);
+
// Array of <Network,ReadOnlyLocalLogs> tracking network validation and results
private static final int MAX_VALIDATION_LOGS = 10;
private static class ValidationLog {
@@ -454,6 +489,11 @@
}
}
+ private final IpConnectivityLog mMetricsLog;
+
+ @VisibleForTesting
+ final AvoidBadWifiTracker mAvoidBadWifiTracker;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -487,8 +527,16 @@
*
* The actual lists are populated when we scan the network types that
* are supported on this device.
+ *
+ * Threading model:
+ * - addSupportedType() is only called in the constructor
+ * - add(), update(), remove() are only called from the ConnectivityService handler thread.
+ * They are therefore not thread-safe with respect to each other.
+ * - getNetworkForType() can be called at any time on binder threads. It is synchronized
+ * on mTypeLists to be thread-safe with respect to a concurrent remove call.
+ * - dump is thread-safe with respect to concurrent add and remove calls.
*/
- private ArrayList<NetworkAgentInfo> mTypeLists[];
+ private final ArrayList<NetworkAgentInfo> mTypeLists[];
public LegacyTypeTracker() {
mTypeLists = (ArrayList<NetworkAgentInfo>[])
@@ -508,11 +556,12 @@
}
public NetworkAgentInfo getNetworkForType(int type) {
- if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
- return mTypeLists[type].get(0);
- } else {
- return null;
+ synchronized (mTypeLists) {
+ if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
+ return mTypeLists[type].get(0);
+ }
}
+ return null;
}
private void maybeLogBroadcast(NetworkAgentInfo nai, DetailedState state, int type,
@@ -535,12 +584,13 @@
if (list.contains(nai)) {
return;
}
-
- list.add(nai);
+ synchronized (mTypeLists) {
+ list.add(nai);
+ }
// Send a broadcast if this is the first network of its type or if it's the default.
final boolean isDefaultNetwork = isDefaultNetwork(nai);
- if (list.size() == 1 || isDefaultNetwork) {
+ if ((list.size() == 1) || isDefaultNetwork) {
maybeLogBroadcast(nai, DetailedState.CONNECTED, type, isDefaultNetwork);
sendLegacyNetworkBroadcast(nai, DetailedState.CONNECTED, type);
}
@@ -552,11 +602,12 @@
if (list == null || list.isEmpty()) {
return;
}
-
final boolean wasFirstNetwork = list.get(0).equals(nai);
- if (!list.remove(nai)) {
- return;
+ synchronized (mTypeLists) {
+ if (!list.remove(nai)) {
+ return;
+ }
}
final DetailedState state = DetailedState.DISCONNECTED;
@@ -591,8 +642,8 @@
for (int type = 0; type < mTypeLists.length; type++) {
final ArrayList<NetworkAgentInfo> list = mTypeLists[type];
final boolean contains = (list != null && list.contains(nai));
- final boolean isFirst = (list != null && list.size() > 0 && nai == list.get(0));
- if (isFirst || (contains && isDefault)) {
+ final boolean isFirst = contains && (nai == list.get(0));
+ if (isFirst || contains && isDefault) {
maybeLogBroadcast(nai, state, type, isDefault);
sendLegacyNetworkBroadcast(nai, state, type);
}
@@ -617,10 +668,12 @@
pw.println();
pw.println("Current state:");
pw.increaseIndent();
- for (int type = 0; type < mTypeLists.length; type++) {
- if (mTypeLists[type] == null|| mTypeLists[type].size() == 0) continue;
- for (NetworkAgentInfo nai : mTypeLists[type]) {
- pw.println(type + " " + naiToString(nai));
+ synchronized (mTypeLists) {
+ for (int type = 0; type < mTypeLists.length; type++) {
+ if (mTypeLists[type] == null || mTypeLists[type].isEmpty()) continue;
+ for (NetworkAgentInfo nai : mTypeLists[type]) {
+ pw.println(type + " " + naiToString(nai));
+ }
}
}
pw.decreaseIndent();
@@ -637,11 +690,18 @@
public ConnectivityService(Context context, INetworkManagementService netManager,
INetworkStatsService statsService, INetworkPolicyManager policyManager) {
+ this(context, netManager, statsService, policyManager, new IpConnectivityLog());
+ }
+
+ @VisibleForTesting
+ protected ConnectivityService(Context context, INetworkManagementService netManager,
+ INetworkStatsService statsService, INetworkPolicyManager policyManager,
+ IpConnectivityLog logger) {
if (DBG) log("ConnectivityService starting up");
+ mMetricsLog = logger;
mDefaultRequest = createInternetRequestForTransport(-1);
- NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest,
- new Binder(), NetworkRequestType.REQUEST);
+ NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest, new Binder());
mNetworkRequests.put(mDefaultRequest, defaultNRI);
mNetworkRequestInfoLogs.log("REGISTER " + defaultNRI);
@@ -666,6 +726,8 @@
mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
+ mLingerDelayMs = SystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+
mContext = checkNotNull(context, "missing Context");
mNetd = checkNotNull(netManager, "missing INetworkManagementService");
mStatsService = checkNotNull(statsService, "missing INetworkStatsService");
@@ -783,6 +845,19 @@
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mKeepaliveTracker = new KeepaliveTracker(mHandler);
+ mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager,
+ mContext.getSystemService(NotificationManager.class));
+
+ final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT,
+ LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT);
+ final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(),
+ Settings.Global.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS,
+ LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS);
+ mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit);
+
+ mAvoidBadWifiTracker = createAvoidBadWifiTracker(
+ mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
}
private NetworkRequest createInternetRequestForTransport(int transportType) {
@@ -792,7 +867,19 @@
if (transportType > -1) {
netCap.addTransportType(transportType);
}
- return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId());
+ return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(),
+ NetworkRequest.Type.REQUEST);
+ }
+
+ // Used only for testing.
+ // TODO: Delete this and either:
+ // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires
+ // changing ContentResolver to make registerContentObserver non-final).
+ // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
+ // by subclassing SettingsObserver.
+ @VisibleForTesting
+ void updateMobileDataAlwaysOn() {
+ mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
}
private void handleMobileDataAlwaysOn() {
@@ -805,7 +892,7 @@
if (enable) {
handleRegisterNetworkRequest(new NetworkRequestInfo(
- null, mDefaultMobileDataRequest, new Binder(), NetworkRequestType.REQUEST));
+ null, mDefaultMobileDataRequest, new Binder()));
} else {
handleReleaseNetworkRequest(mDefaultMobileDataRequest, Process.SYSTEM_UID);
}
@@ -965,7 +1052,9 @@
}
private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) {
- if (ni == null || !LOGD_BLOCKED_NETWORKINFO) return;
+ if (ni == null || !LOGD_BLOCKED_NETWORKINFO) {
+ return;
+ }
boolean removed = false;
boolean added = false;
synchronized (mBlockedAppUids) {
@@ -975,8 +1064,13 @@
removed = true;
}
}
- if (added) log("Returning blocked NetworkInfo to uid=" + uid);
- else if (removed) log("Returning unblocked NetworkInfo to uid=" + uid);
+ if (added) {
+ log("Returning blocked NetworkInfo to uid=" + uid);
+ mNetworkInfoBlockingLogs.log("BLOCKED " + uid);
+ } else if (removed) {
+ log("Returning unblocked NetworkInfo to uid=" + uid);
+ mNetworkInfoBlockingLogs.log("UNBLOCKED " + uid);
+ }
}
/**
@@ -1056,6 +1150,7 @@
return nai != null ? nai.network : null;
}
+ // Public because it's used by mLockdownTracker.
public NetworkInfo getActiveNetworkInfoUnfiltered() {
enforceAccessPermission();
final int uid = Binder.getCallingUid();
@@ -1311,6 +1406,7 @@
* desired
* @return {@code true} on success, {@code false} on failure
*/
+ @Override
public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress) {
enforceChangePermission();
if (mProtectedNetworks.contains(networkType)) {
@@ -1515,10 +1611,21 @@
"ConnectivityService");
}
+ private void enforceConnectivityRestrictedNetworksPermission() {
+ try {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
+ "ConnectivityService");
+ return;
+ } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
+ enforceConnectivityInternalPermission();
+ }
+
private void enforceKeepalivePermission() {
mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
}
+ // Public because it's used by mLockdownTracker.
public void sendConnectedBroadcast(NetworkInfo info) {
enforceConnectivityInternalPermission();
sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
@@ -1875,15 +1982,16 @@
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
pw.println(nai.toString());
pw.increaseIndent();
- pw.println("Requests:");
+ pw.println(String.format("Requests: %d request/%d total",
+ nai.numRequestNetworkRequests(), nai.numNetworkRequests()));
pw.increaseIndent();
- for (int i = 0; i < nai.networkRequests.size(); i++) {
- pw.println(nai.networkRequests.valueAt(i).toString());
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ pw.println(nai.requestAt(i).toString());
}
pw.decreaseIndent();
pw.println("Lingered:");
pw.increaseIndent();
- for (NetworkRequest nr : nai.networkLingered) pw.println(nr.toString());
+ nai.dumpLingerTimers(pw);
pw.decreaseIndent();
pw.decreaseIndent();
}
@@ -1944,7 +2052,9 @@
mKeepaliveTracker.dump(pw);
pw.println();
+ dumpAvoidBadWifiSettings(pw);
+ pw.println();
if (mInetLog != null && mInetLog.size() > 0) {
pw.println();
pw.println("Inet condition reports:");
@@ -1972,6 +2082,12 @@
pw.increaseIndent();
mNetworkRequestInfoLogs.reverseDump(fd, pw, args);
pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mNetworkInfoBlockingLogs (most recent first):");
+ pw.increaseIndent();
+ mNetworkInfoBlockingLogs.reverseDump(fd, pw, args);
+ pw.decreaseIndent();
}
}
@@ -1987,10 +2103,6 @@
return false;
}
- private boolean isRequest(NetworkRequest request) {
- return mNetworkRequests.get(request).isRequest();
- }
-
// must be stateless - things change under us.
private class NetworkStateTrackerHandler extends Handler {
public NetworkStateTrackerHandler(Looper looper) {
@@ -2040,7 +2152,7 @@
Slog.wtf(TAG, "BUG: " + nai + " changed immutable capabilities: "
+ nai.networkCapabilities + " -> " + networkCapabilities);
}
- updateCapabilities(nai, networkCapabilities);
+ updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
break;
}
case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: {
@@ -2111,13 +2223,14 @@
if (nai != null) {
final boolean valid =
(msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
+ final boolean wasValidated = nai.lastValidated;
if (DBG) log(nai.name() + " validation " + (valid ? "passed" : "failed") +
(msg.obj == null ? "" : " with redirect to " + (String)msg.obj));
if (valid != nai.lastValidated) {
final int oldScore = nai.getCurrentScore();
nai.lastValidated = valid;
nai.everValidated |= valid;
- updateCapabilities(nai, nai.networkCapabilities);
+ updateCapabilities(oldScore, nai, nai.networkCapabilities);
// If score has changed, rebroadcast to NetworkFactories. b/17726566
if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
}
@@ -2129,13 +2242,9 @@
NetworkAgent.CMD_REPORT_NETWORK_STATUS,
(valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK),
0, redirectUrlBundle);
- }
- break;
- }
- case NetworkMonitor.EVENT_NETWORK_LINGER_COMPLETE: {
- NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
- if (isLiveNetworkAgent(nai, msg.what)) {
- handleLingerComplete(nai);
+ if (wasValidated && !nai.lastValidated) {
+ handleNetworkUnvalidated(nai);
+ }
}
break;
}
@@ -2148,20 +2257,37 @@
}
// If captive portal status has changed, update capabilities.
if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
+ final int oldScore = nai.getCurrentScore();
nai.lastCaptivePortalDetected = visible;
nai.everCaptivePortalDetected |= visible;
- updateCapabilities(nai, nai.networkCapabilities);
+ updateCapabilities(oldScore, nai, nai.networkCapabilities);
}
if (!visible) {
- setProvNotificationVisibleIntent(false, netId, null, 0, null, null, false);
+ mNotifier.clearNotification(netId);
} else {
if (nai == null) {
loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
break;
}
- setProvNotificationVisibleIntent(true, netId, NotificationType.SIGN_IN,
- nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(),
- (PendingIntent)msg.obj, nai.networkMisc.explicitlySelected);
+ if (!nai.networkMisc.provisioningNotificationDisabled) {
+ mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null,
+ (PendingIntent) msg.obj, nai.networkMisc.explicitlySelected);
+ }
+ }
+ break;
+ }
+ }
+ return true;
+ }
+
+ private boolean maybeHandleNetworkAgentInfoMessage(Message msg) {
+ switch (msg.what) {
+ default:
+ return false;
+ case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: {
+ NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+ if (nai != null && isLiveNetworkAgent(nai, msg.what)) {
+ handleLingerComplete(nai);
}
break;
}
@@ -2171,31 +2297,33 @@
@Override
public void handleMessage(Message msg) {
- if (!maybeHandleAsyncChannelMessage(msg) && !maybeHandleNetworkMonitorMessage(msg)) {
+ if (!maybeHandleAsyncChannelMessage(msg) &&
+ !maybeHandleNetworkMonitorMessage(msg) &&
+ !maybeHandleNetworkAgentInfoMessage(msg)) {
maybeHandleNetworkAgentMessage(msg);
}
}
}
- private void linger(NetworkAgentInfo nai) {
- nai.lingering = true;
- NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_LINGER);
- nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_LINGER);
- notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING);
- }
-
- // Cancel any lingering so the linger timeout doesn't teardown a network.
- // This should be called when a network begins satisfying a NetworkRequest.
- // Note: depending on what state the NetworkMonitor is in (e.g.,
- // if it's awaiting captive portal login, or if validation failed), this
- // may trigger a re-evaluation of the network.
- private void unlinger(NetworkAgentInfo nai) {
- nai.networkLingered.clear();
- if (!nai.lingering) return;
- nai.lingering = false;
- NetworkEvent.logEvent(nai.network.netId, NetworkEvent.NETWORK_UNLINGER);
- if (VDBG) log("Canceling linger of " + nai.name());
- nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+ private void updateLingerState(NetworkAgentInfo nai, long now) {
+ // 1. Update the linger timer. If it's changed, reschedule or cancel the alarm.
+ // 2. If the network was lingering and there are now requests, unlinger it.
+ // 3. If this network is unneeded (which implies it is not lingering), and there is at least
+ // one lingered request, start lingering.
+ nai.updateLingerTimer();
+ if (nai.isLingering() && nai.numRequestNetworkRequests() > 0) {
+ if (DBG) log("Unlingering " + nai.name());
+ nai.unlinger();
+ logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
+ } else if (unneeded(nai) && nai.getLingerExpiry() > 0) { // unneeded() calls isLingering()
+ int lingerTime = (int) (nai.getLingerExpiry() - now);
+ if (DBG) {
+ Log.d(TAG, "Lingering " + nai.name() + " for " + lingerTime + "ms");
+ }
+ nai.linger();
+ logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+ }
}
private void handleAsyncChannelHalfConnect(Message msg) {
@@ -2205,7 +2333,7 @@
if (VDBG) log("NetworkFactory connected");
// A network factory has connected. Send it all current NetworkRequests.
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
- if (!nri.isRequest()) continue;
+ if (nri.request.isListen()) continue;
NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,
(nai != null ? nai.getCurrentScore() : 0), 0, nri.request);
@@ -2240,7 +2368,7 @@
NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
if (nai != null) {
if (DBG) {
- log(nai.name() + " got DISCONNECTED, was satisfying " + nai.networkRequests.size());
+ log(nai.name() + " got DISCONNECTED, was satisfying " + nai.numNetworkRequests());
}
// A network agent has disconnected.
// TODO - if we move the logic to the network agent (have them disconnect
@@ -2277,21 +2405,23 @@
mNetworkForNetId.remove(nai.network.netId);
}
// Remove all previously satisfied requests.
- for (int i = 0; i < nai.networkRequests.size(); i++) {
- NetworkRequest request = nai.networkRequests.valueAt(i);
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest request = nai.requestAt(i);
NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(request.requestId);
if (currentNetwork != null && currentNetwork.network.netId == nai.network.netId) {
mNetworkForRequestId.remove(request.requestId);
sendUpdatedScoreToFactories(request, 0);
}
}
- if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+ nai.clearLingerState();
+ if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
removeDataActivityTracking(nai);
notifyLockdownVpn(nai);
requestNetworkTransitionWakelock(nai.name());
}
mLegacyTypeTracker.remove(nai, wasDefault);
rematchAllNetworksAndRequests(null, 0);
+ mLingerMonitor.noteDisconnect(nai);
if (nai.created) {
// Tell netd to clean up the configuration for this network
// (routing rules, DNS, etc).
@@ -2346,7 +2476,7 @@
private void handleRegisterNetworkRequest(NetworkRequestInfo nri) {
mNetworkRequests.put(nri.request, nri);
mNetworkRequestInfoLogs.log("REGISTER " + nri);
- if (!nri.isRequest()) {
+ if (nri.request.isListen()) {
for (NetworkAgentInfo network : mNetworkAgentInfos.values()) {
if (nri.request.networkCapabilities.hasSignalStrength() &&
network.satisfiesImmutableCapabilitiesOf(nri.request)) {
@@ -2355,7 +2485,7 @@
}
}
rematchAllNetworksAndRequests(null, 0);
- if (nri.isRequest() && mNetworkForRequestId.get(nri.request.requestId) == null) {
+ if (nri.request.isRequest() && mNetworkForRequestId.get(nri.request.requestId) == null) {
sendUpdatedScoreToFactories(nri.request, 0);
}
}
@@ -2372,12 +2502,15 @@
// This is whether it is satisfying any NetworkRequests or were it to become validated,
// would it have a chance of satisfying any NetworkRequests.
private boolean unneeded(NetworkAgentInfo nai) {
- if (!nai.everConnected || nai.isVPN() || nai.lingering) return false;
+ if (!nai.everConnected || nai.isVPN() ||
+ nai.isLingering() || nai.numRequestNetworkRequests() > 0) {
+ return false;
+ }
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
// If this Network is already the highest scoring Network for a request, or if
// there is hope for it to become one if it validated, then it is needed.
- if (nri.isRequest() && nai.satisfies(nri.request) &&
- (nai.networkRequests.get(nri.request.requestId) != null ||
+ if (nri.request.isRequest() && nai.satisfies(nri.request) &&
+ (nai.isSatisfyingRequest(nri.request.requestId) ||
// Note that this catches two important cases:
// 1. Unvalidated cellular will not be reaped when unvalidated WiFi
// is currently satisfying the request. This is desirable when
@@ -2393,108 +2526,161 @@
return true;
}
- private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
- NetworkRequestInfo nri = mNetworkRequests.get(request);
+ private NetworkRequestInfo getNriForAppRequest(
+ NetworkRequest request, int callingUid, String requestedOperation) {
+ final NetworkRequestInfo nri = mNetworkRequests.get(request);
+
if (nri != null) {
if (Process.SYSTEM_UID != callingUid && nri.mUid != callingUid) {
- if (DBG) log("Attempt to release unowned NetworkRequest " + request);
- return;
+ log(String.format("UID %d attempted to %s for unowned request %s",
+ callingUid, requestedOperation, nri));
+ return null;
}
- if (VDBG || (DBG && nri.isRequest())) log("releasing NetworkRequest " + request);
- nri.unlinkDeathRecipient();
- mNetworkRequests.remove(request);
- synchronized (mUidToNetworkRequestCount) {
- int requests = mUidToNetworkRequestCount.get(nri.mUid, 0);
- if (requests < 1) {
- Slog.wtf(TAG, "BUG: too small request count " + requests + " for UID " +
- nri.mUid);
- } else if (requests == 1) {
- mUidToNetworkRequestCount.removeAt(
- mUidToNetworkRequestCount.indexOfKey(nri.mUid));
- } else {
- mUidToNetworkRequestCount.put(nri.mUid, requests - 1);
- }
- }
- mNetworkRequestInfoLogs.log("RELEASE " + nri);
- if (nri.isRequest()) {
- // Find all networks that are satisfying this request and remove the request
- // from their request lists.
- // TODO - it's my understanding that for a request there is only a single
- // network satisfying it, so this loop is wasteful
- boolean wasKept = false;
- for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
- if (nai.networkRequests.get(nri.request.requestId) != null) {
- nai.networkRequests.remove(nri.request.requestId);
- if (VDBG) {
- log(" Removing from current network " + nai.name() +
- ", leaving " + nai.networkRequests.size() +
- " requests.");
- }
- if (unneeded(nai)) {
- if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
- teardownUnneededNetwork(nai);
- } else {
- // suspect there should only be one pass through here
- // but if any were kept do the check below
- wasKept |= true;
- }
- }
- }
-
- NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
- if (nai != null) {
- mNetworkForRequestId.remove(nri.request.requestId);
- }
- // Maintain the illusion. When this request arrived, we might have pretended
- // that a network connected to serve it, even though the network was already
- // connected. Now that this request has gone away, we might have to pretend
- // that the network disconnected. LegacyTypeTracker will generate that
- // phantom disconnect for this type.
- if (nri.request.legacyType != TYPE_NONE && nai != null) {
- boolean doRemove = true;
- if (wasKept) {
- // check if any of the remaining requests for this network are for the
- // same legacy type - if so, don't remove the nai
- for (int i = 0; i < nai.networkRequests.size(); i++) {
- NetworkRequest otherRequest = nai.networkRequests.valueAt(i);
- if (otherRequest.legacyType == nri.request.legacyType &&
- isRequest(otherRequest)) {
- if (DBG) log(" still have other legacy request - leaving");
- doRemove = false;
- }
- }
- }
-
- if (doRemove) {
- mLegacyTypeTracker.remove(nri.request.legacyType, nai, false);
- }
- }
-
- for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
- nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_CANCEL_REQUEST,
- nri.request);
- }
- } else {
- // listens don't have a singular affectedNetwork. Check all networks to see
- // if this listen request applies and remove it.
- for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
- nai.networkRequests.remove(nri.request.requestId);
- if (nri.request.networkCapabilities.hasSignalStrength() &&
- nai.satisfiesImmutableCapabilitiesOf(nri.request)) {
- updateSignalStrengthThresholds(nai, "RELEASE", nri.request);
- }
- }
- }
- callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED);
}
+
+ return nri;
}
+ private void handleRequestCallbackUpdate(NetworkRequest request, int callingUid,
+ String description, int callbackType) {
+ final NetworkRequestInfo nri = getNriForAppRequest(request, callingUid, description);
+ if (nri == null) return;
+
+ final NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
+ // The network that is satisfying this request may have changed since
+ // the application requested the update.
+ //
+ // - If the request is no longer satisfied, don't send any updates.
+ // - If the request is satisfied by a different network, it is the
+ // caller's responsibility to check that the Network object in the
+ // callback matches the network that was returned in the last
+ // onAvailable() callback for this request.
+ if (nai == null) return;
+ callCallbackForRequest(nri, nai, callbackType, 0);
+ }
+
+ private void handleRequestLinkProperties(NetworkRequest request, int callingUid) {
+ handleRequestCallbackUpdate(request, callingUid,
+ "request LinkProperties", ConnectivityManager.CALLBACK_IP_CHANGED);
+ }
+
+ private void handleRequestNetworkCapabilities(NetworkRequest request, int callingUid) {
+ handleRequestCallbackUpdate(request, callingUid,
+ "request NetworkCapabilities", ConnectivityManager.CALLBACK_CAP_CHANGED);
+ }
+
+ private void handleReleaseNetworkRequest(NetworkRequest request, int callingUid) {
+ final NetworkRequestInfo nri = getNriForAppRequest(
+ request, callingUid, "release NetworkRequest");
+ if (nri == null) return;
+
+ if (VDBG || (DBG && nri.request.isRequest())) log("releasing " + request);
+ nri.unlinkDeathRecipient();
+ mNetworkRequests.remove(request);
+ synchronized (mUidToNetworkRequestCount) {
+ int requests = mUidToNetworkRequestCount.get(nri.mUid, 0);
+ if (requests < 1) {
+ Slog.wtf(TAG, "BUG: too small request count " + requests + " for UID " +
+ nri.mUid);
+ } else if (requests == 1) {
+ mUidToNetworkRequestCount.removeAt(
+ mUidToNetworkRequestCount.indexOfKey(nri.mUid));
+ } else {
+ mUidToNetworkRequestCount.put(nri.mUid, requests - 1);
+ }
+ }
+ mNetworkRequestInfoLogs.log("RELEASE " + nri);
+ if (nri.request.isRequest()) {
+ boolean wasKept = false;
+ NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
+ if (nai != null) {
+ nai.removeRequest(nri.request.requestId);
+ if (VDBG) {
+ log(" Removing from current network " + nai.name() +
+ ", leaving " + nai.numNetworkRequests() + " requests.");
+ }
+ // If there are still lingered requests on this network, don't tear it down,
+ // but resume lingering instead.
+ updateLingerState(nai, SystemClock.elapsedRealtime());
+ if (unneeded(nai)) {
+ if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
+ teardownUnneededNetwork(nai);
+ } else {
+ wasKept = true;
+ }
+ mNetworkForRequestId.remove(nri.request.requestId);
+ }
+
+ // TODO: remove this code once we know that the Slog.wtf is never hit.
+ //
+ // Find all networks that are satisfying this request and remove the request
+ // from their request lists.
+ // TODO - it's my understanding that for a request there is only a single
+ // network satisfying it, so this loop is wasteful
+ for (NetworkAgentInfo otherNai : mNetworkAgentInfos.values()) {
+ if (otherNai.isSatisfyingRequest(nri.request.requestId) && otherNai != nai) {
+ Slog.wtf(TAG, "Request " + nri.request + " satisfied by " +
+ otherNai.name() + ", but mNetworkAgentInfos says " +
+ (nai != null ? nai.name() : "null"));
+ }
+ }
+
+ // Maintain the illusion. When this request arrived, we might have pretended
+ // that a network connected to serve it, even though the network was already
+ // connected. Now that this request has gone away, we might have to pretend
+ // that the network disconnected. LegacyTypeTracker will generate that
+ // phantom disconnect for this type.
+ if (nri.request.legacyType != TYPE_NONE && nai != null) {
+ boolean doRemove = true;
+ if (wasKept) {
+ // check if any of the remaining requests for this network are for the
+ // same legacy type - if so, don't remove the nai
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest otherRequest = nai.requestAt(i);
+ if (otherRequest.legacyType == nri.request.legacyType &&
+ otherRequest.isRequest()) {
+ if (DBG) log(" still have other legacy request - leaving");
+ doRemove = false;
+ }
+ }
+ }
+
+ if (doRemove) {
+ mLegacyTypeTracker.remove(nri.request.legacyType, nai, false);
+ }
+ }
+
+ for (NetworkFactoryInfo nfi : mNetworkFactoryInfos.values()) {
+ nfi.asyncChannel.sendMessage(android.net.NetworkFactory.CMD_CANCEL_REQUEST,
+ nri.request);
+ }
+ } else {
+ // listens don't have a singular affectedNetwork. Check all networks to see
+ // if this listen request applies and remove it.
+ for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+ nai.removeRequest(nri.request.requestId);
+ if (nri.request.networkCapabilities.hasSignalStrength() &&
+ nai.satisfiesImmutableCapabilitiesOf(nri.request)) {
+ updateSignalStrengthThresholds(nai, "RELEASE", nri.request);
+ }
+ }
+ }
+ callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_RELEASED, 0);
+ }
+
+ @Override
public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
enforceConnectivityInternalPermission();
mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED,
accept ? 1 : 0, always ? 1: 0, network));
}
+ @Override
+ public void setAvoidUnvalidated(Network network) {
+ enforceConnectivityInternalPermission();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network));
+ }
+
private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
if (DBG) log("handleSetAcceptUnvalidated network=" + network +
" accept=" + accept + " always=" + always);
@@ -2535,6 +2721,20 @@
}
+ private void handleSetAvoidUnvalidated(Network network) {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null || nai.lastValidated) {
+ // Nothing to do. The network either disconnected or revalidated.
+ return;
+ }
+ if (!nai.avoidUnvalidated) {
+ int oldScore = nai.getCurrentScore();
+ nai.avoidUnvalidated = true;
+ rematchAllNetworksAndRequests(nai, oldScore);
+ sendUpdatedScoreToFactories(nai);
+ }
+ }
+
private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
mHandler.sendMessageDelayed(
@@ -2542,6 +2742,81 @@
PROMPT_UNVALIDATED_DELAY_MS);
}
+ public boolean avoidBadWifi() {
+ return mAvoidBadWifiTracker.currentValue();
+ }
+
+ private void rematchForAvoidBadWifiUpdate() {
+ rematchAllNetworksAndRequests(null, 0);
+ for (NetworkAgentInfo nai: mNetworkAgentInfos.values()) {
+ if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ sendUpdatedScoreToFactories(nai);
+ }
+ }
+ }
+
+ // TODO: Evaluate whether this is of interest to other consumers of
+ // AvoidBadWifiTracker and worth moving out of here.
+ private void dumpAvoidBadWifiSettings(IndentingPrintWriter pw) {
+ final boolean configRestrict = mAvoidBadWifiTracker.configRestrictsAvoidBadWifi();
+ if (!configRestrict) {
+ pw.println("Bad Wi-Fi avoidance: unrestricted");
+ return;
+ }
+
+ pw.println("Bad Wi-Fi avoidance: " + avoidBadWifi());
+ pw.increaseIndent();
+ pw.println("Config restrict: " + configRestrict);
+
+ final String value = mAvoidBadWifiTracker.getSettingsValue();
+ String description;
+ // Can't use a switch statement because strings are legal case labels, but null is not.
+ if ("0".equals(value)) {
+ description = "get stuck";
+ } else if (value == null) {
+ description = "prompt";
+ } else if ("1".equals(value)) {
+ description = "avoid";
+ } else {
+ description = value + " (?)";
+ }
+ pw.println("User setting: " + description);
+ pw.println("Network overrides:");
+ pw.increaseIndent();
+ for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+ if (nai.avoidUnvalidated) {
+ pw.println(nai.name());
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+
+ private void showValidationNotification(NetworkAgentInfo nai, NotificationType type) {
+ final String action;
+ switch (type) {
+ case NO_INTERNET:
+ action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
+ break;
+ case LOST_INTERNET:
+ action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION;
+ break;
+ default:
+ Slog.wtf(TAG, "Unknown notification type " + type);
+ return;
+ }
+
+ Intent intent = new Intent(action);
+ intent.setData(Uri.fromParts("netId", Integer.toString(nai.network.netId), null));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName("com.android.settings",
+ "com.android.settings.wifi.WifiNoInternetDialog");
+
+ PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
+ mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+ mNotifier.showNotification(nai.network.netId, type, nai, null, pendingIntent, true);
+ }
+
private void handlePromptUnvalidated(Network network) {
if (VDBG) log("handlePromptUnvalidated " + network);
NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
@@ -2553,17 +2828,17 @@
!nai.networkMisc.explicitlySelected || nai.networkMisc.acceptUnvalidated) {
return;
}
+ showValidationNotification(nai, NotificationType.NO_INTERNET);
+ }
- Intent intent = new Intent(ConnectivityManager.ACTION_PROMPT_UNVALIDATED);
- intent.setData(Uri.fromParts("netId", Integer.toString(network.netId), null));
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setClassName("com.android.settings",
- "com.android.settings.wifi.WifiNoInternetDialog");
+ private void handleNetworkUnvalidated(NetworkAgentInfo nai) {
+ NetworkCapabilities nc = nai.networkCapabilities;
+ if (DBG) log("handleNetworkUnvalidated " + nai.name() + " cap=" + nc);
- PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
- mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
- setProvNotificationVisibleIntent(true, nai.network.netId, NotificationType.NO_INTERNET,
- nai.networkInfo.getType(), nai.networkInfo.getExtraInfo(), pendingIntent, true);
+ if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+ mAvoidBadWifiTracker.shouldNotifyWifiUnvalidated()) {
+ showValidationNotification(nai, NotificationType.LOST_INTERNET);
+ }
}
private class InternalHandler extends Handler {
@@ -2639,6 +2914,10 @@
handleSetAcceptUnvalidated((Network) msg.obj, msg.arg1 != 0, msg.arg2 != 0);
break;
}
+ case EVENT_SET_AVOID_UNVALIDATED: {
+ handleSetAvoidUnvalidated((Network) msg.obj);
+ break;
+ }
case EVENT_PROMPT_UNVALIDATED: {
handlePromptUnvalidated((Network) msg.obj);
break;
@@ -2647,6 +2926,12 @@
handleMobileDataAlwaysOn();
break;
}
+ case EVENT_REQUEST_LINKPROPERTIES:
+ handleRequestLinkProperties((NetworkRequest) msg.obj, msg.arg1);
+ break;
+ case EVENT_REQUEST_NETCAPABILITIES:
+ handleRequestNetworkCapabilities((NetworkRequest) msg.obj, msg.arg1);
+ break;
// Sent by KeepaliveTracker to process an app request on the state machine thread.
case NetworkAgent.CMD_START_PACKET_KEEPALIVE: {
mKeepaliveTracker.handleStartKeepalive(msg);
@@ -2671,6 +2956,7 @@
}
// javadoc from interface
+ @Override
public int tether(String iface) {
ConnectivityManager.enforceTetherChangePermission(mContext);
if (isTetheringSupported()) {
@@ -2688,6 +2974,7 @@
}
// javadoc from interface
+ @Override
public int untether(String iface) {
ConnectivityManager.enforceTetherChangePermission(mContext);
@@ -2706,6 +2993,7 @@
}
// javadoc from interface
+ @Override
public int getLastTetherError(String iface) {
enforceTetherAccessPermission();
@@ -2717,6 +3005,7 @@
}
// TODO - proper iface API for selection by property, inspection, etc
+ @Override
public String[] getTetherableUsbRegexs() {
enforceTetherAccessPermission();
if (isTetheringSupported()) {
@@ -2726,6 +3015,7 @@
}
}
+ @Override
public String[] getTetherableWifiRegexs() {
enforceTetherAccessPermission();
if (isTetheringSupported()) {
@@ -2735,6 +3025,7 @@
}
}
+ @Override
public String[] getTetherableBluetoothRegexs() {
enforceTetherAccessPermission();
if (isTetheringSupported()) {
@@ -2744,6 +3035,7 @@
}
}
+ @Override
public int setUsbTethering(boolean enable) {
ConnectivityManager.enforceTetherChangePermission(mContext);
if (isTetheringSupported()) {
@@ -2755,21 +3047,25 @@
// TODO - move iface listing, queries, etc to new module
// javadoc from interface
+ @Override
public String[] getTetherableIfaces() {
enforceTetherAccessPermission();
return mTethering.getTetherableIfaces();
}
+ @Override
public String[] getTetheredIfaces() {
enforceTetherAccessPermission();
return mTethering.getTetheredIfaces();
}
+ @Override
public String[] getTetheringErroredIfaces() {
enforceTetherAccessPermission();
return mTethering.getErroredIfaces();
}
+ @Override
public String[] getTetheredDhcpRanges() {
enforceConnectivityInternalPermission();
return mTethering.getTetheredDhcpRanges();
@@ -2828,12 +3124,14 @@
}
// 100 percent is full good, 0 is full bad.
+ @Override
public void reportInetCondition(int networkType, int percentage) {
NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
if (nai == null) return;
reportNetworkConnectivity(nai.network, percentage > 50);
}
+ @Override
public void reportNetworkConnectivity(Network network, boolean hasConnectivity) {
enforceAccessPermission();
enforceInternetPermission();
@@ -2878,6 +3176,7 @@
}
}
+ @Override
public ProxyInfo getProxyForNetwork(Network network) {
if (network == null) return getDefaultProxy();
final ProxyInfo globalProxy = getGlobalProxy();
@@ -3449,118 +3748,6 @@
return -1;
}
- private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
- private static enum NotificationType { SIGN_IN, NO_INTERNET; };
-
- private void setProvNotificationVisible(boolean visible, int networkType, String action) {
- Intent intent = new Intent(action);
- PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
- // Concatenate the range of types onto the range of NetIDs.
- int id = MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
- setProvNotificationVisibleIntent(visible, id, NotificationType.SIGN_IN,
- networkType, null, pendingIntent, false);
- }
-
- /**
- * Show or hide network provisioning notifications.
- *
- * We use notifications for two purposes: to notify that a network requires sign in
- * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
- * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
- * particular network we can display the notification type that was most recently requested.
- * So for example if a captive portal fails to reply within a few seconds of connecting, we
- * might first display NO_INTERNET, and then when the captive portal check completes, display
- * SIGN_IN.
- *
- * @param id an identifier that uniquely identifies this notification. This must match
- * between show and hide calls. We use the NetID value but for legacy callers
- * we concatenate the range of types with the range of NetIDs.
- */
- private void setProvNotificationVisibleIntent(boolean visible, int id,
- NotificationType notifyType, int networkType, String extraInfo, PendingIntent intent,
- boolean highPriority) {
- if (VDBG || (DBG && visible)) {
- log("setProvNotificationVisibleIntent " + notifyType + " visible=" + visible
- + " networkType=" + getNetworkTypeName(networkType)
- + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
- }
-
- Resources r = Resources.getSystem();
- NotificationManager notificationManager = (NotificationManager) mContext
- .getSystemService(Context.NOTIFICATION_SERVICE);
-
- if (visible) {
- CharSequence title;
- CharSequence details;
- int icon;
- if (notifyType == NotificationType.NO_INTERNET &&
- networkType == ConnectivityManager.TYPE_WIFI) {
- title = r.getString(R.string.wifi_no_internet, 0);
- details = r.getString(R.string.wifi_no_internet_detailed);
- icon = R.drawable.stat_notify_wifi_in_range; // TODO: Need new icon.
- } else if (notifyType == NotificationType.SIGN_IN) {
- switch (networkType) {
- case ConnectivityManager.TYPE_WIFI:
- title = r.getString(R.string.wifi_available_sign_in, 0);
- details = r.getString(R.string.network_available_sign_in_detailed,
- extraInfo);
- icon = R.drawable.stat_notify_wifi_in_range;
- break;
- case ConnectivityManager.TYPE_MOBILE:
- case ConnectivityManager.TYPE_MOBILE_HIPRI:
- title = r.getString(R.string.network_available_sign_in, 0);
- // TODO: Change this to pull from NetworkInfo once a printable
- // name has been added to it
- details = mTelephonyManager.getNetworkOperatorName();
- icon = R.drawable.stat_notify_rssi_in_range;
- break;
- default:
- title = r.getString(R.string.network_available_sign_in, 0);
- details = r.getString(R.string.network_available_sign_in_detailed,
- extraInfo);
- icon = R.drawable.stat_notify_rssi_in_range;
- break;
- }
- } else {
- Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network type "
- + getNetworkTypeName(networkType));
- return;
- }
-
- Notification notification = new Notification.Builder(mContext)
- .setWhen(0)
- .setSmallIcon(icon)
- .setAutoCancel(true)
- .setTicker(title)
- .setColor(mContext.getColor(
- com.android.internal.R.color.system_notification_accent_color))
- .setContentTitle(title)
- .setContentText(details)
- .setContentIntent(intent)
- .setLocalOnly(true)
- .setPriority(highPriority ?
- Notification.PRIORITY_HIGH :
- Notification.PRIORITY_DEFAULT)
- .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0)
- .setOnlyAlertOnce(true)
- .build();
-
- try {
- notificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
- } catch (NullPointerException npe) {
- loge("setNotificationVisible: visible notificationManager npe=" + npe);
- npe.printStackTrace();
- }
- } else {
- try {
- notificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
- } catch (NullPointerException npe) {
- loge("setNotificationVisible: cancel notificationManager npe=" + npe);
- npe.printStackTrace();
- }
- }
- }
-
/** Location to an updatable file listing carrier provisioning urls.
* An example:
*
@@ -3664,7 +3851,9 @@
enforceConnectivityInternalPermission();
final long ident = Binder.clearCallingIdentity();
try {
- setProvNotificationVisible(visible, networkType, action);
+ // Concatenate the range of types onto the range of NetIDs.
+ int id = MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
+ mNotifier.setProvNotificationVisible(visible, id, action);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -3793,31 +3982,12 @@
}
}
- /**
- * A NetworkRequest as registered by an application can be one of three
- * types:
- *
- * - "listen", for which the framework will issue callbacks about any
- * and all networks that match the specified NetworkCapabilities,
- *
- * - "request", capable of causing a specific network to be created
- * first (e.g. a telephony DUN request), the framework will issue
- * callbacks about the single, highest scoring current network
- * (if any) that matches the specified NetworkCapabilities, or
- *
- * - "track the default network", a hybrid of the two designed such
- * that the framework will issue callbacks for the single, highest
- * scoring current network (if any) that matches the capabilities of
- * the default Internet request (mDefaultRequest), but which cannot
- * cause the framework to either create or retain the existence of
- * any specific network.
- *
- */
- private static enum NetworkRequestType {
- LISTEN,
- TRACK_DEFAULT,
- REQUEST
- };
+ private void ensureNetworkRequestHasType(NetworkRequest request) {
+ if (request.type == NetworkRequest.Type.NONE) {
+ throw new IllegalArgumentException(
+ "All NetworkRequests in ConnectivityService must have a type");
+ }
+ }
/**
* Tracks info about the requester.
@@ -3831,27 +4001,26 @@
final int mPid;
final int mUid;
final Messenger messenger;
- private final NetworkRequestType mType;
- NetworkRequestInfo(NetworkRequest r, PendingIntent pi, NetworkRequestType type) {
+ NetworkRequestInfo(NetworkRequest r, PendingIntent pi) {
request = r;
+ ensureNetworkRequestHasType(request);
mPendingIntent = pi;
messenger = null;
mBinder = null;
mPid = getCallingPid();
mUid = getCallingUid();
- mType = type;
enforceRequestCountLimit();
}
- NetworkRequestInfo(Messenger m, NetworkRequest r, IBinder binder, NetworkRequestType type) {
+ NetworkRequestInfo(Messenger m, NetworkRequest r, IBinder binder) {
super();
messenger = m;
request = r;
+ ensureNetworkRequestHasType(request);
mBinder = binder;
mPid = getCallingPid();
mUid = getCallingUid();
- mType = type;
mPendingIntent = null;
enforceRequestCountLimit();
@@ -3872,16 +4041,6 @@
}
}
- private String typeString() {
- switch (mType) {
- case LISTEN: return "Listen";
- case REQUEST: return "Request";
- case TRACK_DEFAULT: return "Track default";
- default:
- return "unknown type";
- }
- }
-
void unlinkDeathRecipient() {
if (mBinder != null) {
mBinder.unlinkToDeath(this, 0);
@@ -3894,29 +4053,8 @@
releaseNetworkRequest(request);
}
- /**
- * Returns true iff. the contained NetworkRequest is one that:
- *
- * - should be associated with at most one satisfying network
- * at a time;
- *
- * - should cause a network to be kept up if it is the only network
- * which can satisfy the NetworkReqeust.
- *
- * For full detail of how isRequest() is used for pairing Networks with
- * NetworkRequests read rematchNetworkAndRequests().
- *
- * TODO: Rename to something more properly descriptive.
- */
- public boolean isRequest() {
- return (mType == NetworkRequestType.TRACK_DEFAULT) ||
- (mType == NetworkRequestType.REQUEST);
- }
-
public String toString() {
- return typeString() +
- " from uid/pid:" + mUid + "/" + mPid +
- " for " + request +
+ return "uid/pid:" + mUid + "/" + mPid + " " + request +
(mPendingIntent == null ? "" : " to trigger " + mPendingIntent);
}
}
@@ -3966,18 +4104,21 @@
@Override
public NetworkRequest requestNetwork(NetworkCapabilities networkCapabilities,
Messenger messenger, int timeoutMs, IBinder binder, int legacyType) {
- final NetworkRequestType type = (networkCapabilities == null)
- ? NetworkRequestType.TRACK_DEFAULT
- : NetworkRequestType.REQUEST;
+ final NetworkRequest.Type type = (networkCapabilities == null)
+ ? NetworkRequest.Type.TRACK_DEFAULT
+ : NetworkRequest.Type.REQUEST;
// If the requested networkCapabilities is null, take them instead from
// the default network request. This allows callers to keep track of
// the system default network.
- if (type == NetworkRequestType.TRACK_DEFAULT) {
+ if (type == NetworkRequest.Type.TRACK_DEFAULT) {
networkCapabilities = new NetworkCapabilities(mDefaultRequest.networkCapabilities);
enforceAccessPermission();
} else {
networkCapabilities = new NetworkCapabilities(networkCapabilities);
enforceNetworkRequestPermissions(networkCapabilities);
+ // TODO: this is incorrect. We mark the request as metered or not depending on the state
+ // of the app when the request is filed, but we never change the request if the app
+ // changes network state. http://b/29964605
enforceMeteredApnPolicy(networkCapabilities);
}
ensureRequestableCapabilities(networkCapabilities);
@@ -3993,8 +4134,8 @@
}
NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
- nextNetworkRequestId());
- NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder, type);
+ nextNetworkRequestId(), type);
+ NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder);
if (DBG) log("requestNetwork for " + nri);
mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
@@ -4007,7 +4148,7 @@
private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities) {
if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
- enforceConnectivityInternalPermission();
+ enforceConnectivityRestrictedNetworksPermission();
} else {
enforceChangePermission();
}
@@ -4064,9 +4205,8 @@
ensureRequestableCapabilities(networkCapabilities);
NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
- nextNetworkRequestId());
- NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation,
- NetworkRequestType.REQUEST);
+ nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
+ NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation);
if (DBG) log("pendingRequest for " + nri);
mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT,
nri));
@@ -4116,9 +4256,9 @@
}
NetworkRequest networkRequest = new NetworkRequest(
- new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId());
- NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder,
- NetworkRequestType.LISTEN);
+ new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId(),
+ NetworkRequest.Type.LISTEN);
+ NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder);
if (VDBG) log("listenForNetwork for " + nri);
mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
@@ -4134,18 +4274,35 @@
}
NetworkRequest networkRequest = new NetworkRequest(
- new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId());
- NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation,
- NetworkRequestType.LISTEN);
+ new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId(),
+ NetworkRequest.Type.LISTEN);
+ NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation);
if (VDBG) log("pendingListenForNetwork for " + nri);
mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
}
@Override
+ public void requestLinkProperties(NetworkRequest networkRequest) {
+ ensureNetworkRequestHasType(networkRequest);
+ if (networkRequest.type == NetworkRequest.Type.LISTEN) return;
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_REQUEST_LINKPROPERTIES, getCallingUid(), 0, networkRequest));
+ }
+
+ @Override
+ public void requestNetworkCapabilities(NetworkRequest networkRequest) {
+ ensureNetworkRequestHasType(networkRequest);
+ if (networkRequest.type == NetworkRequest.Type.LISTEN) return;
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_REQUEST_NETCAPABILITIES, getCallingUid(), 0, networkRequest));
+ }
+
+ @Override
public void releaseNetworkRequest(NetworkRequest networkRequest) {
- mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST, getCallingUid(),
- 0, networkRequest));
+ ensureNetworkRequestHasType(networkRequest);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_RELEASE_NETWORK_REQUEST, getCallingUid(), 0, networkRequest));
}
@Override
@@ -4220,6 +4377,10 @@
return nai == getDefaultNetwork();
}
+ private boolean isDefaultRequest(NetworkRequestInfo nri) {
+ return nri.request.requestId == mDefaultRequest.requestId;
+ }
+
public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo,
LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
int currentScore, NetworkMisc networkMisc) {
@@ -4415,10 +4576,13 @@
* augmented with any stateful capabilities implied from {@code networkAgent}
* (e.g., validated status and captive portal status).
*
+ * @param oldScore score of the network before any of the changes that prompted us
+ * to call this function.
* @param nai the network having its capabilities updated.
* @param networkCapabilities the new network capabilities.
*/
- private void updateCapabilities(NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
+ private void updateCapabilities(
+ int oldScore, NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
// Don't modify caller's NetworkCapabilities.
networkCapabilities = new NetworkCapabilities(networkCapabilities);
if (nai.lastValidated) {
@@ -4432,7 +4596,6 @@
networkCapabilities.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
}
if (!Objects.equals(nai.networkCapabilities, networkCapabilities)) {
- final int oldScore = nai.getCurrentScore();
if (nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) !=
networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
try {
@@ -4452,10 +4615,10 @@
}
private void sendUpdatedScoreToFactories(NetworkAgentInfo nai) {
- for (int i = 0; i < nai.networkRequests.size(); i++) {
- NetworkRequest nr = nai.networkRequests.valueAt(i);
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
// Don't send listening requests to factories. b/17393458
- if (!isRequest(nr)) continue;
+ if (nr.isListen()) continue;
sendUpdatedScoreToFactories(nr, nai.getCurrentScore());
}
}
@@ -4504,7 +4667,7 @@
}
private void callCallbackForRequest(NetworkRequestInfo nri,
- NetworkAgentInfo networkAgent, int notificationType) {
+ NetworkAgentInfo networkAgent, int notificationType, int arg1) {
if (nri.messenger == null) return; // Default request has no msgr
Bundle bundle = new Bundle();
bundle.putParcelable(NetworkRequest.class.getSimpleName(),
@@ -4516,7 +4679,7 @@
}
switch (notificationType) {
case ConnectivityManager.CALLBACK_LOSING: {
- msg.arg1 = 30 * 1000; // TODO - read this from NetworkMonitor
+ msg.arg1 = arg1;
break;
}
case ConnectivityManager.CALLBACK_CAP_CHANGED: {
@@ -4545,12 +4708,14 @@
}
private void teardownUnneededNetwork(NetworkAgentInfo nai) {
- for (int i = 0; i < nai.networkRequests.size(); i++) {
- NetworkRequest nr = nai.networkRequests.valueAt(i);
- // Ignore listening requests.
- if (!isRequest(nr)) continue;
- loge("Dead network still had at least " + nr);
- break;
+ if (nai.numRequestNetworkRequests() != 0) {
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ // Ignore listening requests.
+ if (nr.isListen()) continue;
+ loge("Dead network still had at least " + nr);
+ break;
+ }
}
nai.asyncChannel.disconnect();
}
@@ -4561,7 +4726,14 @@
return;
}
if (DBG) log("handleLingerComplete for " + oldNetwork.name());
- teardownUnneededNetwork(oldNetwork);
+
+ // If we get here it means that the last linger timeout for this network expired. So there
+ // must be no other active linger timers, and we must stop lingering.
+ oldNetwork.clearLingerState();
+
+ if (unneeded(oldNetwork)) {
+ teardownUnneededNetwork(oldNetwork);
+ }
}
private void makeDefault(NetworkAgentInfo newNetwork) {
@@ -4606,7 +4778,7 @@
// performed to tear down unvalidated networks that have no chance (i.e. even if
// validated) of becoming the highest scoring network.
private void rematchNetworkAndRequests(NetworkAgentInfo newNetwork,
- ReapUnvalidatedNetworks reapUnvalidatedNetworks) {
+ ReapUnvalidatedNetworks reapUnvalidatedNetworks, long now) {
if (!newNetwork.everConnected) return;
boolean keep = newNetwork.isVPN();
boolean isNewDefault = false;
@@ -4632,7 +4804,7 @@
// check if it satisfies the NetworkCapabilities
if (VDBG) log(" checking if request is satisfied: " + nri.request);
if (satisfies) {
- if (!nri.isRequest()) {
+ if (nri.request.isListen()) {
// This is not a request, it's a callback listener.
// Add it to newNetwork regardless of score.
if (newNetwork.addRequest(nri.request)) addedRequests.add(nri);
@@ -4651,13 +4823,13 @@
if (VDBG) log("rematch for " + newNetwork.name());
if (currentNetwork != null) {
if (VDBG) log(" accepting network in place of " + currentNetwork.name());
- currentNetwork.networkRequests.remove(nri.request.requestId);
- currentNetwork.networkLingered.add(nri.request);
+ currentNetwork.removeRequest(nri.request.requestId);
+ currentNetwork.lingerRequest(nri.request, now, mLingerDelayMs);
affectedNetworks.add(currentNetwork);
} else {
if (VDBG) log(" accepting network in place of null");
}
- unlinger(newNetwork);
+ newNetwork.unlingerRequest(nri.request);
mNetworkForRequestId.put(nri.request.requestId, newNetwork);
if (!newNetwork.addRequest(nri.request)) {
Slog.wtf(TAG, "BUG: " + newNetwork.name() + " already has " + nri.request);
@@ -4670,15 +4842,18 @@
// network. Think about if there is a way to reduce this. Push
// netid->request mapping to each factory?
sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore());
- if (mDefaultRequest.requestId == nri.request.requestId) {
+ if (isDefaultRequest(nri)) {
isNewDefault = true;
oldDefaultNetwork = currentNetwork;
+ if (currentNetwork != null) {
+ mLingerMonitor.noteLingerDefaultNetwork(currentNetwork, newNetwork);
+ }
}
}
- } else if (newNetwork.networkRequests.get(nri.request.requestId) != null) {
+ } else if (newNetwork.isSatisfyingRequest(nri.request.requestId)) {
// If "newNetwork" is listed as satisfying "nri" but no longer satisfies "nri",
// mark it as no longer satisfying "nri". Because networks are processed by
- // rematchAllNetworkAndRequests() in descending score order, "currentNetwork" will
+ // rematchAllNetworksAndRequests() in descending score order, "currentNetwork" will
// match "newNetwork" before this loop will encounter a "currentNetwork" with higher
// score than "newNetwork" and where "currentNetwork" no longer satisfies "nri".
// This means this code doesn't have to handle the case where "currentNetwork" no
@@ -4687,12 +4862,12 @@
log("Network " + newNetwork.name() + " stopped satisfying" +
" request " + nri.request.requestId);
}
- newNetwork.networkRequests.remove(nri.request.requestId);
+ newNetwork.removeRequest(nri.request.requestId);
if (currentNetwork == newNetwork) {
mNetworkForRequestId.remove(nri.request.requestId);
sendUpdatedScoreToFactories(nri.request, 0);
} else {
- if (nri.isRequest()) {
+ if (nri.request.isRequest()) {
Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
newNetwork.name() +
" without updating mNetworkForRequestId or factories!");
@@ -4705,23 +4880,7 @@
// a) be requested and b) change is NET_CAPABILITY_TRUSTED,
// so this code is only incorrect for a network that loses
// the TRUSTED capability, which is a rare case.
- callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST);
- }
- }
- // Linger any networks that are no longer needed.
- for (NetworkAgentInfo nai : affectedNetworks) {
- if (nai.lingering) {
- // Already lingered. Nothing to do. This can only happen if "nai" is in
- // "affectedNetworks" twice. The reasoning being that to get added to
- // "affectedNetworks", "nai" must have been satisfying a NetworkRequest
- // (i.e. not lingered) so it could have only been lingered by this loop.
- // unneeded(nai) will be false and we'll call unlinger() below which would
- // be bad, so handle it here.
- } else if (unneeded(nai)) {
- linger(nai);
- } else {
- // Clear nai.networkLingered we might have added above.
- unlinger(nai);
+ callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST, 0);
}
}
if (isNewDefault) {
@@ -4746,6 +4905,15 @@
// before LegacyTypeTracker sends legacy broadcasts
for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
+ // Linger any networks that are no longer needed. This should be done after sending the
+ // available callback for newNetwork.
+ for (NetworkAgentInfo nai : affectedNetworks) {
+ updateLingerState(nai, now);
+ }
+ // Possibly unlinger newNetwork. Unlingering a network does not send any callbacks so it
+ // does not need to be done in any particular order.
+ updateLingerState(newNetwork, now);
+
if (isNewDefault) {
// Maintain the illusion: since the legacy API only
// understands one network at a time, we must pretend
@@ -4792,9 +4960,9 @@
// (notification callbacks) and then uses the old api (getNetworkInfo(type))
// they may get old info. Reverse this after the old startUsing api is removed.
// This is on top of the multiple intent sequencing referenced in the todo above.
- for (int i = 0; i < newNetwork.networkRequests.size(); i++) {
- NetworkRequest nr = newNetwork.networkRequests.valueAt(i);
- if (nr.legacyType != TYPE_NONE && isRequest(nr)) {
+ for (int i = 0; i < newNetwork.numNetworkRequests(); i++) {
+ NetworkRequest nr = newNetwork.requestAt(i);
+ if (nr.legacyType != TYPE_NONE && nr.isRequest()) {
// legacy type tracker filters out repeat adds
mLegacyTypeTracker.add(nr.legacyType, newNetwork);
}
@@ -4811,8 +4979,19 @@
if (reapUnvalidatedNetworks == ReapUnvalidatedNetworks.REAP) {
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
if (unneeded(nai)) {
- if (DBG) log("Reaping " + nai.name());
- teardownUnneededNetwork(nai);
+ if (nai.getLingerExpiry() > 0) {
+ // This network has active linger timers and no requests, but is not
+ // lingering. Linger it.
+ //
+ // One way (the only way?) this can happen if this network is unvalidated
+ // and became unneeded due to another network improving its score to the
+ // point where this network will no longer be able to satisfy any requests
+ // even if it validates.
+ updateLingerState(nai, now);
+ } else {
+ if (DBG) log("Reaping " + nai.name());
+ teardownUnneededNetwork(nai);
+ }
}
}
}
@@ -4839,8 +5018,9 @@
// Optimization: Only reprocess "changed" if its score improved. This is safe because it
// can only add more NetworkRequests satisfied by "changed", and this is exactly what
// rematchNetworkAndRequests() handles.
+ final long now = SystemClock.elapsedRealtime();
if (changed != null && oldScore < changed.getCurrentScore()) {
- rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP);
+ rematchNetworkAndRequests(changed, ReapUnvalidatedNetworks.REAP, now);
} else {
final NetworkAgentInfo[] nais = mNetworkAgentInfos.values().toArray(
new NetworkAgentInfo[mNetworkAgentInfos.size()]);
@@ -4854,7 +5034,8 @@
// is complete could incorrectly teardown a network that hasn't yet been
// rematched.
(nai != nais[nais.length-1]) ? ReapUnvalidatedNetworks.DONT_REAP
- : ReapUnvalidatedNetworks.REAP);
+ : ReapUnvalidatedNetworks.REAP,
+ now);
}
}
}
@@ -4964,7 +5145,8 @@
updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
// Consider network even though it is not yet validated.
- rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP);
+ final long now = SystemClock.elapsedRealtime();
+ rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP, now);
// This has to happen after matching the requests, because callbacks are just requests.
notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
@@ -5012,14 +5194,8 @@
// notify only this one new request of the current state
protected void notifyNetworkCallback(NetworkAgentInfo nai, NetworkRequestInfo nri) {
int notifyType = ConnectivityManager.CALLBACK_AVAILABLE;
- // TODO - read state from monitor to decide what to send.
-// if (nai.networkMonitor.isLingering()) {
-// notifyType = NetworkCallbacks.LOSING;
-// } else if (nai.networkMonitor.isEvaluating()) {
-// notifyType = NetworkCallbacks.callCallbackForRequest(request, nai, notifyType);
-// }
if (nri.mPendingIntent == null) {
- callCallbackForRequest(nri, nai, notifyType);
+ callCallbackForRequest(nri, nai, notifyType, 0);
} else {
sendPendingIntentForRequest(nri, nai, notifyType);
}
@@ -5053,7 +5229,7 @@
intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo());
}
NetworkAgentInfo newDefaultAgent = null;
- if (nai.networkRequests.get(mDefaultRequest.requestId) != null) {
+ if (nai.isSatisfyingRequest(mDefaultRequest.requestId)) {
newDefaultAgent = getDefaultNetwork();
if (newDefaultAgent != null) {
intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO,
@@ -5071,20 +5247,24 @@
}
}
- protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+ protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) {
if (VDBG) log("notifyType " + notifyTypeToName(notifyType) + " for " + networkAgent.name());
- for (int i = 0; i < networkAgent.networkRequests.size(); i++) {
- NetworkRequest nr = networkAgent.networkRequests.valueAt(i);
+ for (int i = 0; i < networkAgent.numNetworkRequests(); i++) {
+ NetworkRequest nr = networkAgent.requestAt(i);
NetworkRequestInfo nri = mNetworkRequests.get(nr);
if (VDBG) log(" sending notification for " + nr);
if (nri.mPendingIntent == null) {
- callCallbackForRequest(nri, networkAgent, notifyType);
+ callCallbackForRequest(nri, networkAgent, notifyType, arg1);
} else {
sendPendingIntentForRequest(nri, networkAgent, notifyType);
}
}
}
+ protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+ notifyNetworkCallbacks(networkAgent, notifyType, 0);
+ }
+
private String notifyTypeToName(int notifyType) {
switch (notifyType) {
case ConnectivityManager.CALLBACK_PRECHECK: return "PRECHECK";
@@ -5207,6 +5387,9 @@
}
}
}
+
+ Settings.Global.putString(mContext.getContentResolver(),
+ Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
}
@VisibleForTesting
@@ -5215,7 +5398,17 @@
return new NetworkMonitor(context, handler, nai, defaultRequest);
}
- private static void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
+ @VisibleForTesting
+ AvoidBadWifiTracker createAvoidBadWifiTracker(Context c, Handler h, Runnable r) {
+ return new AvoidBadWifiTracker(c, h, r);
+ }
+
+ @VisibleForTesting
+ public WakeupMessage makeWakeupMessage(Context c, Handler h, String s, int cmd, Object obj) {
+ return new WakeupMessage(c, h, s, cmd, 0, 0, obj);
+ }
+
+ private void logDefaultNetworkEvent(NetworkAgentInfo newNai, NetworkAgentInfo prevNai) {
int newNetid = NETID_UNSET;
int prevNetid = NETID_UNSET;
int[] transports = new int[0];
@@ -5233,6 +5426,10 @@
hadIPv6 = lp.hasGlobalIPv6Address() && lp.hasIPv6DefaultRoute();
}
- DefaultNetworkEvent.logEvent(newNetid, transports, prevNetid, hadIPv4, hadIPv6);
+ mMetricsLog.log(new DefaultNetworkEvent(newNetid, transports, prevNetid, hadIPv4, hadIPv6));
+ }
+
+ private void logNetworkEvent(NetworkAgentInfo nai, int evtype) {
+ mMetricsLog.log(new NetworkEvent(nai.network.netId, evtype));
}
}
diff --git a/services/core/java/com/android/server/connectivity/LingerMonitor.java b/services/core/java/com/android/server/connectivity/LingerMonitor.java
new file mode 100644
index 0000000..635db19
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/LingerMonitor.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkCapabilities;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.SparseBooleanArray;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.MessageUtils;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+/**
+ * Class that monitors default network linger events and possibly notifies the user of network
+ * switches.
+ *
+ * This class is not thread-safe and all its methods must be called on the ConnectivityService
+ * handler thread.
+ */
+public class LingerMonitor {
+
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+ private static final String TAG = LingerMonitor.class.getSimpleName();
+
+ public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
+ public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;
+
+ private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
+ @VisibleForTesting
+ public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
+ "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
+
+ @VisibleForTesting
+ public static final int NOTIFY_TYPE_NONE = 0;
+ public static final int NOTIFY_TYPE_NOTIFICATION = 1;
+ public static final int NOTIFY_TYPE_TOAST = 2;
+
+ private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
+ new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
+
+ private final Context mContext;
+ private final NetworkNotificationManager mNotifier;
+ private final int mDailyLimit;
+ private final long mRateLimitMillis;
+
+ private long mFirstNotificationMillis;
+ private long mLastNotificationMillis;
+ private int mNotificationCounter;
+
+ /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
+ private final SparseIntArray mNotifications = new SparseIntArray();
+
+ /** Whether we ever notified that we switched away from a particular network. */
+ private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
+
+ public LingerMonitor(Context context, NetworkNotificationManager notifier,
+ int dailyLimit, long rateLimitMillis) {
+ mContext = context;
+ mNotifier = notifier;
+ mDailyLimit = dailyLimit;
+ mRateLimitMillis = rateLimitMillis;
+ }
+
+ private static HashMap<String, Integer> makeTransportToNameMap() {
+ SparseArray<String> numberToName = MessageUtils.findMessageNames(
+ new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+ HashMap<String, Integer> nameToNumber = new HashMap<>();
+ for (int i = 0; i < numberToName.size(); i++) {
+ // MessageUtils will fail to initialize if there are duplicate constant values, so there
+ // are no duplicates here.
+ nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
+ }
+ return nameToNumber;
+ }
+
+ private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
+ return nai.networkCapabilities.hasTransport(transport);
+ }
+
+ private int getNotificationSource(NetworkAgentInfo toNai) {
+ for (int i = 0; i < mNotifications.size(); i++) {
+ if (mNotifications.valueAt(i) == toNai.network.netId) {
+ return mNotifications.keyAt(i);
+ }
+ }
+ return NETID_UNSET;
+ }
+
+ private boolean everNotified(NetworkAgentInfo nai) {
+ return mEverNotified.get(nai.network.netId, false);
+ }
+
+ @VisibleForTesting
+ public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ // TODO: Evaluate moving to CarrierConfigManager.
+ String[] notifySwitches =
+ mContext.getResources().getStringArray(R.array.config_networkNotifySwitches);
+
+ if (VDBG) {
+ Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
+ }
+
+ for (String notifySwitch : notifySwitches) {
+ if (TextUtils.isEmpty(notifySwitch)) continue;
+ String[] transports = notifySwitch.split("-", 2);
+ if (transports.length != 2) {
+ Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
+ continue;
+ }
+ int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
+ int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
+ if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ mNotifier.showNotification(fromNai.network.netId, NotificationType.NETWORK_SWITCH,
+ fromNai, toNai, createNotificationIntent(), true);
+ }
+
+ @VisibleForTesting
+ protected PendingIntent createNotificationIntent() {
+ return PendingIntent.getActivityAsUser(mContext, 0, CELLULAR_SETTINGS,
+ PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+ }
+
+ // Removes any notification that was put up as a result of switching to nai.
+ private void maybeStopNotifying(NetworkAgentInfo nai) {
+ int fromNetId = getNotificationSource(nai);
+ if (fromNetId != NETID_UNSET) {
+ mNotifications.delete(fromNetId);
+ mNotifier.clearNotification(fromNetId);
+ // Toasts can't be deleted.
+ }
+ }
+
+ // Notify the user of a network switch using a notification or a toast.
+ private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
+ int notifyType =
+ mContext.getResources().getInteger(R.integer.config_networkNotifySwitchType);
+ if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
+ notifyType = NOTIFY_TYPE_TOAST;
+ }
+
+ if (VDBG) {
+ Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
+ }
+
+ switch (notifyType) {
+ case NOTIFY_TYPE_NONE:
+ return;
+ case NOTIFY_TYPE_NOTIFICATION:
+ showNotification(fromNai, toNai);
+ break;
+ case NOTIFY_TYPE_TOAST:
+ mNotifier.showToast(fromNai, toNai);
+ break;
+ default:
+ Log.e(TAG, "Unknown notify type " + notifyType);
+ return;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "Notifying switch from=" + fromNai.name() + " to=" + toNai.name() +
+ " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
+ }
+
+ mNotifications.put(fromNai.network.netId, toNai.network.netId);
+ mEverNotified.put(fromNai.network.netId, true);
+ }
+
+ // The default network changed from fromNai to toNai due to a change in score.
+ public void noteLingerDefaultNetwork(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ if (VDBG) {
+ Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.name() +
+ " everValidated=" + fromNai.everValidated +
+ " lastValidated=" + fromNai.lastValidated +
+ " to=" + toNai.name());
+ }
+
+ // If we are currently notifying the user because the device switched to fromNai, now that
+ // we are switching away from it we should remove the notification. This includes the case
+ // where we switch back to toNai because its score improved again (e.g., because it regained
+ // Internet access).
+ maybeStopNotifying(fromNai);
+
+ // If this network never validated, don't notify. Otherwise, we could do things like:
+ //
+ // 1. Unvalidated wifi connects.
+ // 2. Unvalidated mobile data connects.
+ // 3. Cell validates, and we show a notification.
+ // or:
+ // 1. User connects to wireless printer.
+ // 2. User turns on cellular data.
+ // 3. We show a notification.
+ if (!fromNai.everValidated) return;
+
+ // If this network is a captive portal, don't notify. This cannot happen on initial connect
+ // to a captive portal, because the everValidated check above will fail. However, it can
+ // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
+ // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
+ // We don't want to overwrite that notification with this one; the user has already been
+ // notified, and of the two, the captive portal notification is the more useful one because
+ // it allows the user to sign in to the captive portal. In this case, display a toast
+ // in addition to the captive portal notification.
+ //
+ // Note that if the network we switch to is already up when the captive portal reappears,
+ // this won't work because NetworkMonitor tells ConnectivityService that the network is
+ // unvalidated (causing a switch) before asking it to show the sign in notification. In this
+ // case, the toast won't show and we'll only display the sign in notification. This is the
+ // best we can do at this time.
+ boolean forceToast = fromNai.networkCapabilities.hasCapability(
+ NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
+
+ // Only show the notification once, in order to avoid irritating the user every time.
+ // TODO: should we do this?
+ if (everNotified(fromNai)) {
+ if (VDBG) {
+ Log.d(TAG, "Not notifying handover from " + fromNai.name() + ", already notified");
+ }
+ return;
+ }
+
+ // Only show the notification if we switched away because a network became unvalidated, not
+ // because its score changed.
+ // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
+ // unvalidated.
+ if (fromNai.lastValidated) return;
+
+ if (!isNotificationEnabled(fromNai, toNai)) return;
+
+ final long now = SystemClock.elapsedRealtime();
+ if (isRateLimited(now) || isAboveDailyLimit(now)) return;
+
+ notify(fromNai, toNai, forceToast);
+ }
+
+ public void noteDisconnect(NetworkAgentInfo nai) {
+ mNotifications.delete(nai.network.netId);
+ mEverNotified.delete(nai.network.netId);
+ maybeStopNotifying(nai);
+ // No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
+ }
+
+ private boolean isRateLimited(long now) {
+ final long millisSinceLast = now - mLastNotificationMillis;
+ if (millisSinceLast < mRateLimitMillis) {
+ return true;
+ }
+ mLastNotificationMillis = now;
+ return false;
+ }
+
+ private boolean isAboveDailyLimit(long now) {
+ if (mFirstNotificationMillis == 0) {
+ mFirstNotificationMillis = now;
+ }
+ final long millisSinceFirst = now - mFirstNotificationMillis;
+ if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) {
+ mNotificationCounter = 0;
+ mFirstNotificationMillis = 0;
+ }
+ if (mNotificationCounter >= mDailyLimit) {
+ return true;
+ }
+ mNotificationCounter++;
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index d487bd0..9c48aee 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -28,14 +28,21 @@
import android.net.NetworkState;
import android.os.Handler;
import android.os.Messenger;
+import android.os.SystemClock;
+import android.util.Log;
import android.util.SparseArray;
import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.WakeupMessage;
import com.android.server.ConnectivityService;
import com.android.server.connectivity.NetworkMonitor;
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
/**
* A bag class used by ConnectivityService for holding a collection of most recent
@@ -106,6 +113,7 @@
// a NetworkRequest, ConnectivityService will cancel the future disconnection of the NetworkAgent's
// AsyncChannel, and the network is no longer considered "lingering".
public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
+
public NetworkInfo networkInfo;
// This Network object should always be used if possible, so as to encourage reuse of the
// enclosed socket factory and connection pool. Avoid creating other Network objects.
@@ -130,12 +138,14 @@
public boolean everValidated;
// The result of the last validation attempt on this network (true if validated, false if not).
- // This bit exists only because we never unvalidate a network once it's been validated, and that
- // is because the network scoring and revalidation code does not (may not?) deal properly with
- // networks becoming unvalidated.
- // TODO: Fix the network scoring code, remove this, and rename everValidated to validated.
public boolean lastValidated;
+ // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
+ // system is configured not to do this for certain networks, e.g., if the
+ // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
+ // Settings.Global.NETWORK_AVOID_BAD_WIFI.
+ public boolean avoidUnvalidated;
+
// Whether a captive portal was ever detected on this network.
// This is a sticky bit; once set it is never cleared.
public boolean everCaptivePortalDetected;
@@ -143,12 +153,69 @@
// Whether a captive portal was found during the last network validation attempt.
public boolean lastCaptivePortalDetected;
- // Indicates whether the network is lingering. Networks are lingered when they become unneeded
- // as a result of their NetworkRequests being satisfied by a different network, so as to allow
- // communication to wrap up before the network is taken down. This usually only happens to the
- // default network. Lingering ends with either the linger timeout expiring and the network
- // being taken down, or the network satisfying a request again.
- public boolean lingering;
+ // Networks are lingered when they become unneeded as a result of their NetworkRequests being
+ // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
+ // network is taken down. This usually only happens to the default network. Lingering ends with
+ // either the linger timeout expiring and the network being taken down, or the network
+ // satisfying a request again.
+ public static class LingerTimer implements Comparable<LingerTimer> {
+ public final NetworkRequest request;
+ public final long expiryMs;
+
+ public LingerTimer(NetworkRequest request, long expiryMs) {
+ this.request = request;
+ this.expiryMs = expiryMs;
+ }
+ public boolean equals(Object o) {
+ if (!(o instanceof LingerTimer)) return false;
+ LingerTimer other = (LingerTimer) o;
+ return (request.requestId == other.request.requestId) && (expiryMs == other.expiryMs);
+ }
+ public int hashCode() {
+ return Objects.hash(request.requestId, expiryMs);
+ }
+ public int compareTo(LingerTimer other) {
+ return (expiryMs != other.expiryMs) ?
+ Long.compare(expiryMs, other.expiryMs) :
+ Integer.compare(request.requestId, other.request.requestId);
+ }
+ public String toString() {
+ return String.format("%s, expires %dms", request.toString(),
+ expiryMs - SystemClock.elapsedRealtime());
+ }
+ }
+
+ /**
+ * Inform ConnectivityService that the network LINGER period has
+ * expired.
+ * obj = this NetworkAgentInfo
+ */
+ public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001;
+
+ // All linger timers for this network, sorted by expiry time. A linger timer is added whenever
+ // a request is moved to a network with a better score, regardless of whether the network is or
+ // was lingering or not.
+ // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g.,
+ // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire.
+ private final SortedSet<LingerTimer> mLingerTimers = new TreeSet<>();
+
+ // For fast lookups. Indexes into mLingerTimers by request ID.
+ private final SparseArray<LingerTimer> mLingerTimerForRequest = new SparseArray<>();
+
+ // Linger expiry timer. Armed whenever mLingerTimers is non-empty, regardless of whether the
+ // network is lingering or not. Always set to the expiry of the LingerTimer that expires last.
+ // When the timer fires, all linger state is cleared, and if the network has no requests, it is
+ // torn down.
+ private WakeupMessage mLingerMessage;
+
+ // Linger expiry. Holds the expiry time of the linger timer, or 0 if the timer is not armed.
+ private long mLingerExpiryMs;
+
+ // Whether the network is lingering or not. Must be maintained separately from the above because
+ // it depends on the state of other networks and requests, which only ConnectivityService knows.
+ // (Example: we don't linger a network if it would become the best for a NetworkRequest if it
+ // validated).
+ private boolean mLingering;
// This represents the last score received from the NetworkAgent.
private int currentScore;
@@ -162,11 +229,11 @@
private static final int MAXIMUM_NETWORK_SCORE = 100;
// The list of NetworkRequests being satisfied by this Network.
- public final SparseArray<NetworkRequest> networkRequests = new SparseArray<NetworkRequest>();
+ private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
// The list of NetworkRequests that this Network previously satisfied with the highest
// score. A non-empty list indicates that if this Network was validated it is lingered.
- // NOTE: This list is only used for debugging.
- public final ArrayList<NetworkRequest> networkLingered = new ArrayList<NetworkRequest>();
+ // How many of the satisfied requests are actual requests and not listens.
+ private int mNumRequestNetworkRequests = 0;
public final Messenger messenger;
public final AsyncChannel asyncChannel;
@@ -174,6 +241,12 @@
// Used by ConnectivityService to keep track of 464xlat.
public Nat464Xlat clatd;
+ private static final String TAG = ConnectivityService.class.getSimpleName();
+ private static final boolean VDBG = false;
+ private final ConnectivityService mConnService;
+ private final Context mContext;
+ private final Handler mHandler;
+
public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info,
LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler,
NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) {
@@ -184,22 +257,74 @@
linkProperties = lp;
networkCapabilities = nc;
currentScore = score;
- networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
+ mConnService = connService;
+ mContext = context;
+ mHandler = handler;
+ networkMonitor = mConnService.createNetworkMonitor(context, handler, this, defaultRequest);
networkMisc = misc;
}
+ // Functions for manipulating the requests satisfied by this network.
+ //
+ // These functions must only called on ConnectivityService's main thread.
+
/**
* Add {@code networkRequest} to this network as it's satisfied by this network.
- * NOTE: This function must only be called on ConnectivityService's main thread.
* @return true if {@code networkRequest} was added or false if {@code networkRequest} was
* already present.
*/
public boolean addRequest(NetworkRequest networkRequest) {
- if (networkRequests.get(networkRequest.requestId) == networkRequest) return false;
- networkRequests.put(networkRequest.requestId, networkRequest);
+ NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
+ if (existing == networkRequest) return false;
+ if (existing != null && existing.isRequest()) mNumRequestNetworkRequests--;
+ mNetworkRequests.put(networkRequest.requestId, networkRequest);
+ if (networkRequest.isRequest()) mNumRequestNetworkRequests++;
return true;
}
+ /**
+ * Remove the specified request from this network.
+ */
+ public void removeRequest(int requestId) {
+ NetworkRequest existing = mNetworkRequests.get(requestId);
+ if (existing == null) return;
+ mNetworkRequests.remove(requestId);
+ if (existing.isRequest()) {
+ mNumRequestNetworkRequests--;
+ unlingerRequest(existing);
+ }
+ }
+
+ /**
+ * Returns whether this network is currently satisfying the request with the specified ID.
+ */
+ public boolean isSatisfyingRequest(int id) {
+ return mNetworkRequests.get(id) != null;
+ }
+
+ /**
+ * Returns the request at the specified position in the list of requests satisfied by this
+ * network.
+ */
+ public NetworkRequest requestAt(int index) {
+ return mNetworkRequests.valueAt(index);
+ }
+
+ /**
+ * Returns the number of requests currently satisfied by this network for which
+ * {@link android.net.NetworkRequest#isRequest} returns {@code true}.
+ */
+ public int numRequestNetworkRequests() {
+ return mNumRequestNetworkRequests;
+ }
+
+ /**
+ * Returns the number of requests of any type currently satisfied by this network.
+ */
+ public int numNetworkRequests() {
+ return mNetworkRequests.size();
+ }
+
// Does this network satisfy request?
public boolean satisfies(NetworkRequest request) {
return created &&
@@ -232,16 +357,22 @@
}
int score = currentScore;
- // Use NET_CAPABILITY_VALIDATED here instead of lastValidated, this allows
- // ConnectivityService.updateCapabilities() to compute the old score prior to updating
- // networkCapabilities (with a potentially different validated state).
- if (!networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) && !pretendValidated) {
+ if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty()) {
score -= UNVALIDATED_SCORE_PENALTY;
}
if (score < 0) score = 0;
return score;
}
+ // Return true on devices configured to ignore score penalty for wifi networks
+ // that become unvalidated (b/31075769).
+ private boolean ignoreWifiUnvalidationPenalty() {
+ boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+ networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ boolean avoidBadWifi = mConnService.avoidBadWifi() || avoidUnvalidated;
+ return isWifi && !avoidBadWifi && everValidated;
+ }
+
// Get the current score for this Network. This may be modified from what the
// NetworkAgent sent, as it has modifiers applied to it.
public int getCurrentScore() {
@@ -269,13 +400,103 @@
}
}
+ /**
+ * Sets the specified request to linger on this network for the specified time. Called by
+ * ConnectivityService when the request is moved to another network with a higher score.
+ */
+ public void lingerRequest(NetworkRequest request, long now, long duration) {
+ if (mLingerTimerForRequest.get(request.requestId) != null) {
+ // Cannot happen. Once a request is lingering on a particular network, we cannot
+ // re-linger it unless that network becomes the best for that request again, in which
+ // case we should have unlingered it.
+ Log.wtf(TAG, this.name() + ": request " + request.requestId + " already lingered");
+ }
+ final long expiryMs = now + duration;
+ LingerTimer timer = new LingerTimer(request, expiryMs);
+ if (VDBG) Log.d(TAG, "Adding LingerTimer " + timer + " to " + this.name());
+ mLingerTimers.add(timer);
+ mLingerTimerForRequest.put(request.requestId, timer);
+ }
+
+ /**
+ * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+ * Returns true if the given request was lingering on this network, false otherwise.
+ */
+ public boolean unlingerRequest(NetworkRequest request) {
+ LingerTimer timer = mLingerTimerForRequest.get(request.requestId);
+ if (timer != null) {
+ if (VDBG) Log.d(TAG, "Removing LingerTimer " + timer + " from " + this.name());
+ mLingerTimers.remove(timer);
+ mLingerTimerForRequest.remove(request.requestId);
+ return true;
+ }
+ return false;
+ }
+
+ public long getLingerExpiry() {
+ return mLingerExpiryMs;
+ }
+
+ public void updateLingerTimer() {
+ long newExpiry = mLingerTimers.isEmpty() ? 0 : mLingerTimers.last().expiryMs;
+ if (newExpiry == mLingerExpiryMs) return;
+
+ // Even if we're going to reschedule the timer, cancel it first. This is because the
+ // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
+ // never call its callback (handleLingerComplete), even if it has already fired.
+ // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
+ // has already been dispatched, rescheduling to some time in the future it won't stop it
+ // from calling its callback immediately.
+ if (mLingerMessage != null) {
+ mLingerMessage.cancel();
+ mLingerMessage = null;
+ }
+
+ if (newExpiry > 0) {
+ mLingerMessage = mConnService.makeWakeupMessage(
+ mContext, mHandler,
+ "NETWORK_LINGER_COMPLETE." + network.netId,
+ EVENT_NETWORK_LINGER_COMPLETE, this);
+ mLingerMessage.schedule(newExpiry);
+ }
+
+ mLingerExpiryMs = newExpiry;
+ }
+
+ public void linger() {
+ mLingering = true;
+ }
+
+ public void unlinger() {
+ mLingering = false;
+ }
+
+ public boolean isLingering() {
+ return mLingering;
+ }
+
+ public void clearLingerState() {
+ if (mLingerMessage != null) {
+ mLingerMessage.cancel();
+ mLingerMessage = null;
+ }
+ mLingerTimers.clear();
+ mLingerTimerForRequest.clear();
+ updateLingerTimer(); // Sets mLingerExpiryMs, cancels and nulls out mLingerMessage.
+ mLingering = false;
+ }
+
+ public void dumpLingerTimers(PrintWriter pw) {
+ for (LingerTimer timer : mLingerTimers) { pw.println(timer); }
+ }
+
public String toString() {
return "NetworkAgentInfo{ ni{" + networkInfo + "} " +
"network{" + network + "} nethandle{" + network.getNetworkHandle() + "} " +
"lp{" + linkProperties + "} " +
"nc{" + networkCapabilities + "} Score{" + getCurrentScore() + "} " +
"everValidated{" + everValidated + "} lastValidated{" + lastValidated + "} " +
- "created{" + created + "} lingering{" + lingering + "} " +
+ "created{" + created + "} lingering{" + isLingering() + "} " +
"explicitlySelected{" + networkMisc.explicitlySelected + "} " +
"acceptUnvalidated{" + networkMisc.acceptUnvalidated + "} " +
"everCaptivePortalDetected{" + everCaptivePortalDetected + "} " +
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
new file mode 100644
index 0000000..f7b01be
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.widget.Toast;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+import android.util.Slog;
+
+import com.android.internal.R;
+
+import static android.net.NetworkCapabilities.*;
+
+
+public class NetworkNotificationManager {
+
+ public static enum NotificationType { SIGN_IN, NO_INTERNET, LOST_INTERNET, NETWORK_SWITCH };
+
+ private static final String NOTIFICATION_ID = "Connectivity.Notification";
+
+ private static final String TAG = NetworkNotificationManager.class.getSimpleName();
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private final Context mContext;
+ private final TelephonyManager mTelephonyManager;
+ private final NotificationManager mNotificationManager;
+
+ public NetworkNotificationManager(Context c, TelephonyManager t, NotificationManager n) {
+ mContext = c;
+ mTelephonyManager = t;
+ mNotificationManager = n;
+ }
+
+ // TODO: deal more gracefully with multi-transport networks.
+ private static int getFirstTransportType(NetworkAgentInfo nai) {
+ for (int i = 0; i < 64; i++) {
+ if (nai.networkCapabilities.hasTransport(i)) return i;
+ }
+ return -1;
+ }
+
+ private static String getTransportName(int transportType) {
+ Resources r = Resources.getSystem();
+ String[] networkTypes = r.getStringArray(R.array.network_switch_type_name);
+ try {
+ return networkTypes[transportType];
+ } catch (IndexOutOfBoundsException e) {
+ return r.getString(R.string.network_switch_type_name_unknown);
+ }
+ }
+
+ private static int getIcon(int transportType) {
+ return (transportType == TRANSPORT_WIFI) ?
+ R.drawable.stat_notify_wifi_in_range : // TODO: Distinguish ! from ?.
+ R.drawable.stat_notify_rssi_in_range;
+ }
+
+ /**
+ * Show or hide network provisioning notifications.
+ *
+ * We use notifications for two purposes: to notify that a network requires sign in
+ * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
+ * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
+ * particular network we can display the notification type that was most recently requested.
+ * So for example if a captive portal fails to reply within a few seconds of connecting, we
+ * might first display NO_INTERNET, and then when the captive portal check completes, display
+ * SIGN_IN.
+ *
+ * @param id an identifier that uniquely identifies this notification. This must match
+ * between show and hide calls. We use the NetID value but for legacy callers
+ * we concatenate the range of types with the range of NetIDs.
+ * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
+ * or LOST_INTERNET notification, this is the network we're connecting to. For a
+ * NETWORK_SWITCH notification it's the network that we switched from. When this network
+ * disconnects the notification is removed.
+ * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
+ * in all other cases. Only used to determine the text of the notification.
+ */
+ public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
+ NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
+ int transportType;
+ String extraInfo;
+ if (nai != null) {
+ transportType = getFirstTransportType(nai);
+ extraInfo = nai.networkInfo.getExtraInfo();
+ // Only notify for Internet-capable networks.
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
+ } else {
+ // Legacy notifications.
+ transportType = TRANSPORT_CELLULAR;
+ extraInfo = null;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "showNotification " + notifyType
+ + " transportType=" + getTransportName(transportType)
+ + " extraInfo=" + extraInfo + " highPriority=" + highPriority);
+ }
+
+ Resources r = Resources.getSystem();
+ CharSequence title;
+ CharSequence details;
+ int icon = getIcon(transportType);
+ if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.wifi_no_internet, 0);
+ details = r.getString(R.string.wifi_no_internet_detailed);
+ } else if (notifyType == NotificationType.LOST_INTERNET &&
+ transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.wifi_no_internet, 0);
+ details = r.getString(R.string.wifi_no_internet_detailed);
+ } else if (notifyType == NotificationType.SIGN_IN) {
+ switch (transportType) {
+ case TRANSPORT_WIFI:
+ title = r.getString(R.string.wifi_available_sign_in, 0);
+ details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
+ break;
+ case TRANSPORT_CELLULAR:
+ title = r.getString(R.string.network_available_sign_in, 0);
+ // TODO: Change this to pull from NetworkInfo once a printable
+ // name has been added to it
+ details = mTelephonyManager.getNetworkOperatorName();
+ break;
+ default:
+ title = r.getString(R.string.network_available_sign_in, 0);
+ details = r.getString(R.string.network_available_sign_in_detailed, extraInfo);
+ break;
+ }
+ } else if (notifyType == NotificationType.NETWORK_SWITCH) {
+ String fromTransport = getTransportName(transportType);
+ String toTransport = getTransportName(getFirstTransportType(switchToNai));
+ title = r.getString(R.string.network_switch_metered, toTransport);
+ details = r.getString(R.string.network_switch_metered_detail, toTransport,
+ fromTransport);
+ } else {
+ Slog.wtf(TAG, "Unknown notification type " + notifyType + "on network transport "
+ + getTransportName(transportType));
+ return;
+ }
+
+ Notification.Builder builder = new Notification.Builder(mContext)
+ .setWhen(System.currentTimeMillis())
+ .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
+ .setSmallIcon(icon)
+ .setAutoCancel(true)
+ .setTicker(title)
+ .setColor(mContext.getColor(
+ com.android.internal.R.color.system_notification_accent_color))
+ .setContentTitle(title)
+ .setContentIntent(intent)
+ .setLocalOnly(true)
+ .setPriority(highPriority ?
+ Notification.PRIORITY_HIGH :
+ Notification.PRIORITY_DEFAULT)
+ .setDefaults(highPriority ? Notification.DEFAULT_ALL : 0)
+ .setOnlyAlertOnce(true);
+
+ if (notifyType == NotificationType.NETWORK_SWITCH) {
+ builder.setStyle(new Notification.BigTextStyle().bigText(details));
+ } else {
+ builder.setContentText(details);
+ }
+
+ Notification notification = builder.build();
+
+ try {
+ mNotificationManager.notifyAsUser(NOTIFICATION_ID, id, notification, UserHandle.ALL);
+ } catch (NullPointerException npe) {
+ Slog.d(TAG, "setNotificationVisible: visible notificationManager npe=" + npe);
+ }
+ }
+
+ public void clearNotification(int id) {
+ if (DBG) {
+ Slog.d(TAG, "clearNotification id=" + id);
+ }
+ try {
+ mNotificationManager.cancelAsUser(NOTIFICATION_ID, id, UserHandle.ALL);
+ } catch (NullPointerException npe) {
+ Slog.d(TAG, "setNotificationVisible: cancel notificationManager npe=" + npe);
+ }
+ }
+
+ /**
+ * Legacy provisioning notifications coming directly from DcTracker.
+ */
+ public void setProvNotificationVisible(boolean visible, int id, String action) {
+ if (visible) {
+ Intent intent = new Intent(action);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
+ } else {
+ clearNotification(id);
+ }
+ }
+
+ public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ String fromTransport = getTransportName(getFirstTransportType(fromNai));
+ String toTransport = getTransportName(getFirstTransportType(toNai));
+ String text = mContext.getResources().getString(
+ R.string.network_switch_metered_toast, fromTransport, toTransport);
+ Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/services/core/java/com/android/server/connectivity/PermissionMonitor.java b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
index 22cefd1..7cd1b7b 100644
--- a/services/core/java/com/android/server/connectivity/PermissionMonitor.java
+++ b/services/core/java/com/android/server/connectivity/PermissionMonitor.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.CHANGE_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
@@ -65,10 +66,10 @@
private final BroadcastReceiver mIntentReceiver;
// Values are User IDs.
- private final Set<Integer> mUsers = new HashSet<Integer>();
+ private final Set<Integer> mUsers = new HashSet<>();
// Keys are App IDs. Values are true for SYSTEM permission and false for NETWORK permission.
- private final Map<Integer, Boolean> mApps = new HashMap<Integer, Boolean>();
+ private final Map<Integer, Boolean> mApps = new HashMap<>();
public PermissionMonitor(Context context, INetworkManagementService netd) {
mContext = context;
@@ -126,14 +127,14 @@
}
boolean isNetwork = hasNetworkPermission(app);
- boolean isSystem = hasSystemPermission(app);
+ boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
- if (isNetwork || isSystem) {
+ if (isNetwork || hasRestrictedPermission) {
Boolean permission = mApps.get(uid);
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
if (permission == null || permission == NETWORK) {
- mApps.put(uid, isSystem);
+ mApps.put(uid, hasRestrictedPermission);
}
}
}
@@ -164,12 +165,13 @@
return hasPermission(app, CHANGE_NETWORK_STATE);
}
- private boolean hasSystemPermission(PackageInfo app) {
+ private boolean hasRestrictedNetworkPermission(PackageInfo app) {
int flags = app.applicationInfo != null ? app.applicationInfo.flags : 0;
if ((flags & FLAG_SYSTEM) != 0 || (flags & FLAG_UPDATED_SYSTEM_APP) != 0) {
return true;
}
- return hasPermission(app, CONNECTIVITY_INTERNAL);
+ return hasPermission(app, CONNECTIVITY_INTERNAL)
+ || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
}
private int[] toIntArray(List<Integer> list) {
@@ -181,8 +183,8 @@
}
private void update(Set<Integer> users, Map<Integer, Boolean> apps, boolean add) {
- List<Integer> network = new ArrayList<Integer>();
- List<Integer> system = new ArrayList<Integer>();
+ List<Integer> network = new ArrayList<>();
+ List<Integer> system = new ArrayList<>();
for (Entry<Integer, Boolean> app : apps.entrySet()) {
List<Integer> list = app.getValue() ? system : network;
for (int user : users) {
@@ -209,7 +211,7 @@
}
mUsers.add(user);
- Set<Integer> users = new HashSet<Integer>();
+ Set<Integer> users = new HashSet<>();
users.add(user);
update(users, mApps, true);
}
@@ -221,7 +223,7 @@
}
mUsers.remove(user);
- Set<Integer> users = new HashSet<Integer>();
+ Set<Integer> users = new HashSet<>();
users.add(user);
update(users, mApps, false);
}
@@ -235,16 +237,16 @@
try {
PackageInfo app = mPackageManager.getPackageInfo(appName, GET_PERMISSIONS);
boolean isNetwork = hasNetworkPermission(app);
- boolean isSystem = hasSystemPermission(app);
- if (isNetwork || isSystem) {
+ boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+ if (isNetwork || hasRestrictedPermission) {
Boolean permission = mApps.get(appUid);
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
if (permission == null || permission == NETWORK) {
- mApps.put(appUid, isSystem);
+ mApps.put(appUid, hasRestrictedPermission);
- Map<Integer, Boolean> apps = new HashMap<Integer, Boolean>();
- apps.put(appUid, isSystem);
+ Map<Integer, Boolean> apps = new HashMap<>();
+ apps.put(appUid, hasRestrictedPermission);
update(mUsers, apps, true);
}
}
@@ -260,7 +262,7 @@
}
mApps.remove(appUid);
- Map<Integer, Boolean> apps = new HashMap<Integer, Boolean>();
+ Map<Integer, Boolean> apps = new HashMap<>();
apps.put(appUid, NETWORK); // doesn't matter which permission we pick here
update(mUsers, apps, false);
}
diff --git a/services/net/java/android/net/util/AvoidBadWifiTracker.java b/services/net/java/android/net/util/AvoidBadWifiTracker.java
new file mode 100644
index 0000000..c14e811
--- /dev/null
+++ b/services/net/java/android/net/util/AvoidBadWifiTracker.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
+
+import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+
+/**
+ * A class to encapsulate management of the "Smart Networking" capability of
+ * avoiding bad Wi-Fi when, for example upstream connectivity is lost or
+ * certain critical link failures occur.
+ *
+ * This enables the device to switch to another form of connectivity, like
+ * mobile, if it's available and working.
+ *
+ * The Runnable |cb|, if given, is called on the supplied Handler's thread
+ * whether the computed "avoid bad wifi" value changes.
+ *
+ * Disabling this reverts the device to a level of networking sophistication
+ * circa 2012-13 by disabling disparate code paths each of which contribute to
+ * maintaining continuous, working Internet connectivity.
+ *
+ * @hide
+ */
+public class AvoidBadWifiTracker {
+ private static String TAG = AvoidBadWifiTracker.class.getSimpleName();
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final Runnable mReevaluateRunnable;
+ private final SettingObserver mSettingObserver;
+ private volatile boolean mAvoidBadWifi = true;
+
+ public AvoidBadWifiTracker(Context ctx, Handler handler) {
+ this(ctx, handler, null);
+ }
+
+ public AvoidBadWifiTracker(Context ctx, Handler handler, Runnable cb) {
+ mContext = ctx;
+ mHandler = handler;
+ mReevaluateRunnable = () -> { if (update() && cb != null) cb.run(); };
+ mSettingObserver = new SettingObserver();
+
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+ mContext.registerReceiverAsUser(new BroadcastReceiver() {
+ public void onReceive(Context context, Intent intent) {
+ reevaluate();
+ }
+ }, UserHandle.ALL, intentFilter, null, null);
+
+ update();
+ }
+
+ public boolean currentValue() {
+ return mAvoidBadWifi;
+ }
+
+ /**
+ * Whether the device or carrier configuration disables avoiding bad wifi by default.
+ */
+ public boolean configRestrictsAvoidBadWifi() {
+ return (mContext.getResources().getInteger(R.integer.config_networkAvoidBadWifi) == 0);
+ }
+
+ /**
+ * Whether we should display a notification when wifi becomes unvalidated.
+ */
+ public boolean shouldNotifyWifiUnvalidated() {
+ return configRestrictsAvoidBadWifi() && getSettingsValue() == null;
+ }
+
+ public String getSettingsValue() {
+ final ContentResolver resolver = mContext.getContentResolver();
+ return Settings.Global.getString(resolver, NETWORK_AVOID_BAD_WIFI);
+ }
+
+ @VisibleForTesting
+ public void reevaluate() {
+ mHandler.post(mReevaluateRunnable);
+ }
+
+ public boolean update() {
+ final boolean settingAvoidBadWifi = "1".equals(getSettingsValue());
+ final boolean prev = mAvoidBadWifi;
+ mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi();
+ return mAvoidBadWifi != prev;
+ }
+
+ private class SettingObserver extends ContentObserver {
+ private final Uri mUri = Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI);
+
+ public SettingObserver() {
+ super(null);
+ final ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(mUri, false, this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Slog.wtf(TAG, "Should never be reached.");
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (!mUri.equals(uri)) return;
+ reevaluate();
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index 4fae4a7..a656acc 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -17,6 +17,7 @@
package com.android.server;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.ConnectivityManager.getNetworkTypeName;
@@ -24,8 +25,10 @@
import static org.mockito.Mockito.mock;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
@@ -49,6 +52,8 @@
import android.net.NetworkMisc;
import android.net.NetworkRequest;
import android.net.RouteInfo;
+import android.net.metrics.IpConnectivityLog;
+import android.net.util.AvoidBadWifiTracker;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
@@ -61,12 +66,15 @@
import android.os.MessageQueue.IdleHandler;
import android.os.Process;
import android.os.SystemClock;
+import android.provider.Settings;
import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.util.LogPrinter;
+import com.android.internal.util.FakeSettingsProvider;
import com.android.internal.util.WakeupMessage;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkMonitor;
@@ -75,6 +83,8 @@
import java.net.InetAddress;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -90,12 +100,14 @@
private static final String TAG = "ConnectivityServiceTest";
private static final int TIMEOUT_MS = 500;
+ private static final int TEST_LINGER_DELAY_MS = 120;
private BroadcastInterceptingContext mServiceContext;
private WrappedConnectivityService mService;
private WrappedConnectivityManager mCm;
private MockNetworkAgent mWiFiNetworkAgent;
private MockNetworkAgent mCellNetworkAgent;
+ private MockNetworkAgent mEthernetNetworkAgent;
// This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods
// do not go through ConnectivityService but talk to netd directly, so they don't automatically
@@ -118,27 +130,25 @@
}
private class MockContext extends BroadcastInterceptingContext {
+ private final MockContentResolver mContentResolver;
+
MockContext(Context base) {
super(base);
+ mContentResolver = new MockContentResolver();
+ mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
}
@Override
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- // PendingIntents sent by the AlarmManager are not intercepted by
- // BroadcastInterceptingContext so we must really register the receiver.
- // This shouldn't effect the real NetworkMonitors as the action contains a random token.
- if (filter.getAction(0).startsWith("android.net.netmon.lingerExpired")) {
- return getBaseContext().registerReceiver(receiver, filter);
- } else {
- return super.registerReceiver(receiver, filter);
- }
- }
-
- @Override
- public Object getSystemService (String name) {
- if (name == Context.CONNECTIVITY_SERVICE) return mCm;
+ public Object getSystemService(String name) {
+ if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm;
+ if (Context.NOTIFICATION_SERVICE.equals(name)) return mock(NotificationManager.class);
return super.getSystemService(name);
}
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
}
/**
@@ -242,6 +252,9 @@
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.addTransportType(transport);
switch (transport) {
+ case TRANSPORT_ETHERNET:
+ mScore = 70;
+ break;
case TRANSPORT_WIFI:
mScore = 60;
break;
@@ -303,6 +316,11 @@
mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
}
+ public void removeCapability(int capability) {
+ mNetworkCapabilities.removeCapability(capability);
+ mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+ }
+
public void setSignalStrength(int signalStrength) {
mNetworkCapabilities.setSignalStrength(signalStrength);
mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
@@ -318,7 +336,8 @@
* @param validated Indicate if network should pretend to be validated.
*/
public void connect(boolean validated) {
- assertEquals(mNetworkInfo.getDetailedState(), DetailedState.IDLE);
+ assertEquals("MockNetworkAgents can only be connected once",
+ mNetworkInfo.getDetailedState(), DetailedState.IDLE);
assertFalse(mNetworkCapabilities.hasCapability(NET_CAPABILITY_INTERNET));
NetworkCallback callback = null;
@@ -536,6 +555,11 @@
super(context, handler, cmdName, cmd);
}
+ public FakeWakeupMessage(Context context, Handler handler, String cmdName, int cmd,
+ int arg1, int arg2, Object obj) {
+ super(context, handler, cmdName, cmd, arg1, arg2, obj);
+ }
+
@Override
public void schedule(long when) {
long delayMs = when - SystemClock.elapsedRealtime();
@@ -544,12 +568,13 @@
fail("Attempting to send msg more than " + UNREASONABLY_LONG_WAIT +
"ms into the future: " + delayMs);
}
- mHandler.sendEmptyMessageDelayed(mCmd, delayMs);
+ Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj);
+ mHandler.sendMessageDelayed(msg, delayMs);
}
@Override
public void cancel() {
- mHandler.removeMessages(mCmd);
+ mHandler.removeMessages(mCmd, mObj);
}
@Override
@@ -565,28 +590,39 @@
public String gen204ProbeRedirectUrl = null;
public WrappedNetworkMonitor(Context context, Handler handler,
- NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest) {
- super(context, handler, networkAgentInfo, defaultRequest);
+ NetworkAgentInfo networkAgentInfo, NetworkRequest defaultRequest,
+ IpConnectivityLog log) {
+ super(context, handler, networkAgentInfo, defaultRequest, log);
}
@Override
protected CaptivePortalProbeResult isCaptivePortal() {
return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl);
}
+ }
+
+ private class WrappedAvoidBadWifiTracker extends AvoidBadWifiTracker {
+ public boolean configRestrictsAvoidBadWifi;
+
+ public WrappedAvoidBadWifiTracker(Context c, Handler h, Runnable r) {
+ super(c, h, r);
+ }
@Override
- protected WakeupMessage makeWakeupMessage(
- Context context, Handler handler, String cmdName, int cmd) {
- return new FakeWakeupMessage(context, handler, cmdName, cmd);
+ public boolean configRestrictsAvoidBadWifi() {
+ return configRestrictsAvoidBadWifi;
}
}
private class WrappedConnectivityService extends ConnectivityService {
+ public WrappedAvoidBadWifiTracker wrappedAvoidBadWifiTracker;
private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
public WrappedConnectivityService(Context context, INetworkManagementService netManager,
- INetworkStatsService statsService, INetworkPolicyManager policyManager) {
- super(context, netManager, statsService, policyManager);
+ INetworkStatsService statsService, INetworkPolicyManager policyManager,
+ IpConnectivityLog log) {
+ super(context, netManager, statsService, policyManager, log);
+ mLingerDelayMs = TEST_LINGER_DELAY_MS;
}
@Override
@@ -624,12 +660,29 @@
@Override
public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
NetworkAgentInfo nai, NetworkRequest defaultRequest) {
- final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(context, handler, nai,
- defaultRequest);
+ final WrappedNetworkMonitor monitor = new WrappedNetworkMonitor(
+ context, handler, nai, defaultRequest, mock(IpConnectivityLog.class));
mLastCreatedNetworkMonitor = monitor;
return monitor;
}
+ @Override
+ public AvoidBadWifiTracker createAvoidBadWifiTracker(
+ Context c, Handler h, Runnable r) {
+ final WrappedAvoidBadWifiTracker tracker = new WrappedAvoidBadWifiTracker(c, h, r);
+ return tracker;
+ }
+
+ public WrappedAvoidBadWifiTracker getAvoidBadWifiTracker() {
+ return (WrappedAvoidBadWifiTracker) mAvoidBadWifiTracker;
+ }
+
+ @Override
+ public WakeupMessage makeWakeupMessage(
+ Context context, Handler handler, String cmdName, int cmd, Object obj) {
+ return new FakeWakeupMessage(context, handler, cmdName, cmd, 0, 0, obj);
+ }
+
public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
return mLastCreatedNetworkMonitor;
}
@@ -641,7 +694,6 @@
public void waitForIdle() {
waitForIdle(TIMEOUT_MS);
}
-
}
private interface Criteria {
@@ -675,8 +727,6 @@
public void setUp() throws Exception {
super.setUp();
- NetworkMonitor.SetDefaultLingerTime(120);
-
// InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
// http://b/25897652 .
if (Looper.myLooper() == null) {
@@ -687,21 +737,31 @@
mService = new WrappedConnectivityService(mServiceContext,
mock(INetworkManagementService.class),
mock(INetworkStatsService.class),
- mock(INetworkPolicyManager.class));
+ mock(INetworkPolicyManager.class),
+ mock(IpConnectivityLog.class));
mService.systemReady();
mCm = new WrappedConnectivityManager(getContext(), mService);
mCm.bindProcessToNetwork(null);
}
+ public void tearDown() throws Exception {
+ if (mCellNetworkAgent != null) { mCellNetworkAgent.disconnect(); }
+ if (mWiFiNetworkAgent != null) { mWiFiNetworkAgent.disconnect(); }
+ mCellNetworkAgent = mWiFiNetworkAgent = null;
+ super.tearDown();
+ }
+
private int transportToLegacyType(int transport) {
switch (transport) {
+ case TRANSPORT_ETHERNET:
+ return TYPE_ETHERNET;
case TRANSPORT_WIFI:
return TYPE_WIFI;
case TRANSPORT_CELLULAR:
return TYPE_MOBILE;
default:
- throw new IllegalStateException("Unknown transport" + transport);
+ throw new IllegalStateException("Unknown transport " + transport);
}
}
@@ -911,8 +971,6 @@
mWiFiNetworkAgent.adjustScore(11);
waitFor(cv);
verifyActiveNetwork(TRANSPORT_WIFI);
- mCellNetworkAgent.disconnect();
- mWiFiNetworkAgent.disconnect();
}
@LargeTest
@@ -984,8 +1042,6 @@
assertFalse(mCm.getNetworkCapabilities(mWiFiNetworkAgent.getNetwork()).hasCapability(
NET_CAPABILITY_VALIDATED));
verifyActiveNetwork(TRANSPORT_WIFI);
- mCellNetworkAgent.disconnect();
- mWiFiNetworkAgent.disconnect();
}
@LargeTest
@@ -1012,13 +1068,13 @@
assertFalse(mCm.getNetworkCapabilities(mCellNetworkAgent.getNetwork()).hasCapability(
NET_CAPABILITY_VALIDATED));
verifyActiveNetwork(TRANSPORT_WIFI);
- mCellNetworkAgent.disconnect();
- mWiFiNetworkAgent.disconnect();
}
enum CallbackState {
NONE,
AVAILABLE,
+ NETWORK_CAPABILITIES,
+ LINK_PROPERTIES,
LOSING,
LOST
}
@@ -1029,59 +1085,98 @@
* received. assertNoCallback may be called at any time.
*/
private class TestNetworkCallback extends NetworkCallback {
- private final ConditionVariable mConditionVariable = new ConditionVariable();
- private CallbackState mLastCallback = CallbackState.NONE;
- private Network mLastNetwork;
+ // Chosen to be much less than the linger timeout. This ensures that we can distinguish
+ // between a LOST callback that arrives immediately and a LOST callback that arrives after
+ // the linger timeout.
+ private final static int TIMEOUT_MS = 50;
+ private class CallbackInfo {
+ public final CallbackState state;
+ public final Network network;
+ public Object arg;
+ public CallbackInfo(CallbackState s, Network n, Object o) {
+ state = s; network = n; arg = o;
+ }
+ public String toString() { return String.format("%s (%s)", state, network); }
+ public boolean equals(Object o) {
+ if (!(o instanceof CallbackInfo)) return false;
+ // Ignore timeMs, since it's unpredictable.
+ CallbackInfo other = (CallbackInfo) o;
+ return state == other.state && Objects.equals(network, other.network);
+ }
+ }
+ private final LinkedBlockingQueue<CallbackInfo> mCallbacks = new LinkedBlockingQueue<>();
+
+ protected void setLastCallback(CallbackState state, Network network, Object o) {
+ mCallbacks.offer(new CallbackInfo(state, network, o));
+ }
+
+ @Override
public void onAvailable(Network network) {
- assertEquals(CallbackState.NONE, mLastCallback);
- mLastCallback = CallbackState.AVAILABLE;
- mLastNetwork = network;
- mConditionVariable.open();
+ setLastCallback(CallbackState.AVAILABLE, network, null);
}
+ @Override
public void onLosing(Network network, int maxMsToLive) {
- assertEquals(CallbackState.NONE, mLastCallback);
- mLastCallback = CallbackState.LOSING;
- mLastNetwork = network;
- mConditionVariable.open();
+ setLastCallback(CallbackState.LOSING, network, maxMsToLive /* autoboxed int */);
}
+ @Override
public void onLost(Network network) {
- assertEquals(CallbackState.NONE, mLastCallback);
- mLastCallback = CallbackState.LOST;
- mLastNetwork = network;
- mConditionVariable.open();
+ setLastCallback(CallbackState.LOST, network, null);
}
- void expectCallback(CallbackState state) {
- expectCallback(state, null);
+ void expectCallback(CallbackState state, MockNetworkAgent mockAgent, int timeoutMs) {
+ CallbackInfo expected = new CallbackInfo(
+ state, (mockAgent != null) ? mockAgent.getNetwork() : null, 0);
+ CallbackInfo actual;
+ try {
+ actual = mCallbacks.poll(timeoutMs, TimeUnit.MILLISECONDS);
+ assertEquals("Unexpected callback:", expected, actual);
+ } catch (InterruptedException e) {
+ fail("Did not receive expected " + expected + " after " + TIMEOUT_MS + "ms");
+ actual = null; // Or the compiler can't tell it's never used uninitialized.
+ }
+ if (state == CallbackState.LOSING) {
+ String msg = String.format(
+ "Invalid linger time value %d, must be between %d and %d",
+ actual.arg, 0, TEST_LINGER_DELAY_MS);
+ int maxMsToLive = (Integer) actual.arg;
+ assertTrue(msg, 0 <= maxMsToLive && maxMsToLive <= TEST_LINGER_DELAY_MS);
+ }
}
void expectCallback(CallbackState state, MockNetworkAgent mockAgent) {
- waitFor(mConditionVariable);
- assertEquals(state, mLastCallback);
- if (mockAgent != null) {
- assertEquals(mockAgent.getNetwork(), mLastNetwork);
- }
- mLastCallback = CallbackState.NONE;
- mLastNetwork = null;
- mConditionVariable.close();
+ expectCallback(state, mockAgent, TIMEOUT_MS);
}
void assertNoCallback() {
- assertEquals(CallbackState.NONE, mLastCallback);
+ mService.waitForIdle();
+ CallbackInfo c = mCallbacks.peek();
+ assertNull("Unexpected callback: " + c, c);
+ }
+ }
+
+ // Can't be part of TestNetworkCallback because "cannot be declared static; static methods can
+ // only be declared in a static or top level type".
+ static void assertNoCallbacks(TestNetworkCallback ... callbacks) {
+ for (TestNetworkCallback c : callbacks) {
+ c.assertNoCallback();
}
}
@LargeTest
public void testStateChangeNetworkCallbacks() throws Exception {
+ final TestNetworkCallback genericNetworkCallback = new TestNetworkCallback();
final TestNetworkCallback wifiNetworkCallback = new TestNetworkCallback();
final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+ final NetworkRequest genericRequest = new NetworkRequest.Builder()
+ .clearCapabilities().build();
final NetworkRequest wifiRequest = new NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI).build();
final NetworkRequest cellRequest = new NetworkRequest.Builder()
.addTransportType(TRANSPORT_CELLULAR).build();
+ mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
@@ -1089,65 +1184,275 @@
ConditionVariable cv = waitForConnectivityBroadcasts(1);
mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
mCellNetworkAgent.connect(false);
- cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
- wifiNetworkCallback.assertNoCallback();
+ genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
waitFor(cv);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
// This should not trigger spurious onAvailable() callbacks, b/21762680.
mCellNetworkAgent.adjustScore(-1);
mService.waitForIdle();
- wifiNetworkCallback.assertNoCallback();
- cellNetworkCallback.assertNoCallback();
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
cv = waitForConnectivityBroadcasts(2);
mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
mWiFiNetworkAgent.connect(false);
- wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
- cellNetworkCallback.assertNoCallback();
+ genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
waitFor(cv);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
cv = waitForConnectivityBroadcasts(2);
mWiFiNetworkAgent.disconnect();
- wifiNetworkCallback.expectCallback(CallbackState.LOST);
+ genericNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ wifiNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
cellNetworkCallback.assertNoCallback();
waitFor(cv);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
cv = waitForConnectivityBroadcasts(1);
mCellNetworkAgent.disconnect();
- cellNetworkCallback.expectCallback(CallbackState.LOST);
- wifiNetworkCallback.assertNoCallback();
+ genericNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
waitFor(cv);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
// Test validated networks
mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
mCellNetworkAgent.connect(true);
- cellNetworkCallback.expectCallback(CallbackState.AVAILABLE);
- wifiNetworkCallback.assertNoCallback();
+ genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
// This should not trigger spurious onAvailable() callbacks, b/21762680.
mCellNetworkAgent.adjustScore(-1);
mService.waitForIdle();
- wifiNetworkCallback.assertNoCallback();
- cellNetworkCallback.assertNoCallback();
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
mWiFiNetworkAgent.connect(true);
- wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE);
- cellNetworkCallback.expectCallback(CallbackState.LOSING);
+ genericNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ genericNetworkCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ wifiNetworkCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ cellNetworkCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+ mWiFiNetworkAgent.disconnect();
+ genericNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ wifiNetworkCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+
+ mCellNetworkAgent.disconnect();
+ genericNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ assertNoCallbacks(genericNetworkCallback, wifiNetworkCallback, cellNetworkCallback);
+ }
+
+ @SmallTest
+ public void testMultipleLingering() {
+ NetworkRequest request = new NetworkRequest.Builder()
+ .clearCapabilities().addCapability(NET_CAPABILITY_NOT_METERED)
+ .build();
+ TestNetworkCallback callback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(request, callback);
+
+ TestNetworkCallback defaultCallback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(defaultCallback);
+
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mEthernetNetworkAgent = new MockNetworkAgent(TRANSPORT_ETHERNET);
+
+ mCellNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+ mWiFiNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+ mEthernetNetworkAgent.addCapability(NET_CAPABILITY_NOT_METERED);
+
+ mCellNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ mWiFiNetworkAgent.connect(true);
+ // We get AVAILABLE on wifi when wifi connects and satisfies our unmetered request.
+ // We then get LOSING when wifi validates and cell is outscored.
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ mEthernetNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mEthernetNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mEthernetNetworkAgent);
+ assertEquals(mEthernetNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ mEthernetNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mEthernetNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mEthernetNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+
+ for (int i = 0; i < 4; i++) {
+ MockNetworkAgent oldNetwork, newNetwork;
+ if (i % 2 == 0) {
+ mWiFiNetworkAgent.adjustScore(-15);
+ oldNetwork = mWiFiNetworkAgent;
+ newNetwork = mCellNetworkAgent;
+ } else {
+ mWiFiNetworkAgent.adjustScore(15);
+ oldNetwork = mCellNetworkAgent;
+ newNetwork = mWiFiNetworkAgent;
+
+ }
+ callback.expectCallback(CallbackState.LOSING, oldNetwork);
+ // TODO: should we send an AVAILABLE callback to newNetwork, to indicate that it is no
+ // longer lingering?
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, newNetwork);
+ assertEquals(newNetwork.getNetwork(), mCm.getActiveNetwork());
+ }
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ // Verify that if a network no longer satisfies a request, we send LOST and not LOSING, even
+ // if the network is still up.
+ mWiFiNetworkAgent.removeCapability(NET_CAPABILITY_NOT_METERED);
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+ // Wifi no longer satisfies our listen, which is for an unmetered network.
+ // But because its score is 55, it's still up (and the default network).
+ defaultCallback.assertNoCallback();
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ // Disconnect our test networks.
+ mWiFiNetworkAgent.disconnect();
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ mCellNetworkAgent.disconnect();
+ defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+
+ mCm.unregisterNetworkCallback(callback);
+ mService.waitForIdle();
+
+ // Check that a network is only lingered or torn down if it would not satisfy a request even
+ // if it validated.
+ request = new NetworkRequest.Builder().clearCapabilities().build();
+ callback = new TestNetworkCallback();
+
+ mCm.registerNetworkCallback(request, callback);
+
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(false); // Score: 10
+ callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ // Bring up wifi with a score of 20.
+ // Cell stays up because it would satisfy the default request if it validated.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(false); // Score: 20
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
mWiFiNetworkAgent.disconnect();
- wifiNetworkCallback.expectCallback(CallbackState.LOST);
- cellNetworkCallback.assertNoCallback();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+ // Bring up wifi with a score of 70.
+ // Cell is lingered because it would not satisfy any request, even if it validated.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.adjustScore(50);
+ mWiFiNetworkAgent.connect(false); // Score: 70
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ // Tear down wifi.
+ mWiFiNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ // Bring up wifi, then validate it. Previous versions would immediately tear down cell, but
+ // it's arguably correct to linger it, since it was the default network before it validated.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+
+ mWiFiNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
mCellNetworkAgent.disconnect();
- cellNetworkCallback.expectCallback(CallbackState.LOST);
- wifiNetworkCallback.assertNoCallback();
+ callback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+
+ // If a network is lingering, and we add and remove a request from it, resume lingering.
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+ NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ NetworkCallback noopCallback = new NetworkCallback();
+ mCm.requestNetwork(cellRequest, noopCallback);
+ // TODO: should this cause an AVAILABLE callback, to indicate that the network is no longer
+ // lingering?
+ mCm.unregisterNetworkCallback(noopCallback);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+ // Similar to the above: lingering can start even after the lingered request is removed.
+ // Disconnect wifi and switch to cell.
+ mWiFiNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+
+ // Cell is now the default network. Pin it with a cell-specific request.
+ noopCallback = new NetworkCallback(); // Can't reuse NetworkCallbacks. http://b/20701525
+ mCm.requestNetwork(cellRequest, noopCallback);
+
+ // Now connect wifi, and expect it to become the default network.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ // The default request is lingering on cell, but nothing happens to cell, and we send no
+ // callbacks for it, because it's kept up by cellRequest.
+ callback.assertNoCallback();
+ // Now unregister cellRequest and expect cell to start lingering.
+ mCm.unregisterNetworkCallback(noopCallback);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+
+ // Let linger run its course.
+ callback.assertNoCallback();
+ callback.expectCallback(CallbackState.LOST, mCellNetworkAgent,
+ TEST_LINGER_DELAY_MS /* timeoutMs */);
+
+ // Clean up.
+ mWiFiNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+ mCm.unregisterNetworkCallback(callback);
+ mCm.unregisterNetworkCallback(defaultCallback);
}
private void tryNetworkFactoryRequests(int capability) throws Exception {
@@ -1314,7 +1619,7 @@
mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
mCellNetworkAgent.addCapability(NET_CAPABILITY_MMS);
mCellNetworkAgent.connectWithoutInternet();
- networkCallback.expectCallback(CallbackState.AVAILABLE);
+ networkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
verifyActiveNetwork(TRANSPORT_WIFI);
// Test releasing NetworkRequest disconnects cellular with MMS
cv = mCellNetworkAgent.getDisconnectedCV();
@@ -1340,7 +1645,7 @@
MockNetworkAgent mmsNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
mmsNetworkAgent.addCapability(NET_CAPABILITY_MMS);
mmsNetworkAgent.connectWithoutInternet();
- networkCallback.expectCallback(CallbackState.AVAILABLE);
+ networkCallback.expectCallback(CallbackState.AVAILABLE, mmsNetworkAgent);
verifyActiveNetwork(TRANSPORT_CELLULAR);
// Test releasing MMS NetworkRequest does not disconnect main cellular NetworkAgent
cv = mmsNetworkAgent.getDisconnectedCV();
@@ -1366,36 +1671,36 @@
mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
String firstRedirectUrl = "http://example.com/firstPath";
mWiFiNetworkAgent.connectWithCaptivePortal(firstRedirectUrl);
- captivePortalCallback.expectCallback(CallbackState.AVAILABLE);
+ captivePortalCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), firstRedirectUrl);
// Take down network.
// Expect onLost callback.
mWiFiNetworkAgent.disconnect();
- captivePortalCallback.expectCallback(CallbackState.LOST);
+ captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
// Bring up a network with a captive portal.
// Expect onAvailable callback of listen for NET_CAPABILITY_CAPTIVE_PORTAL.
mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
String secondRedirectUrl = "http://example.com/secondPath";
mWiFiNetworkAgent.connectWithCaptivePortal(secondRedirectUrl);
- captivePortalCallback.expectCallback(CallbackState.AVAILABLE);
+ captivePortalCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
assertEquals(mWiFiNetworkAgent.waitForRedirectUrl(), secondRedirectUrl);
// Make captive portal disappear then revalidate.
// Expect onLost callback because network no longer provides NET_CAPABILITY_CAPTIVE_PORTAL.
mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 204;
mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), true);
- captivePortalCallback.expectCallback(CallbackState.LOST);
+ captivePortalCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
// Expect NET_CAPABILITY_VALIDATED onAvailable callback.
- validatedCallback.expectCallback(CallbackState.AVAILABLE);
+ validatedCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
// Break network connectivity.
// Expect NET_CAPABILITY_VALIDATED onLost callback.
mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 500;
mCm.reportNetworkConnectivity(mWiFiNetworkAgent.getNetwork(), false);
- validatedCallback.expectCallback(CallbackState.LOST);
+ validatedCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
}
@SmallTest
@@ -1410,7 +1715,7 @@
// do nothing - should get here
}
- assertTrue("NetworkReqeuest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
+ assertTrue("NetworkRequest builder with MATCH_ALL_REQUESTS_NETWORK_SPECIFIER",
execptionCalled);
try {
@@ -1477,6 +1782,367 @@
defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
}
+ private class TestRequestUpdateCallback extends TestNetworkCallback {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities netCap) {
+ setLastCallback(CallbackState.NETWORK_CAPABILITIES, network, netCap);
+ }
+
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties linkProp) {
+ setLastCallback(CallbackState.LINK_PROPERTIES, network, linkProp);
+ }
+ }
+
+ @LargeTest
+ public void testRequestCallbackUpdates() throws Exception {
+ // File a network request for mobile.
+ final TestNetworkCallback cellNetworkCallback = new TestRequestUpdateCallback();
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+ // Bring up the mobile network.
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+
+ // We should get onAvailable().
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ // We should get onCapabilitiesChanged(), when the mobile network successfully validates.
+ cellNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES, mCellNetworkAgent);
+ cellNetworkCallback.assertNoCallback();
+
+ // Update LinkProperties.
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName("foonet_data0");
+ mCellNetworkAgent.sendLinkProperties(lp);
+ // We should get onLinkPropertiesChanged().
+ cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
+ cellNetworkCallback.assertNoCallback();
+
+ // Register a garden variety default network request.
+ final TestNetworkCallback dfltNetworkCallback = new TestRequestUpdateCallback();
+ mCm.registerDefaultNetworkCallback(dfltNetworkCallback);
+ // Only onAvailable() is called; no other information is delivered.
+ dfltNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ dfltNetworkCallback.assertNoCallback();
+
+ // Request a NetworkCapabilities update; only the requesting callback is notified.
+ mCm.requestNetworkCapabilities(dfltNetworkCallback);
+ dfltNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES, mCellNetworkAgent);
+ cellNetworkCallback.assertNoCallback();
+ dfltNetworkCallback.assertNoCallback();
+
+ // Request a LinkProperties update; only the requesting callback is notified.
+ mCm.requestLinkProperties(dfltNetworkCallback);
+ dfltNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
+ cellNetworkCallback.assertNoCallback();
+ dfltNetworkCallback.assertNoCallback();
+
+ mCm.unregisterNetworkCallback(dfltNetworkCallback);
+ mCm.unregisterNetworkCallback(cellNetworkCallback);
+ }
+
+ @SmallTest
+ public void testRequestBenchmark() throws Exception {
+ // Benchmarks connecting and switching performance in the presence of a large number of
+ // NetworkRequests.
+ // 1. File NUM_REQUESTS requests.
+ // 2. Have a network connect. Wait for NUM_REQUESTS onAvailable callbacks to fire.
+ // 3. Have a new network connect and outscore the previous. Wait for NUM_REQUESTS onLosing
+ // and NUM_REQUESTS onAvailable callbacks to fire.
+ // See how long it took.
+ final int NUM_REQUESTS = 90;
+ final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+ final NetworkCallback[] callbacks = new NetworkCallback[NUM_REQUESTS];
+ final CountDownLatch availableLatch = new CountDownLatch(NUM_REQUESTS);
+ final CountDownLatch losingLatch = new CountDownLatch(NUM_REQUESTS);
+
+ final int REGISTER_TIME_LIMIT_MS = 100;
+ long startTime = System.currentTimeMillis();
+ for (int i = 0; i < NUM_REQUESTS; i++) {
+ callbacks[i] = new NetworkCallback() {
+ @Override public void onAvailable(Network n) { availableLatch.countDown(); }
+ @Override public void onLosing(Network n, int t) { losingLatch.countDown(); }
+ };
+ mCm.registerNetworkCallback(request, callbacks[i]);
+ }
+ long timeTaken = System.currentTimeMillis() - startTime;
+ String msg = String.format("Register %d callbacks: %dms, acceptable %dms",
+ NUM_REQUESTS, timeTaken, REGISTER_TIME_LIMIT_MS);
+ Log.d(TAG, msg);
+ assertTrue(msg, timeTaken < REGISTER_TIME_LIMIT_MS);
+
+ final int CONNECT_TIME_LIMIT_MS = 30;
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ // Don't request that the network validate, because otherwise connect() will block until
+ // the network gets NET_CAPABILITY_VALIDATED, after all the callbacks below have fired,
+ // and we won't actually measure anything.
+ mCellNetworkAgent.connect(false);
+ startTime = System.currentTimeMillis();
+ if (!availableLatch.await(CONNECT_TIME_LIMIT_MS, TimeUnit.MILLISECONDS)) {
+ fail(String.format("Only dispatched %d/%d onAvailable callbacks in %dms",
+ NUM_REQUESTS - availableLatch.getCount(), NUM_REQUESTS,
+ CONNECT_TIME_LIMIT_MS));
+ }
+ timeTaken = System.currentTimeMillis() - startTime;
+ Log.d(TAG, String.format("Connect, %d callbacks: %dms, acceptable %dms",
+ NUM_REQUESTS, timeTaken, CONNECT_TIME_LIMIT_MS));
+
+ final int SWITCH_TIME_LIMIT_MS = 30;
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ // Give wifi a high enough score that we'll linger cell when wifi comes up.
+ mWiFiNetworkAgent.adjustScore(40);
+ mWiFiNetworkAgent.connect(false);
+ startTime = System.currentTimeMillis();
+ if (!losingLatch.await(SWITCH_TIME_LIMIT_MS, TimeUnit.MILLISECONDS)) {
+ fail(String.format("Only dispatched %d/%d onLosing callbacks in %dms",
+ NUM_REQUESTS - losingLatch.getCount(), NUM_REQUESTS, SWITCH_TIME_LIMIT_MS));
+ }
+ timeTaken = System.currentTimeMillis() - startTime;
+ Log.d(TAG, String.format("Linger, %d callbacks: %dms, acceptable %dms",
+ NUM_REQUESTS, timeTaken, SWITCH_TIME_LIMIT_MS));
+
+ final int UNREGISTER_TIME_LIMIT_MS = 10;
+ startTime = System.currentTimeMillis();
+ for (int i = 0; i < NUM_REQUESTS; i++) {
+ mCm.unregisterNetworkCallback(callbacks[i]);
+ }
+ timeTaken = System.currentTimeMillis() - startTime;
+ msg = String.format("Unregister %d callbacks: %dms, acceptable %dms",
+ NUM_REQUESTS, timeTaken, UNREGISTER_TIME_LIMIT_MS);
+ Log.d(TAG, msg);
+ assertTrue(msg, timeTaken < UNREGISTER_TIME_LIMIT_MS);
+ }
+
+ @SmallTest
+ public void testMobileDataAlwaysOn() throws Exception {
+ final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ mCm.registerNetworkCallback(cellRequest, cellNetworkCallback);
+
+ final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory");
+ handlerThread.start();
+ NetworkCapabilities filter = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET);
+ final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(),
+ mServiceContext, "testFactory", filter);
+ testFactory.setScoreFilter(40);
+
+ // Register the factory and expect it to start looking for a network.
+ testFactory.expectAddRequests(1);
+ testFactory.register();
+ testFactory.waitForNetworkRequests(1);
+ assertTrue(testFactory.getMyStartRequested());
+
+ // Bring up wifi. The factory stops looking for a network.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ testFactory.expectAddRequests(2); // Because the default request changes score twice.
+ mWiFiNetworkAgent.connect(true);
+ testFactory.waitForNetworkRequests(1);
+ assertFalse(testFactory.getMyStartRequested());
+
+ ContentResolver cr = mServiceContext.getContentResolver();
+
+ // Turn on mobile data always on. The factory starts looking again.
+ testFactory.expectAddRequests(1);
+ Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 1);
+ mService.updateMobileDataAlwaysOn();
+ testFactory.waitForNetworkRequests(2);
+ assertTrue(testFactory.getMyStartRequested());
+
+ // Bring up cell data and check that the factory stops looking.
+ assertEquals(1, mCm.getAllNetworks().length);
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ testFactory.expectAddRequests(2); // Because the cell request changes score twice.
+ mCellNetworkAgent.connect(true);
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ testFactory.waitForNetworkRequests(2);
+ assertFalse(testFactory.getMyStartRequested()); // Because the cell network outscores us.
+
+ // Check that cell data stays up.
+ mService.waitForIdle();
+ verifyActiveNetwork(TRANSPORT_WIFI);
+ assertEquals(2, mCm.getAllNetworks().length);
+
+ // Turn off mobile data always on and expect the request to disappear...
+ testFactory.expectRemoveRequests(1);
+ Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 0);
+ mService.updateMobileDataAlwaysOn();
+ testFactory.waitForNetworkRequests(1);
+
+ // ... and cell data to be torn down.
+ cellNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ assertEquals(1, mCm.getAllNetworks().length);
+
+ testFactory.unregister();
+ mCm.unregisterNetworkCallback(cellNetworkCallback);
+ handlerThread.quit();
+ }
+
+ @SmallTest
+ public void testAvoidBadWifiSetting() throws Exception {
+ final ContentResolver cr = mServiceContext.getContentResolver();
+ final WrappedAvoidBadWifiTracker tracker = mService.getAvoidBadWifiTracker();
+ final String settingName = Settings.Global.NETWORK_AVOID_BAD_WIFI;
+
+ tracker.configRestrictsAvoidBadWifi = false;
+ String[] values = new String[] {null, "0", "1"};
+ for (int i = 0; i < values.length; i++) {
+ Settings.Global.putInt(cr, settingName, 1);
+ tracker.reevaluate();
+ mService.waitForIdle();
+ String msg = String.format("config=false, setting=%s", values[i]);
+ assertTrue(msg, mService.avoidBadWifi());
+ assertFalse(msg, tracker.shouldNotifyWifiUnvalidated());
+ }
+
+ tracker.configRestrictsAvoidBadWifi = true;
+
+ Settings.Global.putInt(cr, settingName, 0);
+ tracker.reevaluate();
+ mService.waitForIdle();
+ assertFalse(mService.avoidBadWifi());
+ assertFalse(tracker.shouldNotifyWifiUnvalidated());
+
+ Settings.Global.putInt(cr, settingName, 1);
+ tracker.reevaluate();
+ mService.waitForIdle();
+ assertTrue(mService.avoidBadWifi());
+ assertFalse(tracker.shouldNotifyWifiUnvalidated());
+
+ Settings.Global.putString(cr, settingName, null);
+ tracker.reevaluate();
+ mService.waitForIdle();
+ assertFalse(mService.avoidBadWifi());
+ assertTrue(tracker.shouldNotifyWifiUnvalidated());
+ }
+
+ @SmallTest
+ public void testAvoidBadWifi() throws Exception {
+ final ContentResolver cr = mServiceContext.getContentResolver();
+ final WrappedAvoidBadWifiTracker tracker = mService.getAvoidBadWifiTracker();
+
+ // Pretend we're on a carrier that restricts switching away from bad wifi.
+ tracker.configRestrictsAvoidBadWifi = true;
+
+ // File a request for cell to ensure it doesn't go down.
+ final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ mCm.requestNetwork(cellRequest, cellNetworkCallback);
+
+ TestNetworkCallback defaultCallback = new TestNetworkCallback();
+ mCm.registerDefaultNetworkCallback(defaultCallback);
+
+ NetworkRequest validatedWifiRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_VALIDATED)
+ .build();
+ TestNetworkCallback validatedWifiCallback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback);
+
+ Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 0);
+ tracker.reevaluate();
+
+ // Bring up validated cell.
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+ cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ Network cellNetwork = mCellNetworkAgent.getNetwork();
+
+ // Bring up validated wifi.
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ validatedWifiCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+ // Fail validation on wifi.
+ mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+ mCm.reportNetworkConnectivity(wifiNetwork, false);
+ validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+ // Because avoid bad wifi is off, we don't switch to cellular.
+ defaultCallback.assertNoCallback();
+ assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+ // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect
+ // that we switch back to cell.
+ tracker.configRestrictsAvoidBadWifi = false;
+ tracker.reevaluate();
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+ // Switch back to a restrictive carrier.
+ tracker.configRestrictsAvoidBadWifi = true;
+ tracker.reevaluate();
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+ // Simulate the user selecting "switch" on the dialog, and check that we switch to cell.
+ mCm.setAvoidUnvalidated(wifiNetwork);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+ // Disconnect and reconnect wifi to clear the one-time switch above.
+ mWiFiNetworkAgent.disconnect();
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ validatedWifiCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+ // Fail validation on wifi and expect the dialog to appear.
+ mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+ mCm.reportNetworkConnectivity(wifiNetwork, false);
+ validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+ // Simulate the user selecting "switch" and checking the don't ask again checkbox.
+ Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
+ tracker.reevaluate();
+
+ // We now switch to cell.
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+ NET_CAPABILITY_VALIDATED));
+ assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+ // Simulate the user turning the cellular fallback setting off and then on.
+ // We switch to wifi and then to cell.
+ Settings.Global.putString(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
+ tracker.reevaluate();
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+ Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
+ tracker.reevaluate();
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+ // If cell goes down, we switch to wifi.
+ mCellNetworkAgent.disconnect();
+ defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ validatedWifiCallback.assertNoCallback();
+
+ mCm.unregisterNetworkCallback(cellNetworkCallback);
+ mCm.unregisterNetworkCallback(validatedWifiCallback);
+ mCm.unregisterNetworkCallback(defaultCallback);
+ }
+
private static class TestKeepaliveCallback extends PacketKeepaliveCallback {
public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java
new file mode 100644
index 0000000..033b2c9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/DnsEventListenerServiceTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.metrics.DnsEvent;
+import android.net.metrics.IDnsEventListener;
+import android.net.metrics.IpConnectivityLog;
+
+import junit.framework.TestCase;
+import org.junit.Before;
+import org.junit.Test;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
+
+public class DnsEventListenerServiceTest extends TestCase {
+
+ // TODO: read from DnsEventListenerService after this constant is read from system property
+ static final int BATCH_SIZE = 100;
+ static final int EVENT_TYPE = IDnsEventListener.EVENT_GETADDRINFO;
+ // TODO: read from IDnsEventListener
+ static final int RETURN_CODE = 1;
+
+ static final byte[] EVENT_TYPES = new byte[BATCH_SIZE];
+ static final byte[] RETURN_CODES = new byte[BATCH_SIZE];
+ static final int[] LATENCIES = new int[BATCH_SIZE];
+ static {
+ for (int i = 0; i < BATCH_SIZE; i++) {
+ EVENT_TYPES[i] = EVENT_TYPE;
+ RETURN_CODES[i] = RETURN_CODE;
+ LATENCIES[i] = i;
+ }
+ }
+
+ DnsEventListenerService mDnsService;
+
+ @Mock ConnectivityManager mCm;
+ @Mock IpConnectivityLog mLog;
+ ArgumentCaptor<NetworkCallback> mCallbackCaptor;
+ ArgumentCaptor<DnsEvent> mEvCaptor;
+
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mCallbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class);
+ mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class);
+ mDnsService = new DnsEventListenerService(mCm, mLog);
+
+ verify(mCm, times(1)).registerNetworkCallback(any(), mCallbackCaptor.capture());
+ }
+
+ public void testOneBatch() throws Exception {
+ log(105, LATENCIES);
+ log(106, Arrays.copyOf(LATENCIES, BATCH_SIZE - 1)); // one lookup short of a batch event
+
+ verifyLoggedEvents(new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+
+ log(106, Arrays.copyOfRange(LATENCIES, BATCH_SIZE - 1, BATCH_SIZE));
+
+ mEvCaptor = ArgumentCaptor.forClass(DnsEvent.class); // reset argument captor
+ verifyLoggedEvents(
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES));
+ }
+
+ public void testSeveralBatches() throws Exception {
+ log(105, LATENCIES);
+ log(106, LATENCIES);
+ log(105, LATENCIES);
+ log(107, LATENCIES);
+
+ verifyLoggedEvents(
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
+ }
+
+ public void testBatchAndNetworkLost() throws Exception {
+ byte[] eventTypes = Arrays.copyOf(EVENT_TYPES, 20);
+ byte[] returnCodes = Arrays.copyOf(RETURN_CODES, 20);
+ int[] latencies = Arrays.copyOf(LATENCIES, 20);
+
+ log(105, LATENCIES);
+ log(105, latencies);
+ mCallbackCaptor.getValue().onLost(new Network(105));
+ log(105, LATENCIES);
+
+ verifyLoggedEvents(
+ new DnsEvent(105, eventTypes, returnCodes, latencies),
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES));
+ }
+
+ public void testConcurrentBatchesAndDumps() throws Exception {
+ final long stop = System.currentTimeMillis() + 100;
+ final PrintWriter pw = new PrintWriter(new FileOutputStream("/dev/null"));
+ new Thread() {
+ public void run() {
+ while (System.currentTimeMillis() < stop) {
+ mDnsService.dump(pw);
+ }
+ }
+ }.start();
+
+ logAsync(105, LATENCIES);
+ logAsync(106, LATENCIES);
+ logAsync(107, LATENCIES);
+
+ verifyLoggedEvents(500,
+ new DnsEvent(105, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(106, EVENT_TYPES, RETURN_CODES, LATENCIES),
+ new DnsEvent(107, EVENT_TYPES, RETURN_CODES, LATENCIES));
+ }
+
+ public void testConcurrentBatchesAndNetworkLoss() throws Exception {
+ logAsync(105, LATENCIES);
+ Thread.sleep(10L);
+ // call onLost() asynchronously to logAsync's onDnsEvent() calls.
+ mCallbackCaptor.getValue().onLost(new Network(105));
+
+ // do not verify unpredictable batch
+ verify(mLog, timeout(500).times(1)).log(any());
+ }
+
+ void log(int netId, int[] latencies) {
+ for (int l : latencies) {
+ mDnsService.onDnsEvent(netId, EVENT_TYPE, RETURN_CODE, l);
+ }
+ }
+
+ void logAsync(int netId, int[] latencies) {
+ new Thread() {
+ public void run() {
+ log(netId, latencies);
+ }
+ }.start();
+ }
+
+ void verifyLoggedEvents(DnsEvent... expected) {
+ verifyLoggedEvents(0, expected);
+ }
+
+ void verifyLoggedEvents(int wait, DnsEvent... expectedEvents) {
+ verify(mLog, timeout(wait).times(expectedEvents.length)).log(mEvCaptor.capture());
+ for (DnsEvent got : mEvCaptor.getAllValues()) {
+ OptionalInt index = IntStream.range(0, expectedEvents.length)
+ .filter(i -> eventsEqual(expectedEvents[i], got))
+ .findFirst();
+ // Don't match same expected event more than once.
+ index.ifPresent(i -> expectedEvents[i] = null);
+ assertTrue(index.isPresent());
+ }
+ }
+
+ /** equality function for DnsEvent to avoid overriding equals() and hashCode(). */
+ static boolean eventsEqual(DnsEvent expected, DnsEvent got) {
+ return (expected == got) || ((expected != null) && (got != null)
+ && (expected.netId == got.netId)
+ && Arrays.equals(expected.eventTypes, got.eventTypes)
+ && Arrays.equals(expected.returnCodes, got.returnCodes)
+ && Arrays.equals(expected.latenciesMs, got.latenciesMs));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java b/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java
new file mode 100644
index 0000000..bce5787
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/LingerMonitorTest.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkMisc;
+import android.text.format.DateUtils;
+import com.android.internal.R;
+import com.android.server.ConnectivityService;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import junit.framework.TestCase;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.reset;
+
+public class LingerMonitorTest extends TestCase {
+ static final String CELLULAR = "CELLULAR";
+ static final String WIFI = "WIFI";
+
+ static final long LOW_RATE_LIMIT = DateUtils.MINUTE_IN_MILLIS;
+ static final long HIGH_RATE_LIMIT = 0;
+
+ static final int LOW_DAILY_LIMIT = 2;
+ static final int HIGH_DAILY_LIMIT = 1000;
+
+ LingerMonitor mMonitor;
+
+ @Mock ConnectivityService mConnService;
+ @Mock Context mCtx;
+ @Mock NetworkMisc mMisc;
+ @Mock NetworkNotificationManager mNotifier;
+ @Mock Resources mResources;
+
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mCtx.getResources()).thenReturn(mResources);
+ when(mCtx.getPackageName()).thenReturn("com.android.server.connectivity");
+ when(mConnService.createNetworkMonitor(any(), any(), any(), any())).thenReturn(null);
+
+ mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, HIGH_RATE_LIMIT);
+ }
+
+ public void testTransitions() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ NetworkAgentInfo nai1 = wifiNai(100);
+ NetworkAgentInfo nai2 = cellNai(101);
+
+ assertTrue(mMonitor.isNotificationEnabled(nai1, nai2));
+ assertFalse(mMonitor.isNotificationEnabled(nai2, nai1));
+ }
+
+ public void testNotificationOnLinger() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNotification(from, to);
+ }
+
+ public void testToastOnLinger() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyToast(from, to);
+ }
+
+ public void testNotificationClearedAfterDisconnect() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNotification(from, to);
+
+ mMonitor.noteDisconnect(to);
+ verify(mNotifier, times(1)).clearNotification(100);
+ }
+
+ public void testNotificationClearedAfterSwitchingBack() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNotification(from, to);
+
+ mMonitor.noteLingerDefaultNetwork(to, from);
+ verify(mNotifier, times(1)).clearNotification(100);
+ }
+
+ public void testUniqueToast() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyToast(from, to);
+
+ mMonitor.noteLingerDefaultNetwork(to, from);
+ verify(mNotifier, times(1)).clearNotification(100);
+
+ reset(mNotifier);
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ public void testMultipleNotifications() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo wifi1 = wifiNai(100);
+ NetworkAgentInfo wifi2 = wifiNai(101);
+ NetworkAgentInfo cell = cellNai(102);
+
+ mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+ verifyNotification(wifi1, cell);
+
+ mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+ verify(mNotifier, times(1)).clearNotification(100);
+
+ reset(mNotifier);
+ mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+ verifyNotification(wifi2, cell);
+ }
+
+ public void testRateLimiting() throws InterruptedException {
+ mMonitor = new TestableLingerMonitor(mCtx, mNotifier, HIGH_DAILY_LIMIT, LOW_RATE_LIMIT);
+
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo wifi1 = wifiNai(100);
+ NetworkAgentInfo wifi2 = wifiNai(101);
+ NetworkAgentInfo wifi3 = wifiNai(102);
+ NetworkAgentInfo cell = cellNai(103);
+
+ mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+ verifyNotification(wifi1, cell);
+ reset(mNotifier);
+
+ Thread.sleep(50);
+ mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+ mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+ verifyNoNotifications();
+
+ Thread.sleep(50);
+ mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+ mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+ verifyNoNotifications();
+ }
+
+ public void testDailyLimiting() throws InterruptedException {
+ mMonitor = new TestableLingerMonitor(mCtx, mNotifier, LOW_DAILY_LIMIT, HIGH_RATE_LIMIT);
+
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo wifi1 = wifiNai(100);
+ NetworkAgentInfo wifi2 = wifiNai(101);
+ NetworkAgentInfo wifi3 = wifiNai(102);
+ NetworkAgentInfo cell = cellNai(103);
+
+ mMonitor.noteLingerDefaultNetwork(wifi1, cell);
+ verifyNotification(wifi1, cell);
+ reset(mNotifier);
+
+ Thread.sleep(50);
+ mMonitor.noteLingerDefaultNetwork(cell, wifi2);
+ mMonitor.noteLingerDefaultNetwork(wifi2, cell);
+ verifyNotification(wifi2, cell);
+ reset(mNotifier);
+
+ Thread.sleep(50);
+ mMonitor.noteLingerDefaultNetwork(cell, wifi3);
+ mMonitor.noteLingerDefaultNetwork(wifi3, cell);
+ verifyNoNotifications();
+ }
+
+ public void testUniqueNotification() {
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NOTIFICATION);
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNotification(from, to);
+
+ mMonitor.noteLingerDefaultNetwork(to, from);
+ verify(mNotifier, times(1)).clearNotification(100);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNotification(from, to);
+ }
+
+ public void testIgnoreNeverValidatedNetworks() {
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+ from.everValidated = false;
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ public void testIgnoreCurrentlyValidatedNetworks() {
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+ from.lastValidated = true;
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ public void testNoNotificationType() {
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ setNotificationSwitch();
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ public void testNoTransitionToNotify() {
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_NONE);
+ setNotificationSwitch(transition(WIFI, CELLULAR));
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ public void testDifferentTransitionToNotify() {
+ setNotificationType(LingerMonitor.NOTIFY_TYPE_TOAST);
+ setNotificationSwitch(transition(CELLULAR, WIFI));
+ NetworkAgentInfo from = wifiNai(100);
+ NetworkAgentInfo to = cellNai(101);
+
+ mMonitor.noteLingerDefaultNetwork(from, to);
+ verifyNoNotifications();
+ }
+
+ void setNotificationSwitch(String... transitions) {
+ when(mResources.getStringArray(R.array.config_networkNotifySwitches))
+ .thenReturn(transitions);
+ }
+
+ String transition(String from, String to) {
+ return from + "-" + to;
+ }
+
+ void setNotificationType(int type) {
+ when(mResources.getInteger(R.integer.config_networkNotifySwitchType)).thenReturn(type);
+ }
+
+ void verifyNoToast() {
+ verify(mNotifier, never()).showToast(any(), any());
+ }
+
+ void verifyNoNotification() {
+ verify(mNotifier, never())
+ .showNotification(anyInt(), any(), any(), any(), any(), anyBoolean());
+ }
+
+ void verifyNoNotifications() {
+ verifyNoToast();
+ verifyNoNotification();
+ }
+
+ void verifyToast(NetworkAgentInfo from, NetworkAgentInfo to) {
+ verifyNoNotification();
+ verify(mNotifier, times(1)).showToast(from, to);
+ }
+
+ void verifyNotification(NetworkAgentInfo from, NetworkAgentInfo to) {
+ verifyNoToast();
+ verify(mNotifier, times(1)).showNotification(eq(from.network.netId),
+ eq(NotificationType.NETWORK_SWITCH), eq(from), eq(to), any(), eq(true));
+ }
+
+ NetworkAgentInfo nai(int netId, int transport, int networkType, String networkTypeName) {
+ NetworkInfo info = new NetworkInfo(networkType, 0, networkTypeName, "");
+ NetworkCapabilities caps = new NetworkCapabilities();
+ caps.addCapability(0);
+ caps.addTransportType(transport);
+ NetworkAgentInfo nai = new NetworkAgentInfo(null, null, new Network(netId), info, null,
+ caps, 50, mCtx, null, mMisc, null, mConnService);
+ nai.everValidated = true;
+ return nai;
+ }
+
+ NetworkAgentInfo wifiNai(int netId) {
+ return nai(netId, NetworkCapabilities.TRANSPORT_WIFI,
+ ConnectivityManager.TYPE_WIFI, WIFI);
+ }
+
+ NetworkAgentInfo cellNai(int netId) {
+ return nai(netId, NetworkCapabilities.TRANSPORT_CELLULAR,
+ ConnectivityManager.TYPE_MOBILE, CELLULAR);
+ }
+
+ public static class TestableLingerMonitor extends LingerMonitor {
+ public TestableLingerMonitor(Context c, NetworkNotificationManager n, int l, long r) {
+ super(c, n, l, r);
+ }
+ @Override protected PendingIntent createNotificationIntent() {
+ return null;
+ }
+ }
+}