diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java index 8e219a687b..abda1fa71f 100644 --- a/framework/src/android/net/NetworkCapabilities.java +++ b/framework/src/android/net/NetworkCapabilities.java @@ -259,6 +259,19 @@ public final class NetworkCapabilities implements Parcelable { */ 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. * @@ -741,8 +754,10 @@ public final class NetworkCapabilities implements Parcelable { /** * 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, @@ -858,6 +873,19 @@ public final class NetworkCapabilities implements Parcelable { return this; } + /** + * 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. * diff --git a/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java b/service/src/com/android/metrics/ConnectivitySampleMetricsHelper.java new file mode 100644 index 0000000000..93d1d5def1 --- /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 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 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 delegate) { + mDelegate = delegate; + } + + @Override + public int onPullAtom(final int atomTag, final List 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 6770a8fe8c..85507f6d16 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_NOT_VPN; 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.checkAnyPermissionOf; 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.SystemProperties; 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.Pair; 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.GuardedBy; 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.Set; 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 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 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 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 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 87ae0c9e79..648f3bf250 100644 --- a/service/src/com/android/server/connectivity/FullScore.java +++ b/service/src/com/android/server/connectivity/FullScore.java @@ -124,7 +124,7 @@ public class FullScore { 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 @@ -303,6 +303,18 @@ public class FullScore { return new FullScore(mPolicies | (1L << POLICY_IS_VALIDATED), mKeepConnectedReason); } + /** + * 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. */ diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java index 845c04cec3..bdd841f9ed 100644 --- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java +++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java @@ -1105,6 +1105,11 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { * 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 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { * 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 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { * 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 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable { * 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/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java index 28edcb22a1..edd201d11c 100644 --- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java +++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java @@ -64,6 +64,9 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork { + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. private final NetworkCapabilities mNetworkCapabilities; private final HandlerThread mHandlerThread; private final Context mContext; @@ -468,4 +471,8 @@ public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork { public boolean isBypassableVpn() { return mNetworkAgentConfig.isBypassableVpn(); } + + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. } 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 0000000000..3043d50cfa --- /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 Handler.onHandler(f: () -> T): T { + val future = CompletableFuture() + 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 e5dec56a29..2fccdcb9a4 100755 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -75,10 +75,7 @@ import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPR import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK; import static android.net.ConnectivityManager.TYPE_ETHERNET; import static android.net.ConnectivityManager.TYPE_MOBILE; -import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; -import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; -import static android.net.ConnectivityManager.TYPE_PROXY; import static android.net.ConnectivityManager.TYPE_VPN; import static android.net.ConnectivityManager.TYPE_WIFI; import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; @@ -492,6 +489,8 @@ import java.util.stream.Collectors; * Build, install and run with: * runtest frameworks-net -c com.android.server.ConnectivityServiceTest */ +// TODO : move methods from this test to smaller tests in the 'connectivityservice' directory +// to enable faster testing of smaller groups of functionality. @RunWith(DevSdkIgnoreRunner.class) @SmallTest @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) @@ -756,6 +755,9 @@ public class ConnectivityServiceTest { 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); } @@ -1016,6 +1018,9 @@ public class ConnectivityServiceTest { } private class TestNetworkAgentWrapper extends NetworkAgentWrapper { + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. private static final int VALIDATION_RESULT_INVALID = 0; private static final long DATA_STALL_TIMESTAMP = 10L; @@ -1340,6 +1345,9 @@ public class ConnectivityServiceTest { * operations have been processed and test for them. */ private static class MockNetworkFactory extends NetworkFactory { + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSTest and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false); static class RequestEntry { @@ -1476,6 +1484,10 @@ public class ConnectivityServiceTest { } private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork { + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSTest and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. + // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does // not inherit from NetworkAgent. private TestNetworkAgentWrapper mMockNetworkAgent; @@ -1852,6 +1864,9 @@ public class ConnectivityServiceTest { MockitoAnnotations.initMocks(this); + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSTest and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers(); doReturn(asList(PRIMARY_USER_HANDLE)).when(mUserManager).getUserHandles(anyBoolean()); doReturn(PRIMARY_USER_INFO).when(mUserManager).getUserInfo(PRIMARY_USER); @@ -1938,6 +1953,9 @@ public class ConnectivityServiceTest { setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT); setAlwaysOnNetworks(false); setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com"); + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSTest and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. } private void initMockedResources() { @@ -1974,6 +1992,9 @@ public class ConnectivityServiceTest { final ConnectivityResources mConnRes; final ArraySet> mEnabledChangeIds = new ArraySet<>(); + // Note : Please do not add any new instrumentation here. If you need new instrumentation, + // please add it in CSTest and use subclasses of CSTest instead of adding more + // tools in ConnectivityServiceTest. ConnectivityServiceDependencies(final Context mockResContext) { mConnRes = new ConnectivityResources(mockResContext); } @@ -2582,23 +2603,6 @@ public class ConnectivityServiceTest { return true; } - @Test - public void testNetworkTypes() { - // Ensure that our mocks for the networkAttributes config variable work as expected. If they - // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types - // will fail. Failing here is much easier to debug. - assertTrue(mCm.isNetworkSupported(TYPE_WIFI)); - assertTrue(mCm.isNetworkSupported(TYPE_MOBILE)); - assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS)); - assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA)); - assertFalse(mCm.isNetworkSupported(TYPE_PROXY)); - - // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our - // mocks, this assert exercises the ConnectivityService code path that ensures that - // TYPE_ETHERNET is supported if the ethernet service is running. - assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET)); - } - @Test public void testNetworkFeature() throws Exception { // Connect the cell agent and wait for the connected broadcast. @@ -18801,4 +18805,7 @@ public class ConnectivityServiceTest { verifyClatdStop(null /* inOrder */, MOBILE_IFNAME); } + + // Note : adding tests is ConnectivityServiceTest is deprecated, as it is too big for + // maintenance. Please consider adding new tests in subclasses of CSTest instead. } diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt new file mode 100644 index 0000000000..6f8ba6c5f2 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt @@ -0,0 +1,35 @@ +@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it + +package com.android.server + +import android.net.ConnectivityManager +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(DevSdkIgnoreRunner::class) +@SmallTest +@IgnoreUpTo(Build.VERSION_CODES.R) +class CSBasicMethodsTest : CSTest() { + @Test + fun testNetworkTypes() { + // Ensure that mocks for the networkAttributes config variable work as expected. If they + // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types + // will fail. Failing here is much easier to debug. + assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_WIFI)) + assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE)) + assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_MMS)) + assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_FOTA)) + assertFalse(cm.isNetworkSupported(ConnectivityManager.TYPE_PROXY)) + + // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our + // mocks, this assert exercises the ConnectivityService code path that ensures that + // TYPE_ETHERNET is supported if the ethernet service is running. + assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_ETHERNET)) + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt new file mode 100644 index 0000000000..5ae92320d1 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt @@ -0,0 +1,134 @@ +package com.android.server + +import android.content.Context +import android.net.ConnectivityManager +import android.net.INetworkMonitor +import android.net.INetworkMonitorCallbacks +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkAgent +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +import android.net.NetworkProvider +import android.net.NetworkRequest +import android.net.NetworkScore +import android.net.NetworkTestResultParcelable +import android.net.networkstack.NetworkStackClientBase +import android.os.HandlerThread +import com.android.modules.utils.build.SdkLevel +import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.TestableNetworkCallback +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.verify +import org.mockito.stubbing.Answer +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.fail + +private inline fun ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java) + +private val agentCounter = AtomicInteger(1) +private fun nextAgentId() = agentCounter.getAndIncrement() + +/** + * A wrapper for network agents, for use with CSTest. + * + * This class knows how to interact with CSTest and has helpful methods to make fake agents + * that can be manipulated directly from a test. + */ +class CSAgentWrapper( + val context: Context, + csHandlerThread: HandlerThread, + networkStack: NetworkStackClientBase, + nac: NetworkAgentConfig, + val nc: NetworkCapabilities, + val lp: LinkProperties, + val score: FromS, + val provider: NetworkProvider? +) : TestableNetworkCallback.HasNetwork { + private val TAG = "CSAgent${nextAgentId()}" + private val VALIDATION_RESULT_INVALID = 0 + private val VALIDATION_TIMESTAMP = 1234L + private val agent: NetworkAgent + private val nmCallbacks: INetworkMonitorCallbacks + val networkMonitor = mock() + + override val network: Network get() = agent.network!! + + init { + // Capture network monitor callbacks and simulate network monitor + val validateAnswer = Answer { + CSTest.CSTestExecutor.execute { onValidationRequested() } + null + } + doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnected(any(), any()) + doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnectedParcel(any()) + doAnswer(validateAnswer).`when`(networkMonitor).forceReevaluation(anyInt()) + val nmNetworkCaptor = ArgumentCaptor() + val nmCbCaptor = ArgumentCaptor() + doNothing().`when`(networkStack).makeNetworkMonitor( + nmNetworkCaptor.capture(), + any() /* name */, + nmCbCaptor.capture()) + + // Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass. + if (SdkLevel.isAtLeastS()) { + agent = object : NetworkAgent(context, csHandlerThread.looper, TAG, + nc, lp, score.value, nac, provider) {} + } else { + agent = object : NetworkAgent(context, csHandlerThread.looper, TAG, + nc, lp, 50 /* score */, nac, provider) {} + } + agent.register() + assertEquals(agent.network!!.netId, nmNetworkCaptor.value.netId) + nmCallbacks = nmCbCaptor.value + nmCallbacks.onNetworkMonitorCreated(networkMonitor) + } + + private fun onValidationRequested() { + if (SdkLevel.isAtLeastT()) { + verify(networkMonitor).notifyNetworkConnectedParcel(any()) + } else { + verify(networkMonitor).notifyNetworkConnected(any(), any()) + } + nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */) + val p = NetworkTestResultParcelable() + p.result = VALIDATION_RESULT_INVALID + p.probesAttempted = 0 + p.probesSucceeded = 0 + p.redirectUrl = null + p.timestampMillis = VALIDATION_TIMESTAMP + nmCallbacks.notifyNetworkTestedWithExtras(p) + } + + fun connect() { + val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val request = NetworkRequest.Builder().clearCapabilities() + .addTransportType(nc.transportTypes[0]) + .build() + val cb = TestableNetworkCallback() + mgr.registerNetworkCallback(request, cb) + agent.markConnected() + if (null == cb.poll { it is Available && agent.network == it.network }) { + if (!nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) && + nc.hasTransport(TRANSPORT_CELLULAR)) { + // ConnectivityService adds NOT_SUSPENDED by default to all non-cell agents. An + // agent without NOT_SUSPENDED will not connect, instead going into the SUSPENDED + // state, so this call will not terminate. + // Instead of forcefully adding NOT_SUSPENDED to all agents like older tools did, + // it's better to let the developer manage it as they see fit but help them + // debug if they forget. + fail("Could not connect the agent. Did you forget to add " + + "NET_CAPABILITY_NOT_SUSPENDED ?") + } + fail("Could not connect the agent. Instrumentation failure ?") + } + mgr.unregisterNetworkCallback(cb) + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt new file mode 100644 index 0000000000..68613a673c --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -0,0 +1,239 @@ +package com.android.server + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.content.pm.UserInfo +import android.content.res.Resources +import android.net.ConnectivityManager +import android.net.INetd +import android.net.InetAddresses +import android.net.IpPrefix +import android.net.LinkAddress +import android.net.LinkProperties +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED +import android.net.NetworkPolicyManager +import android.net.NetworkProvider +import android.net.NetworkScore +import android.net.PacProxyManager +import android.net.RouteInfo +import android.net.networkstack.NetworkStackClientBase +import android.os.Handler +import android.os.HandlerThread +import android.os.UserHandle +import android.os.UserManager +import android.telephony.TelephonyManager +import android.testing.TestableContext +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.util.test.BroadcastInterceptingContext +import com.android.modules.utils.build.SdkLevel +import com.android.networkstack.apishim.common.UnsupportedApiLevelException +import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker +import com.android.server.connectivity.CarrierPrivilegeAuthenticator +import com.android.server.connectivity.ClatCoordinator +import com.android.server.connectivity.ConnectivityFlags +import com.android.server.connectivity.MultinetworkPolicyTracker +import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies +import com.android.server.connectivity.ProxyTracker +import com.android.testutils.waitForIdle +import org.mockito.AdditionalAnswers.delegatesTo +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import java.util.concurrent.Executors +import kotlin.test.fail + +internal const val HANDLER_TIMEOUT_MS = 2_000 +internal const val TEST_PACKAGE_NAME = "com.android.test.package" +internal const val WIFI_WOL_IFNAME = "test_wlan_wol" +internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1") + +open class FromS(val value: Type) + +/** + * Base class for tests testing ConnectivityService and its satellites. + * + * This class sets up a ConnectivityService running locally in the test. + */ +// TODO (b/272685721) : make ConnectivityServiceTest smaller and faster by moving the setup +// parts into this class and moving the individual tests to multiple separate classes. +open class CSTest { + companion object { + val CSTestExecutor = Executors.newSingleThreadExecutor() + } + + init { + if (!SdkLevel.isAtLeastS()) { + throw UnsupportedApiLevelException("CSTest subclasses must be annotated to only " + + "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)"); + } + } + + val instrumentationContext = + TestableContext(InstrumentationRegistry.getInstrumentation().context) + val context = CSContext(instrumentationContext) + + // See constructor for default-enabled features. All queried features must be either enabled + // or disabled, because the test can't hold READ_DEVICE_CONFIG and device config utils query + // permissions using static contexts. + val enabledFeatures = HashMap().also { + it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true + it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true + it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true + } + fun enableFeature(f: String) = enabledFeatures.set(f, true) + fun disableFeature(f: String) = enabledFeatures.set(f, false) + + // When adding new members, consider if it's not better to build the object in CSTestHelpers + // to keep this file clean of implementation details. Generally, CSTestHelpers should only + // need changes when new details of instrumentation are needed. + val contentResolver = makeMockContentResolver(context) + + val PRIMARY_USER = 0 + val PRIMARY_USER_INFO = UserInfo(PRIMARY_USER, "" /* name */, UserInfo.FLAG_PRIMARY) + val PRIMARY_USER_HANDLE = UserHandle(PRIMARY_USER) + val userManager = makeMockUserManager(PRIMARY_USER_INFO, PRIMARY_USER_HANDLE) + val activityManager = makeActivityManager() + + val networkStack = mock() + val csHandlerThread = HandlerThread("CSTestHandler") + val sysResources = mock().also { initMockedResources(it) } + val packageManager = makeMockPackageManager() + val connResources = makeMockConnResources(sysResources, packageManager) + + val bpfNetMaps = mock() + val clatCoordinator = mock() + val proxyTracker = ProxyTracker(context, mock(), 16 /* EVENT_PROXY_HAS_CHANGED */) + val alarmManager = makeMockAlarmManager() + val systemConfigManager = makeMockSystemConfigManager() + val telephonyManager = mock().also { + doReturn(true).`when`(it).isDataCapable() + } + + val deps = CSDeps() + val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() } + val cm = ConnectivityManager(context, service) + val csHandler = Handler(csHandlerThread.looper) + + inner class CSDeps : ConnectivityService.Dependencies() { + override fun getResources(ctx: Context) = connResources + override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps + override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator + override fun getNetworkStack() = this@CSTest.networkStack + + override fun makeHandlerThread() = csHandlerThread + override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker + + override fun makeCarrierPrivilegeAuthenticator(context: Context, tm: TelephonyManager) = + if (SdkLevel.isAtLeastT()) mock() else null + + private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) { + override fun isTetheringFeatureNotChickenedOut(name: String): Boolean { + return isFeatureEnabled(context, name) + } + } + override fun makeAutomaticOnOffKeepaliveTracker(c: Context, h: Handler) = + AutomaticOnOffKeepaliveTracker(c, h, AOOKTDeps(c)) + + override fun makeMultinetworkPolicyTracker(c: Context, h: Handler, r: Runnable) = + MultinetworkPolicyTracker(c, h, r, + MultinetworkPolicyTrackerTestDependencies(connResources.get())) + + // All queried features must be mocked, because the test cannot hold the + // READ_DEVICE_CONFIG permission and device config utils use static methods for + // checking permissions. + override fun isFeatureEnabled(context: Context?, name: String?) = + enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures") + } + + inner class CSContext(base: Context) : BroadcastInterceptingContext(base) { + val pacProxyManager = mock() + val networkPolicyManager = mock() + + override fun getPackageManager() = this@CSTest.packageManager + override fun getContentResolver() = this@CSTest.contentResolver + + // TODO : buff up the capabilities of this permission scheme to allow checking for + // permission rejections + override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED + override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED + + // Necessary for MultinetworkPolicyTracker, which tries to register a receiver for + // all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS. + // TODO : ensure MultinetworkPolicyTracker's BroadcastReceiver is tested ; ideally, + // just returning null should not have tests pass + override fun registerReceiverForAllUsers( + receiver: BroadcastReceiver?, + filter: IntentFilter, + broadcastPermission: String?, + scheduler: Handler? + ): Intent? = null + + // Create and cache user managers on the fly as necessary. + val userManagers = HashMap() + override fun createContextAsUser(user: UserHandle, flags: Int): Context { + val asUser = mock(Context::class.java, delegatesTo(this)) + doReturn(user).`when`(asUser).getUser() + doAnswer { userManagers.computeIfAbsent(user) { + mock(UserManager::class.java, delegatesTo(userManager)) } + }.`when`(asUser).getSystemService(Context.USER_SERVICE) + return asUser + } + + // List of mocked services. Add additional services here or in subclasses. + override fun getSystemService(serviceName: String) = when (serviceName) { + Context.CONNECTIVITY_SERVICE -> cm + Context.PAC_PROXY_SERVICE -> pacProxyManager + Context.NETWORK_POLICY_SERVICE -> networkPolicyManager + Context.ALARM_SERVICE -> alarmManager + Context.USER_SERVICE -> userManager + Context.ACTIVITY_SERVICE -> activityManager + Context.SYSTEM_CONFIG_SERVICE -> systemConfigManager + Context.TELEPHONY_SERVICE -> telephonyManager + Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked + else -> super.getSystemService(serviceName) + } + } + + // Utility methods for subclasses to use + fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS) + + private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build() + private fun defaultNc() = NetworkCapabilities.Builder() + // Add sensible defaults for agents that don't want to care + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NET_CAPABILITY_NOT_ROAMING) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + .build() + private fun defaultScore() = FromS(NetworkScore.Builder().build()) + private fun defaultLp() = LinkProperties().apply { + addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32)) + addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null)) + } + + // Network agents. See CSAgentWrapper. This class contains utility methods to simplify + // creation. + fun Agent( + nac: NetworkAgentConfig = emptyAgentConfig(), + nc: NetworkCapabilities = defaultNc(), + lp: LinkProperties = defaultLp(), + score: FromS = defaultScore(), + provider: NetworkProvider? = null + ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider) + + fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper { + val nc = NetworkCapabilities.Builder().apply { + transports.forEach { + addTransportType(it) + } + }.addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .build() + return Agent(nc = nc, lp = lp) + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt new file mode 100644 index 0000000000..b8f2151b67 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt @@ -0,0 +1,140 @@ +@file:JvmName("CsTestHelpers") + +package com.android.server + +import android.app.ActivityManager +import android.app.AlarmManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.FEATURE_BLUETOOTH +import android.content.pm.PackageManager.FEATURE_ETHERNET +import android.content.pm.PackageManager.FEATURE_WIFI +import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT +import android.content.pm.UserInfo +import android.content.res.Resources +import android.net.IDnsResolver +import android.net.INetd +import android.net.metrics.IpConnectivityLog +import android.os.Handler +import android.os.HandlerThread +import android.os.SystemClock +import android.os.SystemConfigManager +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.test.mock.MockContentResolver +import com.android.connectivity.resources.R +import com.android.internal.util.WakeupMessage +import com.android.internal.util.test.FakeSettingsProvider +import com.android.modules.utils.build.SdkLevel +import com.android.server.ConnectivityService.Dependencies +import com.android.server.connectivity.ConnectivityResources +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.doNothing +import kotlin.test.fail + +internal inline fun mock() = Mockito.mock(T::class.java) +internal inline fun any() = any(T::class.java) + +internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply { + addProvider(Settings.AUTHORITY, FakeSettingsProvider()) +} + +internal fun makeMockUserManager(info: UserInfo, handle: UserHandle) = mock().also { + doReturn(listOf(info)).`when`(it).getAliveUsers() + doReturn(listOf(handle)).`when`(it).getUserHandles(ArgumentMatchers.anyBoolean()) +} + +internal fun makeActivityManager() = mock().also { + if (SdkLevel.isAtLeastU()) { + doNothing().`when`(it).registerUidFrozenStateChangedCallback(any(), any()) + } +} + +internal fun makeMockPackageManager() = mock().also { pm -> + val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET) + doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) }) +} + +internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock().let { + doReturn(resources).`when`(it).resources + doReturn(pm).`when`(it).packageManager + ConnectivityResources.setResourcesContextForTest(it) + ConnectivityResources(it) +} + +private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000 +internal fun makeMockAlarmManager() = mock().also { am -> + val alrmHdlr = HandlerThread("TestAlarmManager").also { it.start() }.threadHandler + doAnswer { + val (_, date, _, wakeupMsg, handler) = it.arguments + wakeupMsg as WakeupMessage + handler as Handler + val delayMs = ((date as Long) - SystemClock.elapsedRealtime()).coerceAtLeast(0) + if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) { + fail("Attempting to send msg more than $UNREASONABLY_LONG_ALARM_WAIT_MS" + + "ms into the future : $delayMs") + } + alrmHdlr.postDelayed({ handler.post(wakeupMsg::onAlarm) }, wakeupMsg, delayMs) + }.`when`(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(), + any(), any()) + doAnswer { + alrmHdlr.removeCallbacksAndMessages(it.getArgument(0)) + }.`when`(am).cancel(any()) +} + +internal fun makeMockSystemConfigManager() = mock().also { + doReturn(intArrayOf(0)).`when`(it).getSystemPermissionUids(anyString()) +} + +// Mocking resources used by ConnectivityService. Note these can't be defined to return the +// value returned by the mocking, because a non-null method would mean the helper would also +// return non-null and the compiler would check that, but mockito has no qualms returning null +// from a @NonNull method when stubbing. Hence, mock() = doReturn().getString() would crash +// at runtime, because getString() returns non-null String, therefore mock returns non-null String, +// and kotlinc adds an intrinsics check for that, which crashes at runtime when mockito actually +// returns null. +private fun Resources.mock(r: Int, v: Boolean) { doReturn(v).`when`(this).getBoolean(r) } +private fun Resources.mock(r: Int, v: Int) { doReturn(v).`when`(this).getInteger(r) } +private fun Resources.mock(r: Int, v: String) { doReturn(v).`when`(this).getString(r) } +private fun Resources.mock(r: Int, v: Array) { doReturn(v).`when`(this).getStringArray(r) } +private fun Resources.mock(r: Int, v: IntArray) { doReturn(v).`when`(this).getIntArray(r) } + +internal fun initMockedResources(res: Resources) { + // Resources accessed through reflection need to return the id + doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(res) + .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any()) + doReturn(R.array.network_switch_type_name).`when`(res) + .getIdentifier(eq("network_switch_type_name"), eq("array"), any()) + // Mock the values themselves + res.mock(R.integer.config_networkTransitionTimeout, 60_000) + res.mock(R.string.config_networkCaptivePortalServerUrl, "") + res.mock(R.array.config_wakeonlan_supported_interfaces, arrayOf(WIFI_WOL_IFNAME)) + res.mock(R.array.config_networkSupportedKeepaliveCount, arrayOf("0,1", "1,3")) + res.mock(R.array.config_networkNotifySwitches, arrayOfNulls(size = 0)) + res.mock(R.array.config_protectedNetworks, intArrayOf(10, 11, 12, 14, 15)) + res.mock(R.array.network_switch_type_name, arrayOfNulls(size = 0)) + res.mock(R.integer.config_networkAvoidBadWifi, 1) + res.mock(R.integer.config_activelyPreferBadWifi, 0) + res.mock(R.bool.config_cellular_radio_timesharing_capable, true) +} + +private val TEST_LINGER_DELAY_MS = 400 +private val TEST_NASCENT_DELAY_MS = 300 +internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService( + context, + mock(), + mock(), + mock(), + deps).also { + it.mLingerDelayMs = TEST_LINGER_DELAY_MS + it.mNascentDelayMs = TEST_NASCENT_DELAY_MS +}