DO NOT MERGE ANYWHERE Add CONNECTIVITY_USE_RESTRICTED_NETWORKS permission am: 849682f5a0 -s ours am: d55ff11c4b -s ours am: 0a159e0967 -s ours
am: 24c196f55f -s ours
Change-Id: I42aabc35fa30e242125015eac1b7d32790cc4528
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index b9e9b28..0afb546 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -1825,6 +1825,16 @@
return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
+ /* TODO: These permissions checks don't belong in client-side code. Move them to
+ * services.jar, possibly in com.android.server.net. */
+
+ /** {@hide} */
+ public static final boolean checkChangePermission(Context context) {
+ int uid = Binder.getCallingUid();
+ return Settings.checkAndNoteChangeNetworkStateOperation(context, uid, Settings
+ .getPackageNameForUid(context, uid), false /* throwException */);
+ }
+
/** {@hide} */
public static final void enforceChangePermission(Context context) {
int uid = Binder.getCallingUid();
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
index 6243f46..56eba4f 100644
--- a/core/java/android/net/NetworkCapabilities.java
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -182,8 +182,15 @@
*/
public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17;
+ /**
+ * Indicates that this network is available for use by apps, and not a network that is being
+ * kept up in the background to facilitate fast network switching.
+ * @hide
+ */
+ public static final int NET_CAPABILITY_FOREGROUND = 18;
+
private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_CAPTIVE_PORTAL;
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_FOREGROUND;
/**
* Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -194,7 +201,8 @@
// http://b/18206275
(1 << NET_CAPABILITY_TRUSTED) |
(1 << NET_CAPABILITY_VALIDATED) |
- (1 << NET_CAPABILITY_CAPTIVE_PORTAL);
+ (1 << NET_CAPABILITY_CAPTIVE_PORTAL) |
+ (1 << NET_CAPABILITY_FOREGROUND);
/**
* Network specifier for factories which want to match any network specifier
@@ -217,8 +225,7 @@
* get immediately torn down because they do not have the requested capability.
*/
private static final long NON_REQUESTABLE_CAPABILITIES =
- (1 << NET_CAPABILITY_VALIDATED) |
- (1 << NET_CAPABILITY_CAPTIVE_PORTAL);
+ MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_TRUSTED);
/**
* Capabilities that are set by default when the object is constructed.
@@ -325,6 +332,7 @@
public String describeFirstNonRequestableCapability() {
if (hasCapability(NET_CAPABILITY_VALIDATED)) return "NET_CAPABILITY_VALIDATED";
if (hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return "NET_CAPABILITY_CAPTIVE_PORTAL";
+ if (hasCapability(NET_CAPABILITY_FOREGROUND)) return "NET_CAPABILITY_FOREGROUND";
// This cannot happen unless the preceding checks are incomplete.
if ((mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES) != 0) {
return "unknown non-requestable capabilities " + Long.toHexString(mNetworkCapabilities);
@@ -352,6 +360,11 @@
(that.mNetworkCapabilities & ~MUTABLE_CAPABILITIES));
}
+ private boolean equalsNetCapabilitiesRequestable(NetworkCapabilities that) {
+ return ((this.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) ==
+ (that.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES));
+ }
+
/**
* Removes the NET_CAPABILITY_NOT_RESTRICTED capability if all the capabilities it provides are
* typically provided by restricted networks.
@@ -749,6 +762,19 @@
equalsSpecifier(nc));
}
+ /**
+ * Checks that our requestable capabilities are the same as those of the given
+ * {@code NetworkCapabilities}.
+ *
+ * @hide
+ */
+ public boolean equalRequestableCapabilities(NetworkCapabilities nc) {
+ if (nc == null) return false;
+ return (equalsNetCapabilitiesRequestable(nc) &&
+ equalsTransportTypes(nc) &&
+ equalsSpecifier(nc));
+ }
+
@Override
public boolean equals(Object obj) {
if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
@@ -833,6 +859,7 @@
case NET_CAPABILITY_NOT_VPN: capabilities += "NOT_VPN"; break;
case NET_CAPABILITY_VALIDATED: capabilities += "VALIDATED"; break;
case NET_CAPABILITY_CAPTIVE_PORTAL: capabilities += "CAPTIVE_PORTAL"; break;
+ case NET_CAPABILITY_FOREGROUND: capabilities += "FOREGROUND"; break;
}
if (++i < types.length) capabilities += "&";
}
diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java
index 4501f7b..ae72470 100644
--- a/core/java/android/net/NetworkRequest.java
+++ b/core/java/android/net/NetworkRequest.java
@@ -49,7 +49,7 @@
public final int legacyType;
/**
- * A NetworkRequest as used by the system can be one of three types:
+ * A NetworkRequest as used by the system can be one of the following types:
*
* - LISTEN, for which the framework will issue callbacks about any
* and all networks that match the specified NetworkCapabilities,
@@ -64,7 +64,20 @@
* 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.
+ * specific network. Note that from the point of view of the request
+ * matching code, TRACK_DEFAULT is identical to REQUEST: its special
+ * behaviour is not due to different semantics, but to the fact that
+ * the system will only ever create a TRACK_DEFAULT with capabilities
+ * that are identical to the default request's capabilities, thus
+ * causing it to share fate in every way with the default request.
+ *
+ * - BACKGROUND_REQUEST, like REQUEST but does not cause any networks
+ * to retain the NET_CAPABILITY_FOREGROUND capability. A network with
+ * no foreground requests is in the background. A network that has
+ * one or more background requests and loses its last foreground
+ * request to a higher-scoring network will not go into the
+ * background immediately, but will linger and go into the background
+ * after the linger timeout.
*
* - The value NONE is used only by applications. When an application
* creates a NetworkRequest, it does not have a type; the type is set
@@ -77,7 +90,8 @@
NONE,
LISTEN,
TRACK_DEFAULT,
- REQUEST
+ REQUEST,
+ BACKGROUND_REQUEST,
};
/**
@@ -140,7 +154,7 @@
* Add the given capability requirement to this builder. These represent
* the requested network's required capabilities. Note that when searching
* for a network to satisfy a request, all capabilities requested must be
- * satisfied. See {@link NetworkCapabilities} for {@code NET_CAPABILITIY_*}
+ * satisfied. See {@link NetworkCapabilities} for {@code NET_CAPABILITY_*}
* definitions.
*
* @param capability The {@code NetworkCapabilities.NET_CAPABILITY_*} to add.
@@ -284,7 +298,7 @@
};
/**
- * Returns true iff. the contained NetworkRequest is of type LISTEN.
+ * Returns true iff. this NetworkRequest is of type LISTEN.
*
* @hide
*/
@@ -298,8 +312,9 @@
* - 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.
+ * - should cause a network to be kept up, but not necessarily in
+ * the foreground, 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().
@@ -307,9 +322,36 @@
* @hide
*/
public boolean isRequest() {
+ return isForegroundRequest() || isBackgroundRequest();
+ }
+
+ /**
+ * 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 and in the foreground 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 isForegroundRequest() {
return type == Type.TRACK_DEFAULT || type == Type.REQUEST;
}
+ /**
+ * Returns true iff. this NetworkRequest is of type BACKGROUND_REQUEST.
+ *
+ * @hide
+ */
+ public boolean isBackgroundRequest() {
+ return type == Type.BACKGROUND_REQUEST;
+ }
+
public String toString() {
return "NetworkRequest [ " + type + " id=" + requestId +
(legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") +
diff --git a/core/tests/coretests/src/android/net/NetworkStatsTest.java b/core/tests/coretests/src/android/net/NetworkStatsTest.java
index 9074f8a..d48a67a 100644
--- a/core/tests/coretests/src/android/net/NetworkStatsTest.java
+++ b/core/tests/coretests/src/android/net/NetworkStatsTest.java
@@ -454,7 +454,7 @@
.addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
- assertEquals(21, delta.size());
+ assertEquals(20, delta.size());
// tunIface and TEST_IFACE entries are not changed.
assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
@@ -478,38 +478,89 @@
// Existing underlying Iface entries are updated
assertValues(delta, 9, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
- 44783L, 54L, 13829L, 60L, 0L);
+ 44783L, 54L, 14178L, 62L, 0L);
assertValues(delta, 10, underlyingIface, 10100, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
0L, 0L, 0L, 0L, 0L);
// VPN underlying Iface entries are updated
assertValues(delta, 11, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, ROAMING_NO,
- 28304L, 27L, 1719L, 12L, 0L);
+ 28304L, 27L, 1L, 2L, 0L);
assertValues(delta, 12, underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
0L, 0L, 0L, 0L, 0L);
// New entries are added for new application's underlying Iface traffic
assertContains(delta, underlyingIface, 10120, SET_DEFAULT, TAG_NONE, ROAMING_NO,
- 72667L, 197L, 41872L, 219L, 0L);
+ 72667L, 197L, 43123L, 227L, 0L);
assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, TAG_NONE, ROAMING_NO,
- 9297L, 17L, 3936, 19L, 0L);
+ 9297L, 17L, 4054, 19L, 0L);
assertContains(delta, underlyingIface, 10120, SET_DEFAULT, testTag1, ROAMING_NO,
- 21691L, 41L, 13179L, 46L, 0L);
+ 21691L, 41L, 13572L, 48L, 0L);
assertContains(delta, underlyingIface, 10120, SET_FOREGROUND, testTag1, ROAMING_NO,
- 1281L, 2L, 634L, 1L, 0L);
+ 1281L, 2L, 653L, 1L, 0L);
// New entries are added for debug purpose
assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
- 39605L, 46L, 11690, 49, 0);
+ 39605L, 46L, 12039, 51, 0);
assertContains(delta, underlyingIface, 10120, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
- 81964, 214, 45808, 238, 0);
- assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
- 4983, 10, 1717, 10, 0);
+ 81964, 214, 47177, 246, 0);
assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, ROAMING_ALL,
- 126552, 270, 59215, 297, 0);
+ 121569, 260, 59216, 297, 0);
}
+ // Tests a case where all of the data received by the tun0 interface is echo back into the tun0
+ // interface by the vpn app before it's sent out of the underlying interface. The VPN app should
+ // not be charged for the echoed data but it should still be charged for any extra data it sends
+ // via the underlying interface.
+ public void testMigrateTun_VpnAsLoopback() {
+ final int tunUid = 10030;
+ final String tunIface = "tun0";
+ final String underlyingIface = "wlan0";
+ NetworkStats delta = new NetworkStats(TEST_START, 9)
+ // 2 different apps sent/receive data via tun0.
+ .addValues(tunIface, 10100, SET_DEFAULT, TAG_NONE, 50000L, 25L, 100000L, 50L, 0L)
+ .addValues(tunIface, 20100, SET_DEFAULT, TAG_NONE, 500L, 2L, 200L, 5L, 0L)
+ // VPN package resends data through the tunnel (with exaggerated overhead)
+ .addValues(tunIface, tunUid, SET_DEFAULT, TAG_NONE, 240000, 100L, 120000L, 60L, 0L)
+ // 1 app already has some traffic on the underlying interface, the other doesn't yet
+ .addValues(underlyingIface, 10100, SET_DEFAULT, TAG_NONE, 1000L, 10L, 2000L, 20L, 0L)
+ // Traffic through the underlying interface via the vpn app.
+ // This test should redistribute this data correctly.
+ .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
+ 75500L, 37L, 130000L, 70L, 0L);
+
+ assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
+ assertEquals(9, delta.size());
+
+ // tunIface entries should not be changed.
+ assertValues(delta, 0, tunIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 50000L, 25L, 100000L, 50L, 0L);
+ assertValues(delta, 1, tunIface, 20100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 500L, 2L, 200L, 5L, 0L);
+ assertValues(delta, 2, tunIface, tunUid, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 240000L, 100L, 120000L, 60L, 0L);
+
+ // Existing underlying Iface entries are updated
+ assertValues(delta, 3, underlyingIface, 10100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 51000L, 35L, 102000L, 70L, 0L);
+
+ // VPN underlying Iface entries are updated
+ assertValues(delta, 4, underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 25000L, 10L, 29800L, 15L, 0L);
+
+ // New entries are added for new application's underlying Iface traffic
+ assertContains(delta, underlyingIface, 20100, SET_DEFAULT, TAG_NONE, ROAMING_NO,
+ 500L, 2L, 200L, 5L, 0L);
+
+ // New entries are added for debug purpose
+ assertContains(delta, underlyingIface, 10100, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
+ 50000L, 25L, 100000L, 50L, 0L);
+ assertContains(delta, underlyingIface, 20100, SET_DBG_VPN_IN, TAG_NONE, ROAMING_NO,
+ 500, 2L, 200L, 5L, 0L);
+ assertContains(delta, underlyingIface, tunUid, SET_DBG_VPN_OUT, TAG_NONE, ROAMING_ALL,
+ 50500L, 27L, 100200L, 55, 0);
+ }
+
private static void assertContains(NetworkStats stats, String iface, int uid, int set,
int tag, int roaming, long rxBytes, long rxPackets, long txBytes, long txPackets,
long operations) {
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 989892f..5f59e32 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -24,6 +24,7 @@
import static android.net.ConnectivityManager.getNetworkTypeName;
import static android.net.ConnectivityManager.isNetworkTypeValid;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -263,6 +264,11 @@
DONT_REAP
};
+ private enum UnneededFor {
+ LINGER, // Determine whether this network is unneeded and should be lingered.
+ TEARDOWN, // Determine whether this network is unneeded and should be torn down.
+ }
+
/**
* used internally to change our mobile data enabled flag
*/
@@ -700,13 +706,13 @@
if (DBG) log("ConnectivityService starting up");
mMetricsLog = logger;
- mDefaultRequest = createInternetRequestForTransport(-1);
+ mDefaultRequest = createInternetRequestForTransport(-1, NetworkRequest.Type.REQUEST);
NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest, new Binder());
mNetworkRequests.put(mDefaultRequest, defaultNRI);
mNetworkRequestInfoLogs.log("REGISTER " + defaultNRI);
mDefaultMobileDataRequest = createInternetRequestForTransport(
- NetworkCapabilities.TRANSPORT_CELLULAR);
+ NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST);
mHandlerThread = createHandlerThread();
mHandlerThread.start();
@@ -809,7 +815,7 @@
mTestMode = SystemProperties.get("cm.test.mode").equals("true")
&& SystemProperties.get("ro.build.type").equals("eng");
- mTethering = new Tethering(mContext, mNetd, statsService);
+ mTethering = new Tethering(mContext, mNetd, statsService, mPolicyManager);
mPermissionMonitor = new PermissionMonitor(mContext, mNetd);
@@ -860,15 +866,15 @@
mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
}
- private NetworkRequest createInternetRequestForTransport(int transportType) {
+ private NetworkRequest createInternetRequestForTransport(
+ int transportType, NetworkRequest.Type type) {
NetworkCapabilities netCap = new NetworkCapabilities();
netCap.addCapability(NET_CAPABILITY_INTERNET);
netCap.addCapability(NET_CAPABILITY_NOT_RESTRICTED);
if (transportType > -1) {
netCap.addTransportType(transportType);
}
- return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(),
- NetworkRequest.Type.REQUEST);
+ return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
}
// Used only for testing.
@@ -1982,8 +1988,12 @@
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
pw.println(nai.toString());
pw.increaseIndent();
- pw.println(String.format("Requests: %d request/%d total",
- nai.numRequestNetworkRequests(), nai.numNetworkRequests()));
+ pw.println(String.format(
+ "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
+ nai.numForegroundNetworkRequests(),
+ nai.numNetworkRequests() - nai.numRequestNetworkRequests(),
+ nai.numBackgroundNetworkRequests(),
+ nai.numNetworkRequests()));
pw.increaseIndent();
for (int i = 0; i < nai.numNetworkRequests(); i++) {
pw.println(nai.requestAt(i).toString());
@@ -2144,14 +2154,10 @@
case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
final NetworkCapabilities networkCapabilities = (NetworkCapabilities) msg.obj;
if (networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) ||
- networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED)) {
+ networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) ||
+ networkCapabilities.hasCapability(NET_CAPABILITY_FOREGROUND)) {
Slog.wtf(TAG, "BUG: " + nai + " has CS-managed capability.");
}
- if (nai.everConnected && !nai.networkCapabilities.equalImmutableCapabilities(
- networkCapabilities)) {
- Slog.wtf(TAG, "BUG: " + nai + " changed immutable capabilities: "
- + nai.networkCapabilities + " -> " + networkCapabilities);
- }
updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
break;
}
@@ -2311,15 +2317,13 @@
// 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 (nai.isLingering() && nai.numForegroundNetworkRequests() > 0) {
if (DBG) log("Unlingering " + nai.name());
nai.unlinger();
logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
- } else if (unneeded(nai) && nai.getLingerExpiry() > 0) { // unneeded() calls isLingering()
+ } else if (unneeded(nai, UnneededFor.LINGER) && nai.getLingerExpiry() > 0) {
int lingerTime = (int) (nai.getLingerExpiry() - now);
- if (DBG) {
- Log.d(TAG, "Lingering " + nai.name() + " for " + lingerTime + "ms");
- }
+ if (DBG) log("Lingering " + nai.name() + " for " + lingerTime + "ms");
nai.linger();
logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
@@ -2498,15 +2502,37 @@
}
}
- // Is nai unneeded by all NetworkRequests (and should be disconnected)?
- // 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.isLingering() || nai.numRequestNetworkRequests() > 0) {
+ // Determines whether the network is the best (or could become the best, if it validated), for
+ // none of a particular type of NetworkRequests. The type of NetworkRequests considered depends
+ // on the value of reason:
+ //
+ // - UnneededFor.TEARDOWN: non-listen NetworkRequests. If a network is unneeded for this reason,
+ // then it should be torn down.
+ // - UnneededFor.LINGER: foreground NetworkRequests. If a network is unneeded for this reason,
+ // then it should be lingered.
+ private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
+ final int numRequests;
+ switch (reason) {
+ case TEARDOWN:
+ numRequests = nai.numRequestNetworkRequests();
+ break;
+ case LINGER:
+ numRequests = nai.numForegroundNetworkRequests();
+ break;
+ default:
+ Slog.wtf(TAG, "Invalid reason. Cannot happen.");
+ return true;
+ }
+
+ if (!nai.everConnected || nai.isVPN() || nai.isLingering() || numRequests > 0) {
return false;
}
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (reason == UnneededFor.LINGER && nri.request.isBackgroundRequest()) {
+ // Background requests don't affect lingering.
+ continue;
+ }
+
// 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.request.isRequest() && nai.satisfies(nri.request) &&
@@ -2594,6 +2620,7 @@
boolean wasKept = false;
NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);
if (nai != null) {
+ boolean wasBackgroundNetwork = nai.isBackgroundNetwork();
nai.removeRequest(nri.request.requestId);
if (VDBG) {
log(" Removing from current network " + nai.name() +
@@ -2602,13 +2629,17 @@
// 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 (unneeded(nai, UnneededFor.TEARDOWN)) {
if (DBG) log("no live requests for " + nai.name() + "; disconnecting");
teardownUnneededNetwork(nai);
} else {
wasKept = true;
}
mNetworkForRequestId.remove(nri.request.requestId);
+ if (!wasBackgroundNetwork && nai.isBackgroundNetwork()) {
+ // Went from foreground to background.
+ updateCapabilities(nai.getCurrentScore(), nai, nai.networkCapabilities);
+ }
}
// TODO: remove this code once we know that the Slog.wtf is never hit.
@@ -2961,12 +2992,6 @@
ConnectivityManager.enforceTetherChangePermission(mContext);
if (isTetheringSupported()) {
final int status = mTethering.tether(iface);
- if (status == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
- try {
- mPolicyManager.onTetheringChanged(iface, true);
- } catch (RemoteException e) {
- }
- }
return status;
} else {
return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
@@ -2980,12 +3005,6 @@
if (isTetheringSupported()) {
final int status = mTethering.untether(iface);
- if (status == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
- try {
- mPolicyManager.onTetheringChanged(iface, false);
- } catch (RemoteException e) {
- }
- }
return status;
} else {
return ConnectivityManager.TETHER_ERROR_UNSUPPORTED;
@@ -4255,8 +4274,17 @@
enforceAccessPermission();
}
- NetworkRequest networkRequest = new NetworkRequest(
- new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId(),
+ NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ if (!ConnectivityManager.checkChangePermission(mContext)) {
+ // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
+ // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
+ // onLost and onAvailable callbacks when networks move in and out of the background.
+ // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE
+ // can't request networks.
+ nc.addCapability(NET_CAPABILITY_FOREGROUND);
+ }
+
+ NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
NetworkRequest.Type.LISTEN);
NetworkRequestInfo nri = new NetworkRequestInfo(messenger, networkRequest, binder);
if (VDBG) log("listenForNetwork for " + nri);
@@ -4571,6 +4599,17 @@
mNumDnsEntries = last;
}
+ private String getNetworkPermission(NetworkCapabilities nc) {
+ // TODO: make these permission strings AIDL constants instead.
+ if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ return NetworkManagementService.PERMISSION_SYSTEM;
+ }
+ if (!nc.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+ return NetworkManagementService.PERMISSION_NETWORK;
+ }
+ return null;
+ }
+
/**
* Update the NetworkCapabilities for {@code networkAgent} to {@code networkCapabilities}
* augmented with any stateful capabilities implied from {@code networkAgent}
@@ -4583,6 +4622,12 @@
*/
private void updateCapabilities(
int oldScore, NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
+ if (nai.everConnected && !nai.networkCapabilities.equalImmutableCapabilities(
+ networkCapabilities)) {
+ Slog.wtf(TAG, "BUG: " + nai + " changed immutable capabilities: "
+ + nai.networkCapabilities + " -> " + networkCapabilities);
+ }
+
// Don't modify caller's NetworkCapabilities.
networkCapabilities = new NetworkCapabilities(networkCapabilities);
if (nai.lastValidated) {
@@ -4595,20 +4640,38 @@
} else {
networkCapabilities.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
}
- if (!Objects.equals(nai.networkCapabilities, networkCapabilities)) {
- if (nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) !=
- networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
- try {
- mNetd.setNetworkPermission(nai.network.netId,
- networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) ?
- null : NetworkManagementService.PERMISSION_SYSTEM);
- } catch (RemoteException e) {
- loge("Exception in setNetworkPermission: " + e);
- }
+ if (nai.isBackgroundNetwork()) {
+ networkCapabilities.removeCapability(NET_CAPABILITY_FOREGROUND);
+ } else {
+ networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+ }
+
+ if (Objects.equals(nai.networkCapabilities, networkCapabilities)) return;
+
+ final String oldPermission = getNetworkPermission(nai.networkCapabilities);
+ final String newPermission = getNetworkPermission(networkCapabilities);
+ if (!Objects.equals(oldPermission, newPermission) && nai.created && !nai.isVPN()) {
+ try {
+ mNetd.setNetworkPermission(nai.network.netId, newPermission);
+ } catch (RemoteException e) {
+ loge("Exception in setNetworkPermission: " + e);
}
- synchronized (nai) {
- nai.networkCapabilities = networkCapabilities;
- }
+ }
+
+ final NetworkCapabilities prevNc = nai.networkCapabilities;
+ synchronized (nai) {
+ nai.networkCapabilities = networkCapabilities;
+ }
+ if (nai.getCurrentScore() == oldScore &&
+ networkCapabilities.equalRequestableCapabilities(prevNc)) {
+ // If the requestable capabilities haven't changed, and the score hasn't changed, then
+ // the change we're processing can't affect any requests, it can only affect the listens
+ // on this network. We might have been called by rematchNetworkAndRequests when a
+ // network changed foreground state.
+ processListenRequests(nai, true);
+ } else {
+ // If the requestable capabilities have changed or the score changed, we can't have been
+ // called by rematchNetworkAndRequests, so it's safe to start a rematch.
rematchAllNetworksAndRequests(nai, oldScore);
notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
}
@@ -4731,8 +4794,13 @@
// must be no other active linger timers, and we must stop lingering.
oldNetwork.clearLingerState();
- if (unneeded(oldNetwork)) {
+ if (unneeded(oldNetwork, UnneededFor.TEARDOWN)) {
+ // Tear the network down.
teardownUnneededNetwork(oldNetwork);
+ } else {
+ // Put the network in the background.
+ updateCapabilities(oldNetwork.getCurrentScore(), oldNetwork,
+ oldNetwork.networkCapabilities);
}
}
@@ -4750,6 +4818,31 @@
setDefaultDnsSystemProperties(newNetwork.linkProperties.getDnsServers());
}
+ private void processListenRequests(NetworkAgentInfo nai, boolean capabilitiesChanged) {
+ // For consistency with previous behaviour, send onLost callbacks before onAvailable.
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ NetworkRequest nr = nri.request;
+ if (!nr.isListen()) continue;
+ if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
+ nai.removeRequest(nri.request.requestId);
+ callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+ }
+ }
+
+ if (capabilitiesChanged) {
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ }
+
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ NetworkRequest nr = nri.request;
+ if (!nr.isListen()) continue;
+ if (nai.satisfies(nr) && !nai.isSatisfyingRequest(nr.requestId)) {
+ nai.addRequest(nr);
+ notifyNetworkCallback(nai, nri);
+ }
+ }
+ }
+
// Handles a network appearing or improving its score.
//
// - Evaluates all current NetworkRequests that can be
@@ -4783,13 +4876,25 @@
boolean keep = newNetwork.isVPN();
boolean isNewDefault = false;
NetworkAgentInfo oldDefaultNetwork = null;
+
+ final boolean wasBackgroundNetwork = newNetwork.isBackgroundNetwork();
+ final int score = newNetwork.getCurrentScore();
+
if (VDBG) log("rematching " + newNetwork.name());
+
// Find and migrate to this Network any NetworkRequests for
// which this network is now the best.
ArrayList<NetworkAgentInfo> affectedNetworks = new ArrayList<NetworkAgentInfo>();
ArrayList<NetworkRequestInfo> addedRequests = new ArrayList<NetworkRequestInfo>();
- if (VDBG) log(" network has: " + newNetwork.networkCapabilities);
+ NetworkCapabilities nc = newNetwork.networkCapabilities;
+ if (VDBG) log(" network has: " + nc);
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ // Process requests in the first pass and listens in the second pass. This allows us to
+ // change a network's capabilities depending on which requests it has. This is only
+ // correct if the change in capabilities doesn't affect whether the network satisfies
+ // requests or not, and doesn't affect the network's score.
+ if (nri.request.isListen()) continue;
+
final NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);
final boolean satisfies = newNetwork.satisfies(nri.request);
if (newNetwork == currentNetwork && satisfies) {
@@ -4804,22 +4909,14 @@
// check if it satisfies the NetworkCapabilities
if (VDBG) log(" checking if request is satisfied: " + nri.request);
if (satisfies) {
- 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);
- continue;
- }
-
// next check if it's better than any current network we're using for
// this request
if (VDBG) {
log("currentScore = " +
(currentNetwork != null ? currentNetwork.getCurrentScore() : 0) +
- ", newScore = " + newNetwork.getCurrentScore());
+ ", newScore = " + score);
}
- if (currentNetwork == null ||
- currentNetwork.getCurrentScore() < newNetwork.getCurrentScore()) {
+ if (currentNetwork == null || currentNetwork.getCurrentScore() < score) {
if (VDBG) log("rematch for " + newNetwork.name());
if (currentNetwork != null) {
if (VDBG) log(" accepting network in place of " + currentNetwork.name());
@@ -4841,7 +4938,7 @@
// TODO - this could get expensive if we have alot of requests for this
// network. Think about if there is a way to reduce this. Push
// netid->request mapping to each factory?
- sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore());
+ sendUpdatedScoreToFactories(nri.request, score);
if (isDefaultRequest(nri)) {
isNewDefault = true;
oldDefaultNetwork = currentNetwork;
@@ -4867,16 +4964,14 @@
mNetworkForRequestId.remove(nri.request.requestId);
sendUpdatedScoreToFactories(nri.request, 0);
} else {
- if (nri.request.isRequest()) {
- Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
- newNetwork.name() +
- " without updating mNetworkForRequestId or factories!");
- }
+ Slog.wtf(TAG, "BUG: Removing request " + nri.request.requestId + " from " +
+ newNetwork.name() +
+ " without updating mNetworkForRequestId or factories!");
}
- // TODO: technically, sending CALLBACK_LOST here is
- // incorrect if nri is a request (not a listen) and there
- // is a replacement network currently connected that can
- // satisfy it. However, the only capability that can both
+ // TODO: Technically, sending CALLBACK_LOST here is
+ // incorrect if there is a replacement network currently
+ // connected that can satisfy nri, which is a request
+ // (not a listen). However, the only capability that can both
// 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.
@@ -4901,6 +4996,28 @@
}
}
+ if (!newNetwork.networkCapabilities.equalRequestableCapabilities(nc)) {
+ Slog.wtf(TAG, String.format(
+ "BUG: %s changed requestable capabilities during rematch: %s -> %s",
+ nc, newNetwork.networkCapabilities));
+ }
+ if (newNetwork.getCurrentScore() != score) {
+ Slog.wtf(TAG, String.format(
+ "BUG: %s changed score during rematch: %d -> %d",
+ score, newNetwork.getCurrentScore()));
+ }
+
+ // Second pass: process all listens.
+ if (wasBackgroundNetwork != newNetwork.isBackgroundNetwork()) {
+ // If the network went from background to foreground or vice versa, we need to update
+ // its foreground state. It is safe to do this after rematching the requests because
+ // NET_CAPABILITY_FOREGROUND does not affect requests, as is not a requestable
+ // capability and does not affect the network's score (see the Slog.wtf call above).
+ updateCapabilities(score, newNetwork, newNetwork.networkCapabilities);
+ } else {
+ processListenRequests(newNetwork, false);
+ }
+
// do this after the default net is switched, but
// before LegacyTypeTracker sends legacy broadcasts
for (NetworkRequestInfo nri : addedRequests) notifyNetworkCallback(newNetwork, nri);
@@ -4978,7 +5095,7 @@
}
if (reapUnvalidatedNetworks == ReapUnvalidatedNetworks.REAP) {
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
- if (unneeded(nai)) {
+ if (unneeded(nai, UnneededFor.TEARDOWN)) {
if (nai.getLingerExpiry() > 0) {
// This network has active linger timers and no requests, but is not
// lingering. Linger it.
@@ -5092,6 +5209,10 @@
if (!networkAgent.created
&& (state == NetworkInfo.State.CONNECTED
|| (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
+
+ // A network that has just connected has zero requests and is thus a foreground network.
+ networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+
try {
// This should never fail. Specifying an already in use NetID will cause failure.
if (networkAgent.isVPN()) {
@@ -5101,9 +5222,7 @@
!networkAgent.networkMisc.allowBypass));
} else {
mNetd.createPhysicalNetwork(networkAgent.network.netId,
- networkAgent.networkCapabilities.hasCapability(
- NET_CAPABILITY_NOT_RESTRICTED) ?
- null : NetworkManagementService.PERMISSION_SYSTEM);
+ getNetworkPermission(networkAgent.networkCapabilities));
}
} catch (Exception e) {
loge("Error creating network " + networkAgent.network.netId + ": "
@@ -5253,6 +5372,8 @@
NetworkRequest nr = networkAgent.requestAt(i);
NetworkRequestInfo nri = mNetworkRequests.get(nr);
if (VDBG) log(" sending notification for " + nr);
+ // TODO: if we're in the middle of a rematch, can we send a CAP_CHANGED callback for
+ // a network that no longer satisfies the listen?
if (nri.mPendingIntent == null) {
callCallbackForRequest(nri, networkAgent, notifyType, arg1);
} else {
@@ -5324,7 +5445,7 @@
@Override
public String getCaptivePortalServerUrl() {
- return NetworkMonitor.getCaptivePortalServerUrl(mContext);
+ return NetworkMonitor.getCaptivePortalServerHttpUrl(mContext);
}
@Override
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 9c48aee..2a618bc 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -104,14 +104,16 @@
// -----------------------------------------------
// If a network has no chance of satisfying any requests (even if it were to become validated
// and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
-// If the network ever for any period of time had satisfied a NetworkRequest (i.e. had been
-// the highest scoring that satisfied the NetworkRequest's constraints), but is no longer the
-// highest scoring network for any NetworkRequest, then there will be a 30s pause before
-// ConnectivityService disconnects the NetworkAgent's AsyncChannel. During this pause the
-// network is considered "lingering". This pause exists to allow network communication to be
-// wrapped up rather than abruptly terminated. During this pause if the network begins satisfying
-// a NetworkRequest, ConnectivityService will cancel the future disconnection of the NetworkAgent's
-// AsyncChannel, and the network is no longer considered "lingering".
+//
+// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that
+// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any
+// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be
+// wrapped up rather than abruptly terminated. During this pause the network is said to be
+// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest,
+// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and
+// the network is no longer considered "lingering". After the linger timer expires, if the network
+// is satisfying one or more background NetworkRequests it is kept up in the background. If it is
+// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.
public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
public NetworkInfo networkInfo;
@@ -230,11 +232,13 @@
// The list of NetworkRequests being satisfied by this Network.
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.
+
// How many of the satisfied requests are actual requests and not listens.
private int mNumRequestNetworkRequests = 0;
+ // How many of the satisfied requests are of type BACKGROUND_REQUEST.
+ private int mNumBackgroundNetworkRequests = 0;
+
public final Messenger messenger;
public final AsyncChannel asyncChannel;
@@ -268,6 +272,32 @@
//
// These functions must only called on ConnectivityService's main thread.
+ private static final boolean ADD = true;
+ private static final boolean REMOVE = false;
+
+ private void updateRequestCounts(boolean add, NetworkRequest request) {
+ int delta = add ? +1 : -1;
+ switch (request.type) {
+ case REQUEST:
+ case TRACK_DEFAULT:
+ mNumRequestNetworkRequests += delta;
+ break;
+
+ case BACKGROUND_REQUEST:
+ mNumRequestNetworkRequests += delta;
+ mNumBackgroundNetworkRequests += delta;
+ break;
+
+ case LISTEN:
+ break;
+
+ case NONE:
+ default:
+ Log.wtf(TAG, "Unhandled request type " + request.type);
+ break;
+ }
+ }
+
/**
* Add {@code networkRequest} to this network as it's satisfied by this network.
* @return true if {@code networkRequest} was added or false if {@code networkRequest} was
@@ -276,9 +306,15 @@
public boolean addRequest(NetworkRequest networkRequest) {
NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
if (existing == networkRequest) return false;
- if (existing != null && existing.isRequest()) mNumRequestNetworkRequests--;
+ if (existing != null) {
+ // Should only happen if the requestId wraps. If that happens lots of other things will
+ // be broken as well.
+ Log.wtf(TAG, String.format("Duplicate requestId for %s and %s on %s",
+ networkRequest, existing, name()));
+ updateRequestCounts(REMOVE, existing);
+ }
mNetworkRequests.put(networkRequest.requestId, networkRequest);
- if (networkRequest.isRequest()) mNumRequestNetworkRequests++;
+ updateRequestCounts(ADD, networkRequest);
return true;
}
@@ -288,9 +324,9 @@
public void removeRequest(int requestId) {
NetworkRequest existing = mNetworkRequests.get(requestId);
if (existing == null) return;
+ updateRequestCounts(REMOVE, existing);
mNetworkRequests.remove(requestId);
if (existing.isRequest()) {
- mNumRequestNetworkRequests--;
unlingerRequest(existing);
}
}
@@ -319,12 +355,37 @@
}
/**
+ * Returns the number of requests currently satisfied by this network of type
+ * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}.
+ */
+ public int numBackgroundNetworkRequests() {
+ return mNumBackgroundNetworkRequests;
+ }
+
+ /**
+ * Returns the number of foreground requests currently satisfied by this network.
+ */
+ public int numForegroundNetworkRequests() {
+ return mNumRequestNetworkRequests - mNumBackgroundNetworkRequests;
+ }
+
+ /**
* Returns the number of requests of any type currently satisfied by this network.
*/
public int numNetworkRequests() {
return mNetworkRequests.size();
}
+ /**
+ * Returns whether the network is a background network. A network is a background network if it
+ * is satisfying no foreground requests and at least one background request. (If it did not have
+ * a background request, it would be a speculative network that is only being kept up because
+ * it might satisfy a request if it validated).
+ */
+ public boolean isBackgroundNetwork() {
+ return !isVPN() && numForegroundNetworkRequests() == 0 && mNumBackgroundNetworkRequests > 0;
+ }
+
// Does this network satisfy request?
public boolean satisfies(NetworkRequest request) {
return created &&
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index a656acc..4af1cf1 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -597,7 +597,7 @@
@Override
protected CaptivePortalProbeResult isCaptivePortal() {
- return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl);
+ return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl, null);
}
}
@@ -746,6 +746,7 @@
}
public void tearDown() throws Exception {
+ setMobileDataAlwaysOn(false);
if (mCellNetworkAgent != null) { mCellNetworkAgent.disconnect(); }
if (mWiFiNetworkAgent != null) { mWiFiNetworkAgent.disconnect(); }
mCellNetworkAgent = mWiFiNetworkAgent = null;
@@ -1843,6 +1844,85 @@
mCm.unregisterNetworkCallback(cellNetworkCallback);
}
+ private void setMobileDataAlwaysOn(boolean enable) {
+ ContentResolver cr = mServiceContext.getContentResolver();
+ Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, enable ? 1 : 0);
+ mService.updateMobileDataAlwaysOn();
+ mService.waitForIdle();
+ }
+
+ private boolean isForegroundNetwork(MockNetworkAgent network) {
+ NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
+ assertNotNull(nc);
+ return nc.hasCapability(NET_CAPABILITY_FOREGROUND);
+ }
+
+ @SmallTest
+ public void testBackgroundNetworks() throws Exception {
+ // Create a background request. We can't do this ourselves because ConnectivityService
+ // doesn't have an API for it. So just turn on mobile data always on.
+ setMobileDataAlwaysOn(true);
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ final NetworkRequest fgRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_FOREGROUND).build();
+ final TestNetworkCallback callback = new TestNetworkCallback();
+ final TestNetworkCallback fgCallback = new TestNetworkCallback();
+ mCm.registerNetworkCallback(request, callback);
+ mCm.registerNetworkCallback(fgRequest, fgCallback);
+
+ mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+ mCellNetworkAgent.connect(true);
+ callback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+ mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+ mWiFiNetworkAgent.connect(true);
+
+ // When wifi connects, cell lingers.
+ callback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ fgCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+ callback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ fgCallback.expectCallback(CallbackState.LOSING, mCellNetworkAgent);
+ assertTrue(isForegroundNetwork(mCellNetworkAgent));
+ assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+ // When lingering is complete, cell is still there but is now in the background.
+ fgCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent, TEST_LINGER_DELAY_MS);
+ callback.assertNoCallback();
+ assertFalse(isForegroundNetwork(mCellNetworkAgent));
+ assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+ // File a cell request and check that cell comes into the foreground.
+ final NetworkRequest cellRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).build();
+ final TestNetworkCallback cellCallback = new TestNetworkCallback();
+ mCm.requestNetwork(cellRequest, cellCallback);
+ cellCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ callback.assertNoCallback(); // Because the network is already up.
+ assertTrue(isForegroundNetwork(mCellNetworkAgent));
+ assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+ // Release the request. The network immediately goes into the background, since it was not
+ // lingering.
+ mCm.unregisterNetworkCallback(cellCallback);
+ fgCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);
+ callback.assertNoCallback();
+ assertFalse(isForegroundNetwork(mCellNetworkAgent));
+ assertTrue(isForegroundNetwork(mWiFiNetworkAgent));
+
+ // Disconnect wifi and check that cell is foreground again.
+ mWiFiNetworkAgent.disconnect();
+ callback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ fgCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+ fgCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+ assertTrue(isForegroundNetwork(mCellNetworkAgent));
+
+ mCm.unregisterNetworkCallback(callback);
+ mCm.unregisterNetworkCallback(fgCallback);
+ }
+
@SmallTest
public void testRequestBenchmark() throws Exception {
// Benchmarks connecting and switching performance in the presence of a large number of
@@ -1948,8 +2028,7 @@
// 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();
+ setMobileDataAlwaysOn(true);
testFactory.waitForNetworkRequests(2);
assertTrue(testFactory.getMyStartRequested());
@@ -1969,8 +2048,7 @@
// 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();
+ setMobileDataAlwaysOn(false);
testFactory.waitForNetworkRequests(1);
// ... and cell data to be torn down.
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
new file mode 100644
index 0000000..aed3635
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityEventBuilderTest.java
@@ -0,0 +1,359 @@
+/*
+ * 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.ConnectivityMetricsEvent;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.DhcpErrorEvent;
+import android.net.metrics.DnsEvent;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.NetworkEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import com.google.protobuf.nano.MessageNano;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
+import static com.android.server.connectivity.metrics.IpConnectivityLogClass.IpConnectivityLog;
+import static com.android.server.connectivity.MetricsTestUtil.aBool;
+import static com.android.server.connectivity.MetricsTestUtil.aByteArray;
+import static com.android.server.connectivity.MetricsTestUtil.aLong;
+import static com.android.server.connectivity.MetricsTestUtil.aString;
+import static com.android.server.connectivity.MetricsTestUtil.aType;
+import static com.android.server.connectivity.MetricsTestUtil.anInt;
+import static com.android.server.connectivity.MetricsTestUtil.anIntArray;
+import static com.android.server.connectivity.MetricsTestUtil.b;
+import static com.android.server.connectivity.MetricsTestUtil.describeIpEvent;
+import static com.android.server.connectivity.MetricsTestUtil.ipEv;
+
+public class IpConnectivityEventBuilderTest extends TestCase {
+
+ public void testDefaultNetworkEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(DefaultNetworkEvent.class),
+ anInt(102),
+ anIntArray(1, 2, 3),
+ anInt(101),
+ aBool(true),
+ aBool(false));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " default_network_event <",
+ " network_id <",
+ " network_id: 102",
+ " >",
+ " previous_network_id <",
+ " network_id: 101",
+ " >",
+ " previous_network_ip_support: 1",
+ " transport_types: 1",
+ " transport_types: 2",
+ " transport_types: 3",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testDhcpClientEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(DhcpClientEvent.class),
+ aString("wlan0"),
+ aString("SomeState"),
+ anInt(192));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " dhcp_event <",
+ " duration_ms: 192",
+ " error_code: 0",
+ " if_name: \"wlan0\"",
+ " state_transition: \"SomeState\"",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testDhcpErrorEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(DhcpErrorEvent.class),
+ aString("wlan0"),
+ anInt(DhcpErrorEvent.L4_NOT_UDP));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " dhcp_event <",
+ " duration_ms: 0",
+ " error_code: 50397184",
+ " if_name: \"wlan0\"",
+ " state_transition: \"\"",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testDnsEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(DnsEvent.class),
+ anInt(101),
+ aByteArray(b(1), b(1), b(2), b(1), b(1), b(1), b(2), b(2)),
+ aByteArray(b(0), b(0), b(22), b(3), b(1), b(0), b(200), b(178)),
+ anIntArray(3456, 267, 1230, 45, 2111, 450, 638, 1300));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " dns_lookup_batch <",
+ " event_types: 1",
+ " event_types: 1",
+ " event_types: 2",
+ " event_types: 1",
+ " event_types: 1",
+ " event_types: 1",
+ " event_types: 2",
+ " event_types: 2",
+ " latencies_ms: 3456",
+ " latencies_ms: 267",
+ " latencies_ms: 1230",
+ " latencies_ms: 45",
+ " latencies_ms: 2111",
+ " latencies_ms: 450",
+ " latencies_ms: 638",
+ " latencies_ms: 1300",
+ " network_id <",
+ " network_id: 101",
+ " >",
+ " return_codes: 0",
+ " return_codes: 0",
+ " return_codes: 22",
+ " return_codes: 3",
+ " return_codes: 1",
+ " return_codes: 0",
+ " return_codes: 200",
+ " return_codes: 178",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testIpManagerEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(IpManagerEvent.class),
+ aString("wlan0"),
+ anInt(IpManagerEvent.PROVISIONING_OK),
+ aLong(5678));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " ip_provisioning_event <",
+ " event_type: 1",
+ " if_name: \"wlan0\"",
+ " latency_ms: 5678",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testIpReachabilityEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(IpReachabilityEvent.class),
+ aString("wlan0"),
+ anInt(IpReachabilityEvent.NUD_FAILED));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " ip_reachability_event <",
+ " event_type: 512",
+ " if_name: \"wlan0\"",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testNetworkEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(NetworkEvent.class),
+ anInt(100),
+ anInt(5),
+ aLong(20410));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " network_event <",
+ " event_type: 5",
+ " latency_ms: 20410",
+ " network_id <",
+ " network_id: 100",
+ " >",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testValidationProbeEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(ValidationProbeEvent.class),
+ anInt(120),
+ aLong(40730),
+ anInt(ValidationProbeEvent.PROBE_HTTP),
+ anInt(204));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " time_ms: 1",
+ " validation_probe_event <",
+ " latency_ms: 40730",
+ " network_id <",
+ " network_id: 120",
+ " >",
+ " probe_result: 204",
+ " probe_type: 1",
+ " >",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testApfProgramEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(ApfProgramEvent.class),
+ aLong(200),
+ anInt(7),
+ anInt(9),
+ anInt(2048),
+ anInt(3));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " apf_program_event <",
+ " current_ras: 9",
+ " drop_multicast: true",
+ " filtered_ras: 7",
+ " has_ipv4_addr: true",
+ " lifetime: 200",
+ " program_length: 2048",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testApfStatsSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(ApfStats.class),
+ aLong(45000),
+ anInt(10),
+ anInt(2),
+ anInt(2),
+ anInt(1),
+ anInt(2),
+ anInt(4),
+ anInt(2048));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " apf_statistics <",
+ " dropped_ras: 2",
+ " duration_ms: 45000",
+ " matching_ras: 2",
+ " max_program_size: 2048",
+ " parse_errors: 2",
+ " program_updates: 4",
+ " received_ras: 10",
+ " zero_lifetime_ras: 1",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ public void testRaEventSerialization() {
+ ConnectivityMetricsEvent ev = describeIpEvent(
+ aType(RaEvent.class),
+ aLong(2000),
+ aLong(400),
+ aLong(300),
+ aLong(-1),
+ aLong(1000),
+ aLong(-1));
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " ra_event <",
+ " dnssl_lifetime: -1",
+ " prefix_preferred_lifetime: 300",
+ " prefix_valid_lifetime: 400",
+ " rdnss_lifetime: 1000",
+ " route_info_lifetime: -1",
+ " router_lifetime: 2000",
+ " >",
+ " time_ms: 1",
+ ">");
+
+ verifySerialization(want, ev);
+ }
+
+ static void verifySerialization(String want, ConnectivityMetricsEvent... input) {
+ try {
+ byte[] got = IpConnectivityEventBuilder.serialize(0, Arrays.asList(input));
+ IpConnectivityLog log = new IpConnectivityLog();
+ MessageNano.mergeFrom(log, got);
+ assertEquals(want, log.toString());
+ } catch (Exception e) {
+ fail(e.toString());
+ }
+ }
+
+ static String joinLines(String ... elems) {
+ StringBuilder b = new StringBuilder();
+ for (String s : elems) {
+ b.append(s);
+ b.append("\n");
+ }
+ return b.toString();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java
new file mode 100644
index 0000000..3fc89b9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/IpConnectivityMetricsTest.java
@@ -0,0 +1,269 @@
+/*
+ * 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.content.Context;
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.metrics.ApfStats;
+import android.net.metrics.DefaultNetworkEvent;
+import android.net.metrics.DhcpClientEvent;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.IpManagerEvent;
+import android.net.metrics.IpReachabilityEvent;
+import android.net.metrics.RaEvent;
+import android.net.metrics.ValidationProbeEvent;
+import android.os.Parcelable;
+import android.util.Base64;
+import com.android.server.connectivity.metrics.IpConnectivityLogClass;
+import com.google.protobuf.nano.MessageNano;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import junit.framework.TestCase;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class IpConnectivityMetricsTest extends TestCase {
+ static final IpReachabilityEvent FAKE_EV =
+ new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED);
+
+ @Mock Context mCtx;
+ @Mock IIpConnectivityMetrics mMockService;
+
+ IpConnectivityMetrics mService;
+
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mService = new IpConnectivityMetrics(mCtx);
+ }
+
+ public void testLoggingEvents() throws Exception {
+ IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+ assertTrue(logger.log(1, FAKE_EV));
+ assertTrue(logger.log(2, FAKE_EV));
+ assertTrue(logger.log(3, FAKE_EV));
+
+ List<ConnectivityMetricsEvent> got = verifyEvents(3);
+ assertEventsEqual(expectedEvent(1), got.get(0));
+ assertEventsEqual(expectedEvent(2), got.get(1));
+ assertEventsEqual(expectedEvent(3), got.get(2));
+ }
+
+ public void testLoggingEventsWithMultipleCallers() throws Exception {
+ IpConnectivityLog logger = new IpConnectivityLog(mMockService);
+
+ final int nCallers = 10;
+ final int nEvents = 10;
+ for (int n = 0; n < nCallers; n++) {
+ final int i = n;
+ new Thread() {
+ public void run() {
+ for (int j = 0; j < nEvents; j++) {
+ assertTrue(logger.log(i * 100 + j, FAKE_EV));
+ }
+ }
+ }.start();
+ }
+
+ List<ConnectivityMetricsEvent> got = verifyEvents(nCallers * nEvents, 100);
+ Collections.sort(got, EVENT_COMPARATOR);
+ Iterator<ConnectivityMetricsEvent> iter = got.iterator();
+ for (int i = 0; i < nCallers; i++) {
+ for (int j = 0; j < nEvents; j++) {
+ int expectedTimestamp = i * 100 + j;
+ assertEventsEqual(expectedEvent(expectedTimestamp), iter.next());
+ }
+ }
+ }
+
+ public void testBufferFlushing() {
+ String output1 = getdump("flush");
+ assertEquals("", output1);
+
+ new IpConnectivityLog(mService.impl).log(1, FAKE_EV);
+ String output2 = getdump("flush");
+ assertFalse("".equals(output2));
+
+ String output3 = getdump("flush");
+ assertEquals("", output3);
+ }
+
+ public void testEndToEndLogging() {
+ IpConnectivityLog logger = new IpConnectivityLog(mService.impl);
+
+ Parcelable[] events = {
+ new IpReachabilityEvent("wlan0", IpReachabilityEvent.NUD_FAILED),
+ new DhcpClientEvent("wlan0", "SomeState", 192),
+ new DefaultNetworkEvent(102, new int[]{1,2,3}, 101, true, false),
+ new IpManagerEvent("wlan0", IpManagerEvent.PROVISIONING_OK, 5678),
+ new ValidationProbeEvent(120, 40730, ValidationProbeEvent.PROBE_HTTP, 204),
+ new ApfStats(45000, 10, 2, 2, 1, 2, 4, 2048),
+ new RaEvent(2000, 400, 300, -1, 1000, -1)
+ };
+
+ for (int i = 0; i < events.length; i++) {
+ logger.log(100 * (i + 1), events[i]);
+ }
+
+ String want = joinLines(
+ "dropped_events: 0",
+ "events <",
+ " ip_reachability_event <",
+ " event_type: 512",
+ " if_name: \"wlan0\"",
+ " >",
+ " time_ms: 100",
+ ">",
+ "events <",
+ " dhcp_event <",
+ " duration_ms: 192",
+ " error_code: 0",
+ " if_name: \"wlan0\"",
+ " state_transition: \"SomeState\"",
+ " >",
+ " time_ms: 200",
+ ">",
+ "events <",
+ " default_network_event <",
+ " network_id <",
+ " network_id: 102",
+ " >",
+ " previous_network_id <",
+ " network_id: 101",
+ " >",
+ " previous_network_ip_support: 1",
+ " transport_types: 1",
+ " transport_types: 2",
+ " transport_types: 3",
+ " >",
+ " time_ms: 300",
+ ">",
+ "events <",
+ " ip_provisioning_event <",
+ " event_type: 1",
+ " if_name: \"wlan0\"",
+ " latency_ms: 5678",
+ " >",
+ " time_ms: 400",
+ ">",
+ "events <",
+ " time_ms: 500",
+ " validation_probe_event <",
+ " latency_ms: 40730",
+ " network_id <",
+ " network_id: 120",
+ " >",
+ " probe_result: 204",
+ " probe_type: 1",
+ " >",
+ ">",
+ "events <",
+ " apf_statistics <",
+ " dropped_ras: 2",
+ " duration_ms: 45000",
+ " matching_ras: 2",
+ " max_program_size: 2048",
+ " parse_errors: 2",
+ " program_updates: 4",
+ " received_ras: 10",
+ " zero_lifetime_ras: 1",
+ " >",
+ " time_ms: 600",
+ ">",
+ "events <",
+ " ra_event <",
+ " dnssl_lifetime: -1",
+ " prefix_preferred_lifetime: 300",
+ " prefix_valid_lifetime: 400",
+ " rdnss_lifetime: 1000",
+ " route_info_lifetime: -1",
+ " router_lifetime: 2000",
+ " >",
+ " time_ms: 700",
+ ">");
+
+ verifySerialization(want, getdump("flush"));
+ }
+
+ String getdump(String ... command) {
+ StringWriter buffer = new StringWriter();
+ PrintWriter writer = new PrintWriter(buffer);
+ mService.impl.dump(null, writer, command);
+ return buffer.toString();
+ }
+
+ List<ConnectivityMetricsEvent> verifyEvents(int n, int timeoutMs) throws Exception {
+ ArgumentCaptor<ConnectivityMetricsEvent> captor =
+ ArgumentCaptor.forClass(ConnectivityMetricsEvent.class);
+ verify(mMockService, timeout(timeoutMs).times(n)).logEvent(captor.capture());
+ return captor.getAllValues();
+ }
+
+ List<ConnectivityMetricsEvent> verifyEvents(int n) throws Exception {
+ return verifyEvents(n, 10);
+ }
+
+ static void verifySerialization(String want, String output) {
+ try {
+ byte[] got = Base64.decode(output, Base64.DEFAULT);
+ IpConnectivityLogClass.IpConnectivityLog log =
+ new IpConnectivityLogClass.IpConnectivityLog();
+ MessageNano.mergeFrom(log, got);
+ assertEquals(want, log.toString());
+ } catch (Exception e) {
+ fail(e.toString());
+ }
+ }
+
+ static String joinLines(String ... elems) {
+ StringBuilder b = new StringBuilder();
+ for (String s : elems) {
+ b.append(s).append("\n");
+ }
+ return b.toString();
+ }
+
+ static ConnectivityMetricsEvent expectedEvent(int timestamp) {
+ return new ConnectivityMetricsEvent((long)timestamp, 0, 0, FAKE_EV);
+ }
+
+ /** Outer equality for ConnectivityMetricsEvent to avoid overriding equals() and hashCode(). */
+ static void assertEventsEqual(ConnectivityMetricsEvent expected, ConnectivityMetricsEvent got) {
+ assertEquals(expected.timestamp, got.timestamp);
+ assertEquals(expected.componentTag, got.componentTag);
+ assertEquals(expected.eventTag, got.eventTag);
+ assertEquals(expected.data, got.data);
+ }
+
+ static final Comparator<ConnectivityMetricsEvent> EVENT_COMPARATOR =
+ new Comparator<ConnectivityMetricsEvent>() {
+ @Override
+ public int compare(ConnectivityMetricsEvent ev1, ConnectivityMetricsEvent ev2) {
+ return (int) (ev1.timestamp - ev2.timestamp);
+ }
+ };
+}
diff --git a/services/tests/servicestests/src/com/android/server/connectivity/MetricsTestUtil.java b/services/tests/servicestests/src/com/android/server/connectivity/MetricsTestUtil.java
new file mode 100644
index 0000000..e201012
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/connectivity/MetricsTestUtil.java
@@ -0,0 +1,120 @@
+/*
+ * 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.ConnectivityMetricsEvent;
+import android.net.ConnectivityMetricsLogger;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+abstract public class MetricsTestUtil {
+ private MetricsTestUtil() {
+ }
+
+ static ConnectivityMetricsEvent ipEv(Parcelable p) {
+ return ev(ConnectivityMetricsLogger.COMPONENT_TAG_CONNECTIVITY, p);
+ }
+
+ static ConnectivityMetricsEvent telephonyEv() {
+ return ev(ConnectivityMetricsLogger.COMPONENT_TAG_TELEPHONY, new Bundle());
+ }
+
+ static ConnectivityMetricsEvent ev(int tag, Parcelable p) {
+ return new ConnectivityMetricsEvent(1L, tag, 0, p);
+ }
+
+ // Utiliy interface for describing the content of a Parcel. This relies on
+ // the implementation defails of Parcelable and on the fact that the fully
+ // qualified Parcelable class names are written as string in the Parcels.
+ interface ParcelField {
+ void write(Parcel p);
+ }
+
+ static ConnectivityMetricsEvent describeIpEvent(ParcelField... fs) {
+ Parcel p = Parcel.obtain();
+ for (ParcelField f : fs) {
+ f.write(p);
+ }
+ p.setDataPosition(0);
+ return ipEv(p.readParcelable(ClassLoader.getSystemClassLoader()));
+ }
+
+ static ParcelField aType(Class<?> c) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeString(c.getName());
+ }
+ };
+ }
+
+ static ParcelField aBool(boolean b) {
+ return aByte((byte) (b ? 1 : 0));
+ }
+
+ static ParcelField aByte(byte b) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeByte(b);
+ }
+ };
+ }
+
+ static ParcelField anInt(int i) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeInt(i);
+ }
+ };
+ }
+
+ static ParcelField aLong(long l) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeLong(l);
+ }
+ };
+ }
+
+ static ParcelField aString(String s) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeString(s);
+ }
+ };
+ }
+
+ static ParcelField aByteArray(byte... ary) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeByteArray(ary);
+ }
+ };
+ }
+
+ static ParcelField anIntArray(int... ary) {
+ return new ParcelField() {
+ public void write(Parcel p) {
+ p.writeIntArray(ary);
+ }
+ };
+ }
+
+ static byte b(int i) {
+ return (byte) i;
+ }
+}