From c00cf63e8a0fb2c0a3b5a4f86ed0a82f4eecd4c6 Mon Sep 17 00:00:00 2001 From: Junyu Lai Date: Fri, 20 Jun 2025 14:31:52 +0800 Subject: [PATCH] Add metrics for requests satisfied by local networks This change introduces `SatisfiedByLocalNetworkMetrics` to count the number of `NetworkRequest`s per UID that can be satisfied by a local-only network. The purpose is to gather data on how many applications register callbacks that would match non-thread local network connectivity. This data helps in understanding app behavior and can inform future development of local-only network features particularly for the backward compatibility of existing NetworkRequests. - A new class `SatisfiedByLocalNetworkMetrics` handles the counting and logging logic. - `ConnectivityService` calls into this class upon `NetworkRequest` registration. - Counts are logged to statsd. Sample dumpsys output: SatisfiedByLocalNetworkMetrics: {LISTEN={10217=2}} Bug: 406172997 Flag: EXEMPT controlled by the `satisfied_by_local_network_metrics` Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.CSSatisfiedByLocalNetworkMetricsTest \ ConnectivityCoverageTests:android.net.connectivity.com.android.metrics.SatisfiedByLocalNetworkMetricsTest \ --update-device Test: m statsd_testdrive && statsd_testdrive -e 10247 Test: Manual: atest dumpsys connectivity Change-Id: I62f6299a1c726537b373e3e80d41a494323f3cf2 --- .../SatisfiedByLocalNetworkMetrics.java | 218 ++++++++++++++++++ .../android/server/ConnectivityService.java | 36 +++ .../connectivity/ConnectivityFlags.java | 3 + .../SatisfiedByLocalNetworkMetricsTest.kt | 200 ++++++++++++++++ .../server/ConnectivityServiceTest.java | 9 + .../CSSatisfiedByLocalNetworkMetricsTest.kt | 129 +++++++++++ .../server/connectivityservice/base/CSTest.kt | 10 +- 7 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 service/src/com/android/metrics/SatisfiedByLocalNetworkMetrics.java create mode 100644 tests/unit/java/com/android/metrics/SatisfiedByLocalNetworkMetricsTest.kt create mode 100644 tests/unit/java/com/android/server/connectivityservice/CSSatisfiedByLocalNetworkMetricsTest.kt diff --git a/service/src/com/android/metrics/SatisfiedByLocalNetworkMetrics.java b/service/src/com/android/metrics/SatisfiedByLocalNetworkMetrics.java new file mode 100644 index 0000000000..1ded75b534 --- /dev/null +++ b/service/src/com/android/metrics/SatisfiedByLocalNetworkMetrics.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2025 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 static android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; + +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_BACKGROUND_REQUEST; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_LISTEN; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_LISTEN_FOR_BEST; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_REQUEST; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_RESERVATION; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_TRACK_DEFAULT; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_TRACK_SYSTEM_DEFAULT; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_UNSPECIFIED; + +import android.annotation.NonNull; +import android.app.StatsManager; +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.connectivity.ConnectivityInternalApiUtil; +import android.os.Handler; +import android.util.IndentingPrintWriter; +import android.util.SparseIntArray; +import android.util.StatsEvent; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.FrameworkConnectivityStatsLog; +import com.android.net.module.util.HandlerUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tracks NetworkRequest metrics related to local network capabilities. + * + * This class is thread-safe because all access to its mutable internal state + * is serialized through a single {@link Handler} thread. + */ +public class SatisfiedByLocalNetworkMetrics implements StatsManager.StatsPullAtomCallback { + private final Context mContext; + private final Handler mHandler; + private final Dependencies mDeps; + + private static int requestTypeToEnum(NetworkRequest.Type reqType) { + return switch (reqType) { + case LISTEN -> SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_LISTEN; + case TRACK_DEFAULT -> + SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_TRACK_DEFAULT; + case REQUEST -> SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_REQUEST; + case BACKGROUND_REQUEST -> + SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_BACKGROUND_REQUEST; + case TRACK_SYSTEM_DEFAULT -> + SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_TRACK_SYSTEM_DEFAULT; + case LISTEN_FOR_BEST -> + SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_LISTEN_FOR_BEST; + case RESERVATION -> + SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_RESERVATION; + default -> SATISFIED_BY_LOCAL_NETWORK_REQUESTS__TYPE__NETWORK_REQUEST_TYPE_UNSPECIFIED; + }; + } + + public static class Dependencies { + @NonNull + StatsEvent buildStatsEvent(int uid, NetworkRequest.Type requestType, int count) { + return FrameworkConnectivityStatsLog.buildStatsEvent( + SATISFIED_BY_LOCAL_NETWORK_REQUESTS, + uid, + requestTypeToEnum(requestType), + count + ); + } + } + + /** + * Represents the capabilities and transports that could be provided by a + * non-thread local network agent. + * This is used to test against existing application NetworkRequests. + * + * Refer to {@link ConnectivityInternalApiUtil#buildTetheringNetworkAgent} and + * {@code TetheringUtils#getTransportTypeForTetherableType}. + */ + private static final NetworkCapabilities NON_THREAD_LOCAL_NETWORK_CAPABILITIES = + new NetworkCapabilities.Builder() + .addCapability(NET_CAPABILITY_LOCAL_NETWORK) + .addCapability(NET_CAPABILITY_NOT_METERED) + .addCapability(NET_CAPABILITY_NOT_ROAMING) + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_SUSPENDED) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_USB) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(); + + // This map stores counts per unique NetworkRequest.Type and then per UID. + // Key: NetworkRequest.Type (representing the 'request type' as defined by the system) + // Value: SparseIntArray mapping UID to the count of satisfied requests for that specific type. + // This should only be accessed from the same thread. + private final Map mSatisfiedRequestsPerTypeAndUid = + new HashMap<>(); + + /** + * Constructs a new SatisfiedByLocalNetworkMetrics instance. + * + * @param context The application context, used to get system services like StatsManager. + * @param handler The Handler on which the onPullAtom callback will be invoked. + */ + public SatisfiedByLocalNetworkMetrics(Context context, Handler handler) { + this(context, handler, new Dependencies()); + } + + @VisibleForTesting + public SatisfiedByLocalNetworkMetrics(Context context, Handler handler, Dependencies deps) { + mContext = context; + mHandler = handler; + mDeps = deps; + } + + /** + * Registers this class as a callback for the relevant pull atom. + */ + public void start() { + final StatsManager statsManager = mContext.getSystemService(StatsManager.class); + statsManager.setPullAtomCallback( + SATISFIED_BY_LOCAL_NETWORK_REQUESTS, + null, // Use default pull frequency + (it) -> mHandler.post(it), + this // This class implements StatsPullAtomCallback + ); + } + + /** + * Checks if a given NetworkRequest would be satisfied by the static local network + * capabilities and increments counters if it matches. + * + * @param request The NetworkRequest from the application's callback. + * @param uid The UID of the application that registered the callback. + */ + public void logRequest(NetworkRequest request, int uid) { + // Get a cloned caps with altered MatchNonThreadLocalNetworks to evaluate + // whether the request will be affected by Non-Thread local networks. + final NetworkCapabilities clonedCaps = new NetworkCapabilities(request.networkCapabilities); + clonedCaps.setMatchNonThreadLocalNetworks(true); + mHandler.post(() -> handleLogRequest(request.type, clonedCaps, uid)); + } + + private void handleLogRequest(NetworkRequest.Type reqType, NetworkCapabilities caps, int uid) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + // Check if the request matches the defined local network capabilities + if (caps.satisfiedByNetworkCapabilities(NON_THREAD_LOCAL_NETWORK_CAPABILITIES)) { + // Get or create the SparseIntArray for this specific NetworkRequest.Type. + final SparseIntArray uidCounts = mSatisfiedRequestsPerTypeAndUid.computeIfAbsent( + reqType, k -> new SparseIntArray()); + + // Increment the count for this UID under this specific request type + final int currentCount = uidCounts.get(uid, 0); + final int newCount = currentCount + 1; + uidCounts.put(uid, newCount); + } + } + + /** + * Callback method invoked by StatsManager when a pull atom is requested. + */ + @Override + public int onPullAtom(final int atomTag, final List data) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + if (atomTag != SATISFIED_BY_LOCAL_NETWORK_REQUESTS) { + return StatsManager.PULL_SKIP; + } + + // Iterate through all stored NetworkRequest.Type (request types) + for (Map.Entry entry : + mSatisfiedRequestsPerTypeAndUid.entrySet()) { + final NetworkRequest.Type requestType = entry.getKey(); + // Counts for UIDs that made this specific request type. + final SparseIntArray uidCounts = entry.getValue(); + + for (int i = 0; i < uidCounts.size(); i++) { + final int uid = uidCounts.keyAt(i); + final int count = uidCounts.valueAt(i); + data.add(mDeps.buildStatsEvent(uid, requestType, count)); + } + } + return StatsManager.PULL_SUCCESS; + } + + /** + * Dumps the current metrics for debugging purposes. + * + * @param pw The IndentingPrintWriter to write the output to. + */ + public void dump(IndentingPrintWriter pw) { + HandlerUtils.ensureRunningOnHandlerThread(mHandler); + pw.println("SatisfiedByLocalNetworkMetrics: " + mSatisfiedRequestsPerTypeAndUid); + } +} + diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index ff95a378a8..442f68b6f1 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -168,6 +168,7 @@ import static com.android.server.connectivity.ConnectivityFlags.NAMESPACE_TETHER import static com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS; import static com.android.server.connectivity.ConnectivityFlags.QUEUE_NETWORK_AGENT_EVENTS_IN_SYSTEM_SERVER; import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI; +import static com.android.server.connectivity.ConnectivityFlags.SATISFIED_BY_LOCAL_NETWORK_METRICS; import static com.android.server.connectivity.ConnectivityFlags.WIFI_DATA_INACTIVITY_TIMEOUT; import android.Manifest; @@ -344,6 +345,7 @@ import com.android.metrics.NetworkRequestCount; import com.android.metrics.RequestCountForType; import com.android.metrics.SatelliteAccessInfo; import com.android.metrics.SatelliteCoarseUsageMetricsCollector; +import com.android.metrics.SatisfiedByLocalNetworkMetrics; import com.android.modules.utils.BasicShellCommandHandler; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; @@ -1095,6 +1097,7 @@ public class ConnectivityService extends IConnectivityManager.Stub private final IpConnectivityLog mMetricsLog; private final DefaultNetworkRematchMetrics mDefaultNetworkRematchMetrics; + private final SatisfiedByLocalNetworkMetrics mSatisfiedByLocalNetworkMetrics; @Nullable private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics; @@ -1731,6 +1734,15 @@ public class ConnectivityService extends IConnectivityManager.Stub return new DefaultNetworkRematchMetrics(); } + /** + * @see SatisfiedByLocalNetworkMetrics + */ + @Nullable + public SatisfiedByLocalNetworkMetrics makeSatisfiedByLocalNetworkMetrics(Context context, + Handler handler) { + return new SatisfiedByLocalNetworkMetrics(context, handler); + } + /** Creates an L2capNetworkProvider */ public L2capNetworkProvider makeL2capNetworkProvider(Context context) { return new L2capNetworkProvider(context); @@ -2122,6 +2134,12 @@ public class ConnectivityService extends IConnectivityManager.Stub mSatelliteCoarseUsageMetricsCollector = null; mDefaultNetworkRematchMetrics = null; } + if (mDeps.isFeatureNotChickenedOut(mContext, SATISFIED_BY_LOCAL_NETWORK_METRICS)) { + mSatisfiedByLocalNetworkMetrics = + mDeps.makeSatisfiedByLocalNetworkMetrics(mContext, mHandler); + } else { + mSatisfiedByLocalNetworkMetrics = null; + } if (mDeps.flagConnectivityServiceDestroySocket()) { mIpToNetworksMap = new HashMap<>(); @@ -4364,6 +4382,9 @@ public class ConnectivityService extends IConnectivityManager.Stub } ConnectivitySampleMetricsHelper.start(mContext, mHandler, CONNECTIVITY_STATE_SAMPLE, this::sampleConnectivityStateToStatsEvent); + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.start(); + } // 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#initialize, which @@ -4702,6 +4723,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (mConstrainedDataSatelliteMetrics) { mSatelliteCoarseUsageMetricsCollector.dump(pw); } + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.dump(pw); + } pw.println(); pw.println("Legacy network activity:"); @@ -8901,6 +8925,9 @@ public class ConnectivityService extends IConnectivityManager.Stub callingAttributionTag, declaredMethodsFlag); if (DBG) log("requestNetwork for " + nri); trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST, nri); + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.logRequest(networkRequest, asUid); + } if (timeoutMs > 0) { mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST, nri), timeoutMs); @@ -9114,6 +9141,9 @@ public class ConnectivityService extends IConnectivityManager.Stub callingAttributionTag); if (DBG) log("pendingRequest for " + nri); trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT, nri); + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.logRequest(networkRequest, callingUid); + } return networkRequest; } @@ -9260,6 +9290,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (VDBG) log("listenForNetwork for " + nri); trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER, nri); + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.logRequest(networkRequest, callingUid); + } return networkRequest; } @@ -9285,6 +9318,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (VDBG) log("pendingListenForNetwork for " + nri); trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT, nri); + if (mSatisfiedByLocalNetworkMetrics != null) { + mSatisfiedByLocalNetworkMetrics.logRequest(networkRequest, callingUid); + } } /** Returns the next Network provider ID. */ diff --git a/service/src/com/android/server/connectivity/ConnectivityFlags.java b/service/src/com/android/server/connectivity/ConnectivityFlags.java index 19c97fee1d..19ea0cc2ee 100644 --- a/service/src/com/android/server/connectivity/ConnectivityFlags.java +++ b/service/src/com/android/server/connectivity/ConnectivityFlags.java @@ -73,6 +73,9 @@ public final class ConnectivityFlags { public static final String CONSTRAINED_DATA_SATELLITE_METRICS = "constrained_data_satellite_metrics"; + public static final String SATISFIED_BY_LOCAL_NETWORK_METRICS = + "satisfied_by_local_network_metrics"; + /** * A feature flag to control whether the early link properties update for vpn should be enabled. * diff --git a/tests/unit/java/com/android/metrics/SatisfiedByLocalNetworkMetricsTest.kt b/tests/unit/java/com/android/metrics/SatisfiedByLocalNetworkMetricsTest.kt new file mode 100644 index 0000000000..016b068368 --- /dev/null +++ b/tests/unit/java/com/android/metrics/SatisfiedByLocalNetworkMetricsTest.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025 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.app.StatsManager +import android.content.Context +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.NetworkRequest.Type +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.util.StatsEvent +import com.android.net.module.util.FrameworkConnectivityStatsLog.SATISFIED_BY_LOCAL_NETWORK_REQUESTS +import com.android.server.any +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.postAndWait +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +/** + * Unit tests for {@link SatisfiedByLocalNetworkMetrics}. + */ +// Local network is supported until U. +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) +@DevSdkIgnoreRunner.MonitorThreadLeak +@RunWith(DevSdkIgnoreRunner::class) +class SatisfiedByLocalNetworkMetricsTest { + companion object { + private const val TEST_UID_1 = 10001 + private const val TEST_UID_2 = 10002 + private const val TIMEOUT_MS = 2000 + + // Simulate caps which cannot be satisfied by non-thread local + // networks. + private val INTERNET_CAPS = NetworkCapabilities.Builder() + .addCapability(NET_CAPABILITY_INTERNET).build() + + // Simulate caps which can potentially be satisfied by non-thread + // local networks. + private val WIFI_CAPS = NetworkCapabilities.Builder() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addTransportType(TRANSPORT_WIFI) + .build() + } + + // Spy the Dependencies object to inspect content of buildStatsEvent. + // Because StatsEvent has no getter to inspect the content. + private val deps = spy(SatisfiedByLocalNetworkMetrics.Dependencies()) + private val mockStatsManager = mock(StatsManager::class.java) + private val mockContext = mock(Context::class.java).also { + doReturn(mockStatsManager).`when`(it).getSystemService(StatsManager::class.java) + } + private val handlerThread = HandlerThread("SatisfiedByLocalNetworkMetricsTestThread").also { + it.start() + } + private val testHandler = Handler(handlerThread.looper) + private val metrics = SatisfiedByLocalNetworkMetrics(mockContext, testHandler, deps) + + @After + fun tearDown() { + handlerThread.quitSafely() + handlerThread.join(TIMEOUT_MS.toLong()) + } + + @Test + fun testLogRequest_singleStats() { + val testRequest = createTestRequest(Type.REQUEST, WIFI_CAPS) + + metrics.logRequest(testRequest, TEST_UID_1) + + // Now, pull the atom and verify the stats event + val data = mutableListOf() + val result = testHandler.postAndWait { + metrics.onPullAtom(SATISFIED_BY_LOCAL_NETWORK_REQUESTS, data) + } + + assertEquals(StatsManager.PULL_SUCCESS, result) + assertEquals(1, data.size) + verifyStatsEventBuilt(deps, TEST_UID_1, Type.REQUEST, 1) + } + + @Test + fun testLogRequest_doesNotReportWhenNotSatisfied() { + val testRequest = createTestRequest(Type.REQUEST, INTERNET_CAPS) + + metrics.logRequest(testRequest, TEST_UID_1) + + // Pull the atom; no events should be generated + val data = mutableListOf() + val result = testHandler.postAndWait { + metrics.onPullAtom(SATISFIED_BY_LOCAL_NETWORK_REQUESTS, data) + } + + assertEquals(StatsManager.PULL_SUCCESS, result) // PULL_SUCCESS even if no data + assertTrue(data.isEmpty()) // No stats events should be added + } + + @Test + fun testLogRequest_sameUidAndRequestType() { + val testRequest = createTestRequest(Type.REQUEST, WIFI_CAPS) + + metrics.logRequest(testRequest, TEST_UID_1) // Count 1 + metrics.logRequest(testRequest, TEST_UID_1) // Count 2 + metrics.logRequest(testRequest, TEST_UID_1) // Count 3 + + val data = mutableListOf() + testHandler.postAndWait { + metrics.onPullAtom(SATISFIED_BY_LOCAL_NETWORK_REQUESTS, data) + } + + assertEquals(1, data.size) + verifyStatsEventBuilt(deps, TEST_UID_1, Type.REQUEST, 3) + } + + private fun createTestRequest(reqType: Type, nc: NetworkCapabilities) = + NetworkRequest(nc, 0, 0, reqType) + + @Test + fun testLogRequest_differentUidsAndRequestTypes() { + val testRequest1 = createTestRequest(Type.REQUEST, WIFI_CAPS) + val testRequest2 = createTestRequest(Type.LISTEN, WIFI_CAPS) + + metrics.logRequest(testRequest1, TEST_UID_1) // Type.REQUEST, UID_1, count 1 + metrics.logRequest(testRequest1, TEST_UID_1) // Type.REQUEST, UID_1, count 2 + metrics.logRequest(testRequest2, TEST_UID_1) // Type.LISTEN, UID_1, count 1 + metrics.logRequest(testRequest1, TEST_UID_2) // Type.REQUEST, UID_2, count 1 + metrics.logRequest(testRequest2, TEST_UID_2) // Type.LISTEN, UID_2, count 1 + metrics.logRequest(testRequest2, TEST_UID_2) // Type.LISTEN, UID_2, count 2 + + val data = mutableListOf() + testHandler.postAndWait { + metrics.onPullAtom(SATISFIED_BY_LOCAL_NETWORK_REQUESTS, data) + } + + assertEquals(4, data.size) + verifyStatsEventBuilt(deps, TEST_UID_1, Type.REQUEST, 2) + verifyStatsEventBuilt(deps, TEST_UID_1, Type.LISTEN, 1) + verifyStatsEventBuilt(deps, TEST_UID_2, Type.REQUEST, 1) + verifyStatsEventBuilt(deps, TEST_UID_2, Type.LISTEN, 2) + } + + @Test + fun testWrongAtomTag() { + val data = mutableListOf() + val result = testHandler.postAndWait { + metrics.onPullAtom(12345, data) // A different tag + } + + assertEquals(StatsManager.PULL_SKIP, result) + assertTrue(data.isEmpty()) // No stats events should be added + verify(deps, never()).buildStatsEvent(any(), any(), any()) + } + + /** + * Helper function to verify that `buildStatsEvent` was called on the mocked dependencies + * with the specified UID, NetworkRequest.Type, and count. + * This makes it explicit what kind of StatsEvent is expected to be built. + */ + private fun verifyStatsEventBuilt( + deps: SatisfiedByLocalNetworkMetrics.Dependencies, + expectedUid: Int, + expectedRequestType: Type, + expectedCount: Int + ) { + verify(deps, times(1)).buildStatsEvent( + eq(expectedUid), + eq(expectedRequestType), + eq(expectedCount) + ) + } +} diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 08be2ecf03..471b76f013 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -393,6 +393,7 @@ import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.FakeSettingsProvider; import com.android.metrics.DefaultNetworkRematchMetrics; import com.android.metrics.SatelliteCoarseUsageMetricsCollector; +import com.android.metrics.SatisfiedByLocalNetworkMetrics; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.ArrayTrackRecord; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; @@ -654,6 +655,7 @@ public class ConnectivityServiceTest { @Mock SatelliteAccessController mSatelliteAccessController; @Mock SatelliteCoarseUsageMetricsCollector mSatelliteCoarseUsageMetricsCollector; @Mock DefaultNetworkRematchMetrics mDefaultNetworkRematchMetrics; + @Mock SatisfiedByLocalNetworkMetrics mSatisfiedByLocalNetworkMetrics; // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the // underlying binder calls. @@ -2108,6 +2110,12 @@ public class ConnectivityServiceTest { return mDefaultNetworkRematchMetrics; } + @Override + public SatisfiedByLocalNetworkMetrics makeSatisfiedByLocalNetworkMetrics(Context context, + Handler handler) { + return mSatisfiedByLocalNetworkMetrics; + } + @Override public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) { return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b)); @@ -2223,6 +2231,7 @@ public class ConnectivityServiceTest { case ConnectivityFlags.CLOSE_QUIC_CONNECTION: case ConnectivityFlags.EARLY_LINK_PROPERTIES_UPDATE_FOR_VPN: case ConnectivityFlags.CONSTRAINED_DATA_SATELLITE_METRICS: + case ConnectivityFlags.SATISFIED_BY_LOCAL_NETWORK_METRICS: return true; default: throw new UnsupportedOperationException("Unknown flag " + name diff --git a/tests/unit/java/com/android/server/connectivityservice/CSSatisfiedByLocalNetworkMetricsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSSatisfiedByLocalNetworkMetricsTest.kt new file mode 100644 index 0000000000..135cb1980e --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSSatisfiedByLocalNetworkMetricsTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2025 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 + +import android.app.PendingIntent +import android.content.Intent +import android.net.ConnectivityManager.NetworkCallback +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkRequest +import android.net.NetworkRequest.Type +import android.os.Build +import androidx.test.filters.SmallTest +import com.android.server.connectivity.ConnectivityFlags.SATISFIED_BY_LOCAL_NETWORK_METRICS +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.argThat +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.eq +import org.mockito.Mockito.never +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions + +@RunWith(DevSdkIgnoreRunner::class) +@SmallTest +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class CSSatisfiedByLocalNetworkMetricsTest : CSTest() { + companion object { + private const val TEST_UID = 12345 + } + + private val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_INTERNET).build() + private val mockCb = mock() + private val mockPendingIntent by lazy { + PendingIntent.getBroadcast( + context, + 0, + Intent("TEST_ACTION"), + PendingIntent.FLAG_IMMUTABLE + ) + } + + @Before + override fun setUp() { + super.setUp() + deps.callingUid = TEST_UID + // Clear invocations generated by satisfiedByLocalNetworkMetrics#start(). + clearInvocations(satisfiedByLocalNetworkMetrics) + } + + private fun verifyLogRequest(reqType: Type, uid: Int) { + verify(satisfiedByLocalNetworkMetrics, timeout(HANDLER_TIMEOUT_MS)) + .logRequest(argThat { it.type == reqType }, eq(uid)) + verifyNoMoreInteractions(satisfiedByLocalNetworkMetrics) + clearInvocations(satisfiedByLocalNetworkMetrics) + } + + private fun verifyNoLogRequest() { + waitForIdle() + verify(satisfiedByLocalNetworkMetrics, never()).logRequest(any(), anyInt()) + } + + @Test + fun testRequestNetwork() { + cm.requestNetwork(request, mockCb) + waitForIdle() + verifyLogRequest(Type.REQUEST, TEST_UID) + } + + @Test + fun testListenForNetwork() { + cm.registerNetworkCallback(request, mockCb) + waitForIdle() + verifyLogRequest(Type.LISTEN, TEST_UID) + } + + @Test + fun testListenForBestNetwork() { + cm.registerBestMatchingNetworkCallback(request, mockCb, csHandler) + waitForIdle() + verifyLogRequest(Type.LISTEN_FOR_BEST, TEST_UID) + } + + @Test + fun testPendingRequestForNetwork() { + cm.requestNetwork(request, mockPendingIntent) + waitForIdle() + verifyLogRequest(Type.REQUEST, TEST_UID) + } + + @Test + fun testPendingListenForNetwork() { + cm.registerNetworkCallback(request, mockPendingIntent) + waitForIdle() + verifyLogRequest(Type.LISTEN, TEST_UID) + } + + @Test + @FeatureFlags([Flag(name = SATISFIED_BY_LOCAL_NETWORK_METRICS, enabled = false)]) + fun testLogRequest_FlagDisabled() { + cm.requestNetwork(request, mockCb) + waitForIdle() + verifyNoLogRequest() + cm.unregisterNetworkCallback(mockCb) + + cm.registerNetworkCallback(request, mockCb) + waitForIdle() + verifyNoLogRequest() + } +} diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt index 1a97e3a9c0..b6b728fbdc 100644 --- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -68,6 +68,7 @@ import com.android.internal.app.IBatteryStats import com.android.internal.util.test.BroadcastInterceptingContext import com.android.metrics.DefaultNetworkRematchMetrics import com.android.metrics.SatelliteCoarseUsageMetricsCollector +import com.android.metrics.SatisfiedByLocalNetworkMetrics import com.android.modules.utils.build.SdkLevel import com.android.net.module.util.ArrayTrackRecord import com.android.net.module.util.SharedLog @@ -187,6 +188,7 @@ open class CSTest { it[ConnectivityFlags.CLOSE_QUIC_CONNECTION] = true it[ConnectivityFlags.EARLY_LINK_PROPERTIES_UPDATE_FOR_VPN] = true it[ConnectivityFlags.CONSTRAINED_DATA_SATELLITE_METRICS] = true + it[ConnectivityFlags.SATISFIED_BY_LOCAL_NETWORK_METRICS] = true } fun setFeatureEnabled(flag: String, enabled: Boolean) = enabledFeatures.set(flag, enabled) @@ -237,6 +239,7 @@ open class CSTest { val satelliteAccessController = mock() val satelliteCoarseUsageMetricsCollector = mock() val defaultNetworkRematchMetrics = mock() + val satisfiedByLocalNetworkMetrics = mock() val quicConnectionCloser = mock() val destroySocketsWrapper = mock() val dnsResolver = mock() @@ -262,7 +265,7 @@ open class CSTest { annotation class Flag(val name: String, val enabled: Boolean) @Before - fun setUp() { + open fun setUp() { // Set feature flags before constructing ConnectivityService val testMethodName = testNameRule.methodName try { @@ -357,6 +360,11 @@ open class CSTest { return defaultNetworkRematchMetrics } + override fun makeSatisfiedByLocalNetworkMetrics(context: Context, handler: Handler): + SatisfiedByLocalNetworkMetrics { + return satisfiedByLocalNetworkMetrics + } + private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) { override fun isTetheringFeatureNotChickenedOut(name: String): Boolean { return isFeatureEnabled(context, name)