diff --git a/service/Android.bp b/service/Android.bp index e71a6c176f..fb74238caf 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -164,6 +164,7 @@ java_library { "framework-bluetooth.stubs.module_lib", "framework-configinfrastructure.stubs.module_lib", "framework-connectivity-pre-jarjar", + "framework-connectivity-t-pre-jarjar", // The framework-connectivity-t library is only available on T+ platforms // so any calls to it must be protected with a check to ensure that it is // available. The linter will detect any unprotected calls through an API diff --git a/service/src/com/android/metrics/SatelliteCoarseUsageMetricsCollector.java b/service/src/com/android/metrics/SatelliteCoarseUsageMetricsCollector.java new file mode 100644 index 0000000000..2da07b9995 --- /dev/null +++ b/service/src/com/android/metrics/SatelliteCoarseUsageMetricsCollector.java @@ -0,0 +1,320 @@ +/* + * 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_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_SATELLITE; +import static android.net.NetworkTemplate.MATCH_MOBILE; + +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED__EVENT_TYPE__CRITICAL_BYTES_EVENT_TYPE_SATELLITE_COARSE_RX_USAGE; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED__EVENT_TYPE__CRITICAL_BYTES_EVENT_TYPE_SATELLITE_COARSE_TX_USAGE; + +import static java.util.concurrent.TimeUnit.HOURS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.usage.NetworkStats; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkRequest; +import android.net.NetworkTemplate; +import android.os.Handler; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.modules.utils.BackgroundThread; +import com.android.modules.utils.build.SdkLevel; +import com.android.net.module.util.FrameworkConnectivityStatsLog; + +import javax.annotation.CheckReturnValue; + +/** + * Monitors satellite network connections to collect overall usage during active + * satellite sessions. It uses NetworkStatsManager to query snapshots upon + * connection and disconnection, then logs the difference. + * + * This class does not provide thread safety and should be only accessed on the same thread. + */ +public class SatelliteCoarseUsageMetricsCollector { + private static final String TAG = SatelliteCoarseUsageMetricsCollector.class.getSimpleName(); + private static final NetworkTemplate SATELLITE_TEMPLATE; + + static { + // To prevent NetworkTemplate#Builder not found error when + // initializing CSTest on S-based image. + if (SdkLevel.isAtLeastU()) { + SATELLITE_TEMPLATE = new NetworkTemplate.Builder(MATCH_MOBILE) + .setTransportTypes(new int[]{TRANSPORT_SATELLITE}).build(); + } else { + SATELLITE_TEMPLATE = null; + } + } + /** + * The max bucket duration used when storing usage in NetworkStatsService. + * See NetworkStatsService#DefaultNetworkStatsSettings#getUidConfig. + */ + private static final long MAX_NETSTATS_BUCKET_DURATION_MS = HOURS.toMillis(2); + + private ConnectivityManager mCm; + private NetworkStatsManager mNsm; + private final Context mContext; + private final Handler mHandler; + private final SatelliteNetworkCallback mNetworkCallback = new SatelliteNetworkCallback(); + + // Null when no satellite network is connected. + @Nullable + private MyStatsEntry mSatelliteBaseline = null; + + private final MyStatsEntry mReportedUsage = new MyStatsEntry(); + private final Dependencies mDeps; + + // The start timestamp used to query the snapshots of data usage. + final long mStartTime; + + /** + * Constructs a helper to monitor satellite network usage. + * + * @param context The context. + */ + public SatelliteCoarseUsageMetricsCollector(@NonNull Context context) { + this(context, new Dependencies()); + } + + /** + * Constructs a helper to monitor satellite network usage. + * + * @param context The context. + */ + public SatelliteCoarseUsageMetricsCollector(@NonNull Context context, + @NonNull Dependencies deps) { + mContext = context; + mDeps = deps; + mHandler = mDeps.getBackgroundThreadHandler(); + mStartTime = getStartTimestamp(); + } + + /** + * Helper class for test injection. + */ + @VisibleForTesting + public static class Dependencies { + /** + * Reports the given network usage entry to system metrics. + */ + public void reportUsage(MyStatsEntry entry) { + FrameworkConnectivityStatsLog.write_non_chained(CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED, + Process.SYSTEM_UID, + TAG, + CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED__EVENT_TYPE__CRITICAL_BYTES_EVENT_TYPE_SATELLITE_COARSE_RX_USAGE, + entry.rxBytes); + FrameworkConnectivityStatsLog.write_non_chained(CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED, + Process.SYSTEM_UID, + TAG, + CORE_NETWORKING_CRITICAL_BYTES_EVENT_OCCURRED__EVENT_TYPE__CRITICAL_BYTES_EVENT_TYPE_SATELLITE_COARSE_TX_USAGE, + entry.txBytes); + } + + /** + * Queries the network stats summary for a given network template. + */ + @NonNull + public MyStatsEntry getSummary(@NonNull NetworkStatsManager nsm, long startTime) { + final MyStatsEntry ret = new MyStatsEntry(); + final NetworkStats stats = nsm.querySummary(SATELLITE_TEMPLATE, + startTime, Long.MAX_VALUE); + // This null check simplifies testing by avoiding the need to mock + // the complex NetworkStats object. In production, this object + // is not expected to be null. See ConnectivityServiceIntegrationTest + // for context. + if (stats == null) { + return ret; + } + final NetworkStats.Bucket recycle = new NetworkStats.Bucket(); + while (stats.hasNextBucket()) { + stats.getNextBucket(recycle); + ret.add(new MyStatsEntry(recycle)); + } + return ret; + } + + /** Get the background thread handler */ + @NonNull + public Handler getBackgroundThreadHandler() { + return BackgroundThread.getHandler(); + } + } + + @VisibleForTesting + public static class MyStatsEntry { + public long rxBytes; + public long txBytes; + + public MyStatsEntry() { + this.rxBytes = 0; + this.txBytes = 0; + } + + public MyStatsEntry(@NonNull NetworkStats.Bucket bucket) { + this.rxBytes = bucket.getRxBytes(); + this.txBytes = bucket.getTxBytes(); + } + + public MyStatsEntry(long rxBytes, long txBytes) { + this.rxBytes = rxBytes; + this.txBytes = txBytes; + } + + /** + * Adds the stats from another {@link MyStatsEntry} to this entry. + * + * This method modifies the current object. + */ + public void add(@NonNull MyStatsEntry other) { + this.rxBytes += other.rxBytes; + this.txBytes += other.txBytes; + } + + /** + * Subtracts the stats from another {@link MyStatsEntry} to this entry. + * + * This method modifies the current object. + */ + public void subtract(@NonNull MyStatsEntry other) { + this.rxBytes -= other.rxBytes; + this.txBytes -= other.txBytes; + } + + /** + * Adds the stats from another {@link MyStatsEntry} from this entry. + * + * This method does not modify the current object but returns a new one. + */ + @CheckReturnValue + @NonNull + public MyStatsEntry plus(@NonNull MyStatsEntry other) { + return new MyStatsEntry(rxBytes + other.rxBytes, txBytes + other.txBytes); + } + + /** + * Subtracts the stats from another {@link MyStatsEntry} from this entry. + * + * This method does not modify the current object but returns a new one. + */ + @CheckReturnValue + @NonNull + public MyStatsEntry minus(@NonNull MyStatsEntry other) { + return new MyStatsEntry(rxBytes - other.rxBytes, txBytes - other.txBytes); + } + + @Override + public String toString() { + return "{rxBytes=" + rxBytes + ", txBytes=" + txBytes + "}"; + } + } + + private class SatelliteNetworkCallback extends ConnectivityManager.NetworkCallback { + /** + * Called when a satellite network is lost. Collects and reports the satellite coarse usage + * difference. + * + * Note that multiple satellite networks could occur, for example, when + * transitioning from one subscription to another. The amount of usage might + * be slightly inaccurate in such cases. + * + * @param network The Network object that was just lost. + */ + @Override + public void onLost(Network network) { + final MyStatsEntry snapshot = mDeps.getSummary(mNsm, mStartTime); + // Report diff with metrics. + final MyStatsEntry diff = snapshot.minus(mSatelliteBaseline); + mDeps.reportUsage(diff); + mReportedUsage.add(diff); + mSatelliteBaseline = snapshot; + Log.d(TAG, "onLost: Last satellite network " + network + ", Reported usage: " + diff); + } + } + + // Calculate the start timestamp used for querying data usage snapshots. + // This timestamp needs to be early enough to ensure the query window + // starts at or before the beginning of the usage bucket in which the + // device boot event occurred. This is achieved by setting the start time + // to one bucket duration prior to the calculated bootTimeMillis. + // + // It is acceptable if this timestamp is not perfectly aligned with exact + // bucket boundaries or results in covering some redundant data from before + // the boot event. Any such duplicated or irrelevant data will be effectively + // subtracted out when calculating usage from the differences of two snapshots, + // provided both snapshots are queried using this same consistent start timestamp. + private long getStartTimestamp() { + final long elapsedMillisSinceBoot = SystemClock.elapsedRealtime(); + final long currentTimeMillis = System.currentTimeMillis(); + final long bootTimeMillis = currentTimeMillis - elapsedMillisSinceBoot; + return bootTimeMillis - MAX_NETSTATS_BUCKET_DURATION_MS; + } + + /** + * Starts monitoring for satellite network connections. + */ + public void startMonitoring() { + if (!SdkLevel.isAtLeastU()) { + throw new UnsupportedOperationException(TAG + " is not supported below U"); + } + mHandler.post(() -> handleStartMonitoring()); + } + + private void handleStartMonitoring() { + // Note that in the constructor it is too early to get the managers. + mCm = mContext.getSystemService(ConnectivityManager.class); + mNsm = mContext.getSystemService(NetworkStatsManager.class); + // Note: This might create heavy workload on NetworkStatsService if queried with high + // frequency, as the calling UID is system. Each query made with this manager + // updates usage stored in files before returning the newest result. + // This behavior is not rate-limited because the calling uid is the system UID. + mNsm.setPollOnOpen(true); + // Get the first baseline. + mSatelliteBaseline = mDeps.getSummary(mNsm, mStartTime); + + final NetworkRequest satelliteRequest = new NetworkRequest.Builder() + .clearCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_SATELLITE) + .build(); + // The callback uses the shared background thread. Potential delays of + // dozens of seconds are acceptable because this class measures coarse + // usage after a connection is lost, so the metric's accuracy is not + // that sensitive to the timing. + // A dedicated thread would be needed if it turns out inaccurate. + mCm.registerNetworkCallback(satelliteRequest, mNetworkCallback, mHandler); + } + + /** Dump info to dumpsys */ + public void dump(@NonNull IndentingPrintWriter pw) { + pw.println(TAG + ":"); + pw.increaseIndent(); + pw.println("Reported usage " + mReportedUsage); + pw.decreaseIndent(); + pw.println(); + } +} diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index dc1e89ce75..2a9d0140a1 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -344,6 +344,7 @@ import com.android.metrics.NetworkList; import com.android.metrics.NetworkRequestCount; import com.android.metrics.RequestCountForType; import com.android.metrics.SatelliteAccessInfo; +import com.android.metrics.SatelliteCoarseUsageMetricsCollector; import com.android.modules.utils.BasicShellCommandHandler; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; @@ -1066,6 +1067,7 @@ public class ConnectivityService extends IConnectivityManager.Stub private final NetworkNotificationManager mNotifier; private final LingerMonitor mLingerMonitor; private final SatelliteAccessController mSatelliteAccessController; + private final SatelliteCoarseUsageMetricsCollector mSatelliteCoarseUsageMetricsCollector; private final L2capNetworkProvider mL2capNetworkProvider; @@ -1707,6 +1709,15 @@ public class ConnectivityService extends IConnectivityManager.Stub connectivityServiceInternalHandler); } + /** + * @see SatelliteCoarseUsageMetricsCollector + */ + @Nullable + public SatelliteCoarseUsageMetricsCollector makeSatelliteCoarseUsageMetricsCollector( + @NonNull final Context context) { + return new SatelliteCoarseUsageMetricsCollector(context); + } + /** Creates an L2capNetworkProvider */ public L2capNetworkProvider makeL2capNetworkProvider(Context context) { return new L2capNetworkProvider(context); @@ -2090,6 +2101,12 @@ public class ConnectivityService extends IConnectivityManager.Stub } mConstrainedDataSatelliteMetrics = (mSatelliteAccessController != null) && mDeps.isFeatureNotChickenedOut(mContext, CONSTRAINED_DATA_SATELLITE_METRICS); + if (mConstrainedDataSatelliteMetrics) { + mSatelliteCoarseUsageMetricsCollector = + mDeps.makeSatelliteCoarseUsageMetricsCollector(mContext); + } else { + mSatelliteCoarseUsageMetricsCollector = null; + } if (mDeps.flagConnectivityServiceDestroySocket()) { mIpToNetworksMap = new HashMap<>(); @@ -4363,6 +4380,10 @@ public class ConnectivityService extends IConnectivityManager.Stub // forever, this replaces a synchronous call to PermissionMonitor#initialize, which // could have blocked forever too. permissionMonitorInitializeDone.block(); + + if (mConstrainedDataSatelliteMetrics) { + mSatelliteCoarseUsageMetricsCollector.startMonitoring(); + } } /** @@ -4689,6 +4710,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (mSatelliteAccessController != null) { mSatelliteAccessController.dump(pw); } + if (mConstrainedDataSatelliteMetrics) { + mSatelliteCoarseUsageMetricsCollector.dump(pw); + } pw.println(); pw.println("Legacy network activity:"); diff --git a/tests/unit/java/com/android/metrics/SatelliteCoarseUsageMetricsCollectorTest.kt b/tests/unit/java/com/android/metrics/SatelliteCoarseUsageMetricsCollectorTest.kt new file mode 100644 index 0000000000..989fceeeb3 --- /dev/null +++ b/tests/unit/java/com/android/metrics/SatelliteCoarseUsageMetricsCollectorTest.kt @@ -0,0 +1,156 @@ +/* + * 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.usage.NetworkStatsManager +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import android.os.Handler +import android.os.Looper +import com.android.metrics.SatelliteCoarseUsageMetricsCollector.MyStatsEntry +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.waitForIdle +import kotlin.test.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +/** + * Unit tests for [SatelliteCoarseUsageMetricsCollector]. + * These tests mock Android system services and the custom Dependencies class + * to verify the behavior of the collector during satellite network events, + * using MockK for mocking and argument capturing. + */ +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) +class SatelliteCoarseUsageMetricsCollectorTest { + companion object { + const val THREAD_BLOCK_TIMEOUT_MS = 1000L + } + + private val mockCm = mock(ConnectivityManager::class.java) + private val mockNsm = mock(NetworkStatsManager::class.java) + private val mockCtx = mock(Context::class.java).also { + doReturn(mockCm).`when`(it).getSystemService(ConnectivityManager::class.java) + doReturn(mockNsm).`when`(it).getSystemService(NetworkStatsManager::class.java) + } + private val handler = Handler(Looper.getMainLooper()) + private val mockDeps = mock(SatelliteCoarseUsageMetricsCollector.Dependencies::class.java) + .also { + doReturn(handler).`when`(it).backgroundThreadHandler + } + private val collector = SatelliteCoarseUsageMetricsCollector(mockCtx, mockDeps) + + // Return the network callback for listening network changes. + private fun startMonitoring(): ConnectivityManager.NetworkCallback { + val cbCaptor = ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback::class.java) + collector.startMonitoring() + handler.waitForIdle(THREAD_BLOCK_TIMEOUT_MS) + verify(mockCm).registerNetworkCallback(any(), cbCaptor.capture(), any()) + return cbCaptor.value.also { + assertNotNull(it) + } + } + + private fun mockGetSummary(rxBytes: Long, txBytes: Long) { + val statsEntry = MyStatsEntry(rxBytes, txBytes) + doReturn(statsEntry).`when`(mockDeps).getSummary(any(), anyLong()) + } + + private fun assertReportedUsage(rxBytes: Long, txBytes: Long) { + val reportedUsageCaptor = ArgumentCaptor.forClass(MyStatsEntry::class.java) + verify(mockDeps).reportUsage(reportedUsageCaptor.capture()) + + val reportedUsage = reportedUsageCaptor.value + assertEquals(rxBytes, reportedUsage.rxBytes) + assertEquals(txBytes, reportedUsage.txBytes) + } + + @Test + fun testSingleNetwork() { + val mockNet = mock(Network::class.java) + mockGetSummary(100, 50) + val cb = startMonitoring() + mockGetSummary(300, 150) + cb.onLost(mockNet) + assertReportedUsage(200, 100) + } + + // Test multiple satellite networks. This could happen when telephony creates + // satellite networks with different types of services (e.g., MMS, internet). + @Test + fun testMultipleNetworks() { + // Mock multiple networks, ensuring they are identified as distinct. + val mockNet1 = Network(1) + val mockNet2 = Network(2) + + // Simulate baseline. + mockGetSummary(100, 50) + val cb = startMonitoring() + + // Simulate onAvailable events, verify nothing happens. + cb.onAvailable(mockNet1) + cb.onAvailable(mockNet2) + verify(mockDeps, never()).reportUsage(any()) + + // Simulate first network lost (not the last), simulate no change at all, + // verify reportUsage is invoked with zero bytes. + cb.onLost(mockNet1) + assertReportedUsage(0L, 0L) + + // Simulate second network lost (the last one). + clearInvocations(mockDeps) + mockGetSummary(300, 150) + cb.onLost(mockNet2) + assertReportedUsage(200L, 100L) + } + + @Test + fun testMyStatsEntryPlusSubtract() { + val entry1 = MyStatsEntry(10, 20) + val entry2 = MyStatsEntry(5, 10) + val result = entry1.plus(entry2) + assertEquals(15L, result.rxBytes) + assertEquals(30L, result.txBytes) + entry1.subtract(entry2) + assertEquals(5L, entry1.rxBytes) + assertEquals(10L, entry1.txBytes) + } + + @Test + fun testMyStatsEntryMinusAdd() { + val entry1 = MyStatsEntry(100, 200) + val entry2 = MyStatsEntry(10, 20) + val result = entry1.minus(entry2) + assertEquals(90L, result.rxBytes) + assertEquals(180L, result.txBytes) + entry1.add(entry2) + assertEquals(110L, entry1.rxBytes) + assertEquals(220L, entry1.txBytes) + } +} diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 648d2b0cfd..7cb7219411 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -391,6 +391,7 @@ import com.android.internal.net.VpnConfig; import com.android.internal.util.WakeupMessage; import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.metrics.SatelliteCoarseUsageMetricsCollector; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.ArrayTrackRecord; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; @@ -650,6 +651,7 @@ public class ConnectivityServiceTest { @Mock SubscriptionManager mSubscriptionManager; @Mock KeepaliveTracker.Dependencies mMockKeepaliveTrackerDependencies; @Mock SatelliteAccessController mSatelliteAccessController; + @Mock SatelliteCoarseUsageMetricsCollector mSatelliteCoarseUsageMetricsCollector; // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the // underlying binder calls. @@ -2093,6 +2095,12 @@ public class ConnectivityServiceTest { return mSatelliteAccessController; } + @Override + public SatelliteCoarseUsageMetricsCollector makeSatelliteCoarseUsageMetricsCollector( + @NonNull final Context context) { + return mSatelliteCoarseUsageMetricsCollector; + } + @Override public boolean intentFilterEquals(final PendingIntent a, final PendingIntent b) { return runAsShell(GET_INTENT_SENDER_INTENT, () -> a.intentFilterEquals(b)); 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 d98553a277..916f04d93d 100644 --- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt +++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt @@ -66,6 +66,7 @@ import android.util.SparseArray import androidx.test.platform.app.InstrumentationRegistry import com.android.internal.app.IBatteryStats import com.android.internal.util.test.BroadcastInterceptingContext +import com.android.metrics.SatelliteCoarseUsageMetricsCollector import com.android.modules.utils.build.SdkLevel import com.android.net.module.util.ArrayTrackRecord import com.android.net.module.util.SharedLog @@ -233,6 +234,7 @@ open class CSTest { val multicastRoutingCoordinatorService = mock() val satelliteAccessController = mock() + val satelliteCoarseUsageMetricsCollector = mock() val quicConnectionCloser = mock() val destroySocketsWrapper = mock() val dnsResolver = mock() @@ -345,6 +347,10 @@ open class CSTest { return satelliteAccessController } + override fun makeSatelliteCoarseUsageMetricsCollector( + context: Context + ) = satelliteCoarseUsageMetricsCollector + private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) { override fun isTetheringFeatureNotChickenedOut(name: String): Boolean { return isFeatureEnabled(context, name)