Merge "Add metrics for requests satisfied by local networks" into main

This commit is contained in:
Treehugger Robot 2025-07-01 10:06:58 -07:00 • committed by Android (Google) Code Review
commit 28fc2eb8aa
7 changed files with 604 additions and 1 deletions

View file

@ -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<NetworkRequest.Type, SparseIntArray> 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<StatsEvent> 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<NetworkRequest.Type, SparseIntArray> 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);
}
}

View file

@ -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. */

View file

@ -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.
*

View file

@ -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<StatsEvent>()
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<StatsEvent>()
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<StatsEvent>()
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<StatsEvent>()
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<StatsEvent>()
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)
)
}
}

View file

@ -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

View file

@ -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<NetworkCallback>()
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()
}
}

View file

@ -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<SatelliteAccessController>()
val satelliteCoarseUsageMetricsCollector = mock<SatelliteCoarseUsageMetricsCollector>()
val defaultNetworkRematchMetrics = mock<DefaultNetworkRematchMetrics>()
val satisfiedByLocalNetworkMetrics = mock<SatisfiedByLocalNetworkMetrics>()
val quicConnectionCloser = mock<QuicConnectionCloser>()
val destroySocketsWrapper = mock<DestroySocketsWrapper>()
val dnsResolver = mock<IDnsResolver>()
@ -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)