Implement ConnectivityStateMetrics sample
Test: ConnectivitySampleMetricsTest
Change-Id: I0afdda023208c3f8620cb5b89add66448af596d7
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 8e219a6..abda1fa 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -260,6 +260,19 @@
private int mEnterpriseId;
/**
+ * Gets the enterprise IDs as an int. Internal callers only.
+ *
+ * DO NOT USE THIS if not immediately collapsing back into a scalar. Instead,
+ * prefer getEnterpriseIds/hasEnterpriseId.
+ *
+ * @return the internal, version-dependent int representing enterprise ids
+ * @hide
+ */
+ public int getEnterpriseIdsInternal() {
+ return mEnterpriseId;
+ }
+
+ /**
* Get enteprise identifiers set.
*
* Get all the enterprise capabilities identifier set on this {@code NetworkCapability}
@@ -741,8 +754,10 @@
/**
* Capabilities that are managed by ConnectivityService.
+ * @hide
*/
- private static final long CONNECTIVITY_MANAGED_CAPABILITIES =
+ @VisibleForTesting
+ public static final long CONNECTIVITY_MANAGED_CAPABILITIES =
BitUtils.packBitList(
NET_CAPABILITY_VALIDATED,
NET_CAPABILITY_CAPTIVE_PORTAL,
@@ -859,6 +874,19 @@
}
/**
+ * Gets the capabilities as an int. Internal callers only.
+ *
+ * DO NOT USE THIS if not immediately collapsing back into a scalar. Instead,
+ * prefer getCapabilities/hasCapability.
+ *
+ * @return an internal, version-dependent int representing the capabilities
+ * @hide
+ */
+ public long getCapabilitiesInternal() {
+ return mNetworkCapabilities;
+ }
+
+ /**
* Gets all the capabilities set on this {@code NetworkCapability} instance.
*
* @return an array of capability values for this instance.
diff --git a/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java b/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java
new file mode 100644
index 0000000..93d1d5d
--- /dev/null
+++ b/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 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.metrics;
+
+import android.annotation.NonNull;
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A class to register, sample and send connectivity state metrics.
+ */
+public class ConnectivitySampleMetricsHelper implements StatsManager.StatsPullAtomCallback {
+ private static final String TAG = ConnectivitySampleMetricsHelper.class.getSimpleName();
+
+ final Supplier<StatsEvent> mDelegate;
+
+ /**
+ * Start collecting metrics.
+ * @param context some context to get services
+ * @param connectivityServiceHandler the connectivity service handler
+ * @param atomTag the tag to collect metrics from
+ * @param delegate a method returning data when called on the handler thread
+ */
+ // Unfortunately it seems essentially impossible to unit test this method. The only thing
+ // to test is that there is a call to setPullAtomCallback, but StatsManager is final and
+ // can't be mocked without mockito-extended. Using mockito-extended in FrameworksNetTests
+ // would have a very large impact on performance, while splitting the unit test for this
+ // class in a separate target would make testing very hard to manage. Therefore, there
+ // can unfortunately be no unit tests for this method, but at least it is very simple.
+ public static void start(@NonNull final Context context,
+ @NonNull final Handler connectivityServiceHandler,
+ final int atomTag,
+ @NonNull final Supplier<StatsEvent> delegate) {
+ final ConnectivitySampleMetricsHelper metrics =
+ new ConnectivitySampleMetricsHelper(delegate);
+ final StatsManager mgr = context.getSystemService(StatsManager.class);
+ if (null == mgr) return; // No metrics for you
+ mgr.setPullAtomCallback(atomTag, null /* metadata */,
+ new HandlerExecutor(connectivityServiceHandler), metrics);
+ }
+
+ public ConnectivitySampleMetricsHelper(@NonNull final Supplier<StatsEvent> delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public int onPullAtom(final int atomTag, final List<StatsEvent> data) {
+ Log.d(TAG, "Sampling data for atom : " + atomTag);
+ data.add(mDelegate.get());
+ return StatsManager.PULL_SUCCESS;
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 6770a8f..85507f6 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -77,6 +77,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5;
@@ -102,6 +103,7 @@
import static com.android.net.module.util.PermissionUtils.enforceAnyPermissionOf;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermission;
import static com.android.net.module.util.PermissionUtils.enforceNetworkStackPermissionOr;
+import static com.android.server.ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE;
import static java.util.Map.Entry;
@@ -235,6 +237,9 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
+import android.stats.connectivity.MeteredState;
+import android.stats.connectivity.RequestType;
+import android.stats.connectivity.ValidatedState;
import android.sysprop.NetworkProperties;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
@@ -247,6 +252,7 @@
import android.util.Range;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.StatsEvent;
import androidx.annotation.RequiresApi;
@@ -255,6 +261,16 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.MessageUtils;
+import com.android.metrics.ConnectionDurationForTransports;
+import com.android.metrics.ConnectionDurationPerTransports;
+import com.android.metrics.ConnectivitySampleMetricsHelper;
+import com.android.metrics.ConnectivityStateSample;
+import com.android.metrics.NetworkCountForTransports;
+import com.android.metrics.NetworkCountPerTransports;
+import com.android.metrics.NetworkDescription;
+import com.android.metrics.NetworkList;
+import com.android.metrics.NetworkRequestCount;
+import com.android.metrics.RequestCountForType;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
@@ -337,6 +353,7 @@
import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -2340,6 +2357,134 @@
return out;
}
+ // Because StatsEvent is not usable in tests (everything inside it is hidden), this
+ // method is used to convert a ConnectivityStateSample into a StatsEvent, so that tests
+ // can call sampleConnectivityState and make the checks on it.
+ @NonNull
+ private StatsEvent sampleConnectivityStateToStatsEvent() {
+ final ConnectivityStateSample sample = sampleConnectivityState();
+ return ConnectivityStatsLog.buildStatsEvent(
+ ConnectivityStatsLog.CONNECTIVITY_STATE_SAMPLE,
+ sample.getNetworkCountPerTransports().toByteArray(),
+ sample.getConnectionDurationPerTransports().toByteArray(),
+ sample.getNetworkRequestCount().toByteArray(),
+ sample.getNetworks().toByteArray());
+ }
+
+ /**
+ * Gather and return a snapshot of the current connectivity state, to be used as a sample.
+ *
+ * This is used for metrics. These snapshots will be sampled and constitute a base for
+ * statistics about connectivity state of devices.
+ */
+ @VisibleForTesting
+ @NonNull
+ public ConnectivityStateSample sampleConnectivityState() {
+ ensureRunningOnConnectivityServiceThread();
+ final ConnectivityStateSample.Builder builder = ConnectivityStateSample.newBuilder();
+ builder.setNetworkCountPerTransports(sampleNetworkCount(mNetworkAgentInfos));
+ builder.setConnectionDurationPerTransports(sampleConnectionDuration(mNetworkAgentInfos));
+ builder.setNetworkRequestCount(sampleNetworkRequestCount(mNetworkRequests.values()));
+ builder.setNetworks(sampleNetworks(mNetworkAgentInfos));
+ return builder.build();
+ }
+
+ private static NetworkCountPerTransports sampleNetworkCount(
+ @NonNull final ArraySet<NetworkAgentInfo> nais) {
+ final SparseIntArray countPerTransports = new SparseIntArray();
+ for (final NetworkAgentInfo nai : nais) {
+ int transports = (int) nai.networkCapabilities.getTransportTypesInternal();
+ countPerTransports.put(transports, 1 + countPerTransports.get(transports, 0));
+ }
+ final NetworkCountPerTransports.Builder builder = NetworkCountPerTransports.newBuilder();
+ for (int i = countPerTransports.size() - 1; i >= 0; --i) {
+ final NetworkCountForTransports.Builder c = NetworkCountForTransports.newBuilder();
+ c.setTransportTypes(countPerTransports.keyAt(i));
+ c.setNetworkCount(countPerTransports.valueAt(i));
+ builder.addNetworkCountForTransports(c);
+ }
+ return builder.build();
+ }
+
+ private static ConnectionDurationPerTransports sampleConnectionDuration(
+ @NonNull final ArraySet<NetworkAgentInfo> nais) {
+ final ConnectionDurationPerTransports.Builder builder =
+ ConnectionDurationPerTransports.newBuilder();
+ for (final NetworkAgentInfo nai : nais) {
+ final ConnectionDurationForTransports.Builder c =
+ ConnectionDurationForTransports.newBuilder();
+ c.setTransportTypes((int) nai.networkCapabilities.getTransportTypesInternal());
+ final long durationMillis = SystemClock.elapsedRealtime() - nai.getConnectedTime();
+ final long millisPerSecond = TimeUnit.SECONDS.toMillis(1);
+ // Add millisPerSecond/2 to round up or down to the nearest value
+ c.setDurationSec((int) ((durationMillis + millisPerSecond / 2) / millisPerSecond));
+ builder.addConnectionDurationForTransports(c);
+ }
+ return builder.build();
+ }
+
+ private static NetworkRequestCount sampleNetworkRequestCount(
+ @NonNull final Collection<NetworkRequestInfo> nris) {
+ final NetworkRequestCount.Builder builder = NetworkRequestCount.newBuilder();
+ final SparseIntArray countPerType = new SparseIntArray();
+ for (final NetworkRequestInfo nri : nris) {
+ final int type;
+ if (Process.SYSTEM_UID == nri.mAsUid) {
+ // The request is filed "as" the system, so it's the system on its own behalf.
+ type = RequestType.RT_SYSTEM.getNumber();
+ } else if (Process.SYSTEM_UID == nri.mUid) {
+ // The request is filed by the system as some other app, so it's the system on
+ // behalf of an app.
+ type = RequestType.RT_SYSTEM_ON_BEHALF_OF_APP.getNumber();
+ } else {
+ // Not the system, so it's an app requesting on its own behalf.
+ type = RequestType.RT_APP.getNumber();
+ }
+ countPerType.put(type, countPerType.get(type, 0));
+ }
+ for (int i = countPerType.size() - 1; i >= 0; --i) {
+ final RequestCountForType.Builder r = RequestCountForType.newBuilder();
+ r.setRequestType(RequestType.forNumber(countPerType.keyAt(i)));
+ r.setRequestCount(countPerType.valueAt(i));
+ builder.addRequestCountForType(r);
+ }
+ return builder.build();
+ }
+
+ private static NetworkList sampleNetworks(@NonNull final ArraySet<NetworkAgentInfo> nais) {
+ final NetworkList.Builder builder = NetworkList.newBuilder();
+ for (final NetworkAgentInfo nai : nais) {
+ final NetworkCapabilities nc = nai.networkCapabilities;
+ final NetworkDescription.Builder d = NetworkDescription.newBuilder();
+ d.setTransportTypes((int) nc.getTransportTypesInternal());
+ final MeteredState meteredState;
+ if (nc.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)) {
+ meteredState = MeteredState.METERED_TEMPORARILY_UNMETERED;
+ } else if (nc.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+ meteredState = MeteredState.METERED_NO;
+ } else {
+ meteredState = MeteredState.METERED_YES;
+ }
+ d.setMeteredState(meteredState);
+ final ValidatedState validatedState;
+ if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
+ validatedState = ValidatedState.VS_PORTAL;
+ } else if (nc.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)) {
+ validatedState = ValidatedState.VS_PARTIAL;
+ } else if (nc.hasCapability(NET_CAPABILITY_VALIDATED)) {
+ validatedState = ValidatedState.VS_VALID;
+ } else {
+ validatedState = ValidatedState.VS_INVALID;
+ }
+ d.setValidatedState(validatedState);
+ d.setScorePolicies(nai.getScore().getPoliciesInternal());
+ d.setCapabilities(nc.getCapabilitiesInternal());
+ d.setEnterpriseId(nc.getEnterpriseIdsInternal());
+ builder.addNetworkDescription(d);
+ }
+ return builder.build();
+ }
+
@Override
public boolean isNetworkSupported(int networkType) {
enforceAccessPermission();
@@ -3453,6 +3598,8 @@
if (mDeps.isAtLeastT()) {
mBpfNetMaps.setPullAtomCallback(mContext);
}
+ ConnectivitySampleMetricsHelper.start(mContext, mHandler,
+ CONNECTIVITY_STATE_SAMPLE, this::sampleConnectivityStateToStatsEvent);
// Wait PermissionMonitor to finish the permission update. Then MultipathPolicyTracker won't
// have permission problem. While CV#block() is unbounded in time and can in principle block
// forever, this replaces a synchronous call to PermissionMonitor#startMonitoring, which
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
index 87ae0c9..648f3bf 100644
--- a/service/src/com/android/server/connectivity/FullScore.java
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -124,7 +124,7 @@
new Class[]{FullScore.class, NetworkScore.class}, new String[]{"POLICY_"});
@VisibleForTesting
- static @NonNull String policyNameOf(final int policy) {
+ public static @NonNull String policyNameOf(final int policy) {
final String name = sMessageNames.get(policy);
if (name == null) {
// Don't throw here because name might be null due to proguard stripping out the
@@ -304,6 +304,18 @@
}
/**
+ * Gets the policies as an long. Internal callers only.
+ *
+ * DO NOT USE if not immediately collapsing back into a scalar. Instead, use
+ * {@link #hasPolicy}.
+ * @return the internal, version-dependent int representing the policies.
+ * @hide
+ */
+ public long getPoliciesInternal() {
+ return mPolicies;
+ }
+
+ /**
* @return whether this score has a particular policy.
*/
@VisibleForTesting
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 845c04c..bdd841f 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -1105,6 +1105,11 @@
* already present.
*/
public boolean addRequest(NetworkRequest networkRequest) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread: "
+ + Thread.currentThread().getName());
+ }
NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
if (existing == networkRequest) return false;
if (existing != null) {
@@ -1123,6 +1128,11 @@
* Remove the specified request from this network.
*/
public void removeRequest(int requestId) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread: "
+ + Thread.currentThread().getName());
+ }
NetworkRequest existing = mNetworkRequests.get(requestId);
if (existing == null) return;
updateRequestCounts(REMOVE, existing);
@@ -1144,6 +1154,11 @@
* network.
*/
public NetworkRequest requestAt(int index) {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread: "
+ + Thread.currentThread().getName());
+ }
return mNetworkRequests.valueAt(index);
}
@@ -1174,6 +1189,11 @@
* Returns the number of requests of any type currently satisfied by this network.
*/
public int numNetworkRequests() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread: "
+ + Thread.currentThread().getName());
+ }
return mNetworkRequests.size();
}
diff --git a/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
new file mode 100644
index 0000000..3043d50
--- /dev/null
+++ b/tests/unit/java/com/android/metrics/ConnectivitySampleMetricsTest.kt
@@ -0,0 +1,173 @@
+package com.android.metrics
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.CONNECTIVITY_MANAGED_CAPABILITIES
+import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE
+import android.net.NetworkCapabilities.NET_CAPABILITY_IMS
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1
+import android.net.NetworkCapabilities.NET_ENTERPRISE_ID_3
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkScore
+import android.net.NetworkScore.POLICY_EXITING
+import android.net.NetworkScore.POLICY_TRANSPORT_PRIMARY
+import android.os.Build
+import android.os.Handler
+import android.stats.connectivity.MeteredState
+import android.stats.connectivity.ValidatedState
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.BitUtils
+import com.android.server.CSTest
+import com.android.server.FromS
+import com.android.server.connectivity.FullScore
+import com.android.server.connectivity.FullScore.POLICY_IS_UNMETERED
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CompletableFuture
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+private fun <T> Handler.onHandler(f: () -> T): T {
+ val future = CompletableFuture<T>()
+ post { future.complete(f()) }
+ return future.get()
+}
+
+private fun flags(vararg flags: Int) = flags.fold(0L) { acc, it -> acc or (1L shl it) }
+
+private fun Number.toTransportsString() = StringBuilder().also { sb ->
+ BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+ { NetworkCapabilities.transportNameOf(it) }, "|") }.toString()
+
+private fun Number.toCapsString() = StringBuilder().also { sb ->
+ BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+ { NetworkCapabilities.capabilityNameOf(it) }, "&") }.toString()
+
+private fun Number.toPolicyString() = StringBuilder().also {sb ->
+ BitUtils.appendStringRepresentationOfBitMaskToStringBuilder(sb, this.toLong(),
+ { FullScore.policyNameOf(it) }, "|") }.toString()
+
+private fun Number.exceptCSManaged() = this.toLong() and CONNECTIVITY_MANAGED_CAPABILITIES.inv()
+
+private val NetworkCapabilities.meteredState get() = when {
+ hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) ->
+ MeteredState.METERED_TEMPORARILY_UNMETERED
+ hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ->
+ MeteredState.METERED_NO
+ else ->
+ MeteredState.METERED_YES
+}
+
+private val NetworkCapabilities.validatedState get() = when {
+ hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) -> ValidatedState.VS_PORTAL
+ hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY) -> ValidatedState.VS_PARTIAL
+ hasCapability(NET_CAPABILITY_VALIDATED) -> ValidatedState.VS_VALID
+ else -> ValidatedState.VS_INVALID
+}
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class ConnectivitySampleMetricsTest : CSTest() {
+ @Test
+ fun testSampleConnectivityState() {
+ val wifi1Caps = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .build()
+ val wifi1Score = NetworkScore.Builder().setExiting(true).build()
+ val agentWifi1 = Agent(nc = wifi1Caps, score = FromS(wifi1Score)).also { it.connect() }
+
+ val wifi2Caps = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_ENTERPRISE)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addEnterpriseId(NET_ENTERPRISE_ID_3)
+ .build()
+ val wifi2Score = NetworkScore.Builder().setTransportPrimary(true).build()
+ val agentWifi2 = Agent(nc = wifi2Caps, score = FromS(wifi2Score)).also { it.connect() }
+
+ val cellCaps = NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_IMS)
+ .addCapability(NET_CAPABILITY_ENTERPRISE)
+ .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addEnterpriseId(NET_ENTERPRISE_ID_1)
+ .build()
+ val cellScore = NetworkScore.Builder().build()
+ val agentCell = Agent(nc = cellCaps, score = FromS(cellScore)).also { it.connect() }
+
+ val stats = csHandler.onHandler { service.sampleConnectivityState() }
+ assertEquals(3, stats.networks.networkDescriptionList.size)
+ val foundCell = stats.networks.networkDescriptionList.find {
+ it.transportTypes == (1 shl TRANSPORT_CELLULAR)
+ } ?: fail("Can't find cell network (searching by transport)")
+ val foundWifi1 = stats.networks.networkDescriptionList.find {
+ it.transportTypes == (1 shl TRANSPORT_WIFI) &&
+ 0L != (it.capabilities and (1L shl NET_CAPABILITY_NOT_METERED))
+ } ?: fail("Can't find wifi1 (searching by WIFI transport and the NOT_METERED capability)")
+ val foundWifi2 = stats.networks.networkDescriptionList.find {
+ it.transportTypes == (1 shl TRANSPORT_WIFI) &&
+ 0L != (it.capabilities and (1L shl NET_CAPABILITY_ENTERPRISE))
+ } ?: fail("Can't find wifi2 (searching by WIFI transport and the ENTERPRISE capability)")
+
+ fun checkNetworkDescription(
+ network: String,
+ found: NetworkDescription,
+ expected: NetworkCapabilities
+ ) {
+ assertEquals(expected.transportTypesInternal, found.transportTypes.toLong(),
+ "Transports differ for network $network, " +
+ "expected ${expected.transportTypesInternal.toTransportsString()}, " +
+ "found ${found.transportTypes.toTransportsString()}")
+ val expectedCaps = expected.capabilitiesInternal.exceptCSManaged()
+ val foundCaps = found.capabilities.exceptCSManaged()
+ assertEquals(expectedCaps, foundCaps,
+ "Capabilities differ for network $network, " +
+ "expected ${expectedCaps.toCapsString()}, " +
+ "found ${foundCaps.toCapsString()}")
+ assertEquals(expected.enterpriseIdsInternal, found.enterpriseId,
+ "Enterprise IDs differ for network $network, " +
+ "expected ${expected.enterpriseIdsInternal}," +
+ " found ${found.enterpriseId}")
+ assertEquals(expected.meteredState, found.meteredState,
+ "Metered states differ for network $network, " +
+ "expected ${expected.meteredState}, " +
+ "found ${found.meteredState}")
+ assertEquals(expected.validatedState, found.validatedState,
+ "Validated states differ for network $network, " +
+ "expected ${expected.validatedState}, " +
+ "found ${found.validatedState}")
+ }
+
+ checkNetworkDescription("Cell network", foundCell, cellCaps)
+ checkNetworkDescription("Wifi1", foundWifi1, wifi1Caps)
+ checkNetworkDescription("Wifi2", foundWifi2, wifi2Caps)
+
+ assertEquals(0, foundCell.scorePolicies, "Cell score policies incorrect, expected 0, " +
+ "found ${foundCell.scorePolicies.toPolicyString()}")
+ val expectedWifi1Policies = flags(POLICY_EXITING, POLICY_IS_UNMETERED)
+ assertEquals(expectedWifi1Policies, foundWifi1.scorePolicies,
+ "Wifi1 score policies incorrect, " +
+ "expected ${expectedWifi1Policies.toPolicyString()}, " +
+ "found ${foundWifi1.scorePolicies.toPolicyString()}")
+ val expectedWifi2Policies = flags(POLICY_TRANSPORT_PRIMARY)
+ assertEquals(expectedWifi2Policies, foundWifi2.scorePolicies,
+ "Wifi2 score policies incorrect, " +
+ "expected ${expectedWifi2Policies.toPolicyString()}, " +
+ "found ${foundWifi2.scorePolicies.toPolicyString()}")
+ }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 3243033..2fccdcb 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -755,6 +755,9 @@
if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
if (Context.ACTIVITY_SERVICE.equals(name)) return mActivityManager;
if (Context.TELEPHONY_SUBSCRIPTION_SERVICE.equals(name)) return mSubscriptionManager;
+ // StatsManager is final and can't be mocked, and uses static methods for mostly
+ // everything. The simplest fix is to return null and not have metrics in tests.
+ if (Context.STATS_MANAGER.equals(name)) return null;
return super.getSystemService(name);
}