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
This commit is contained in:
parent
1d37c4ce68
commit
c00cf63e8a
7 changed files with 604 additions and 1 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue