Register OnSubscriptionsChangedListener and cache results.

Register a listener to get notified of SubscriptionInfo changes and
store all carrierIds of active subscriptions in a cache. The executor
for the listener callback runs on a different thread to the connectivity
thread but posts the SubscriptionInfo list to the connectivity thread
for caching.

Bug: 273451360
Test: atest FrameworksNetTests
(cherry picked from https://android-review.googlesource.com/q/commit:c48d856976c9fcb8150f4de524d69cc5d68ce46d)
Merged-In: I889d4da725ccda713367309c257622a0bf9939f3
Change-Id: I889d4da725ccda713367309c257622a0bf9939f3
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index b4e466a..3f0d14c 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -307,7 +307,7 @@
                 mContext, mConnectivityServiceHandler);
 
         mAlarmManager = mDependencies.getAlarmManager(context);
-        mKeepaliveStatsTracker = new KeepaliveStatsTracker(handler);
+        mKeepaliveStatsTracker = new KeepaliveStatsTracker(context, handler);
     }
 
     private void startTcpPollingAlarm(@NonNull AutomaticOnOffKeepalive ki) {
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 81345ab..9f08673 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -16,14 +16,24 @@
 
 package com.android.server.connectivity;
 
+import static android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+
 import android.annotation.NonNull;
+import android.content.Context;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.metrics.DailykeepaliveInfoReported;
@@ -31,6 +41,7 @@
 import com.android.metrics.DurationPerNumOfKeepalive;
 import com.android.metrics.KeepaliveLifetimeForCarrier;
 import com.android.metrics.KeepaliveLifetimePerCarrier;
+import com.android.modules.utils.BackgroundThread;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -52,6 +63,14 @@
     @NonNull private final Handler mConnectivityServiceHandler;
     @NonNull private final Dependencies mDependencies;
 
+    // Mapping of subId to carrierId. Updates are received from OnSubscriptionsChangedListener
+    private final SparseIntArray mCachedCarrierIdPerSubId = new SparseIntArray();
+    // The default subscription id obtained from SubscriptionManager.getDefaultSubscriptionId.
+    // Updates are done from the OnSubscriptionsChangedListener. Note that there is no callback done
+    // to OnSubscriptionsChangedListener when the default sub id changes.
+    // TODO: Register a listener for the default subId when it is possible.
+    private int mCachedDefaultSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
     // Class to store network information, lifetime durations and active state of a keepalive.
     private static final class KeepaliveStats {
         // The carrier ID for a keepalive, or TelephonyManager.UNKNOWN_CARRIER_ID(-1) if not set.
@@ -214,16 +233,53 @@
         }
     }
 
-    public KeepaliveStatsTracker(@NonNull Handler handler) {
-        this(handler, new Dependencies());
+    public KeepaliveStatsTracker(@NonNull Context context, @NonNull Handler handler) {
+        this(context, handler, new Dependencies());
     }
 
     @VisibleForTesting
-    public KeepaliveStatsTracker(@NonNull Handler handler, @NonNull Dependencies dependencies) {
+    public KeepaliveStatsTracker(
+            @NonNull Context context,
+            @NonNull Handler handler,
+            @NonNull Dependencies dependencies) {
+        Objects.requireNonNull(context);
         mDependencies = Objects.requireNonNull(dependencies);
         mConnectivityServiceHandler = Objects.requireNonNull(handler);
 
+        final SubscriptionManager subscriptionManager =
+                Objects.requireNonNull(context.getSystemService(SubscriptionManager.class));
+
         mLastUpdateDurationsTimestamp = mDependencies.getUptimeMillis();
+
+        // The default constructor for OnSubscriptionsChangedListener will always implicitly grab
+        // the looper of the current thread. In the case the current thread does not have a looper,
+        // this will throw. Therefore, post a runnable that creates it there.
+        // When the callback is called on the BackgroundThread, post a message on the CS handler
+        // thread to update the caches, which can only be touched there.
+        BackgroundThread.getHandler().post(() ->
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                        r -> r.run(), new OnSubscriptionsChangedListener() {
+                            @Override
+                            public void onSubscriptionsChanged() {
+                                final List<SubscriptionInfo> activeSubInfoList =
+                                        subscriptionManager.getActiveSubscriptionInfoList();
+                                // A null subInfo list here indicates the current state is unknown
+                                // but not necessarily empty, simply ignore it. Another call to the
+                                // listener will be invoked in the future.
+                                if (activeSubInfoList == null) return;
+                                final int defaultSubId =
+                                        subscriptionManager.getDefaultSubscriptionId();
+                                mConnectivityServiceHandler.post(() -> {
+                                    mCachedCarrierIdPerSubId.clear();
+                                    mCachedDefaultSubscriptionId = defaultSubId;
+
+                                    for (final SubscriptionInfo subInfo : activeSubInfoList) {
+                                        mCachedCarrierIdPerSubId.put(subInfo.getSubscriptionId(),
+                                                subInfo.getCarrierId());
+                                    }
+                                });
+                            }
+                        }));
     }
 
     /** Ensures the list of duration metrics is large enough for number of registered keepalives. */
@@ -279,11 +335,33 @@
         mLastUpdateDurationsTimestamp = timeNow;
     }
 
-    // TODO(b/273451360): Make use of SubscriptionManager.OnSubscriptionsChangedListener since
-    // TelephonyManager.getSimCarrierId will be a cross-process call.
-    private int getCarrierId() {
-        // No implementation yet.
-        return TelephonyManager.UNKNOWN_CARRIER_ID;
+    // TODO: Move this function to frameworks/libs/net/.../NetworkCapabilitiesUtils.java
+    private static int getSubId(@NonNull NetworkCapabilities nc, int defaultSubId) {
+        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            final NetworkSpecifier networkSpecifier = nc.getNetworkSpecifier();
+            if (networkSpecifier instanceof TelephonyNetworkSpecifier) {
+                return ((TelephonyNetworkSpecifier) networkSpecifier).getSubscriptionId();
+            }
+            // Use the default subscriptionId.
+            return defaultSubId;
+        }
+        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+            final TransportInfo info = nc.getTransportInfo();
+            if (info instanceof WifiInfo) {
+                return ((WifiInfo) info).getSubscriptionId();
+            }
+        }
+
+        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    }
+
+    private int getCarrierId(@NonNull NetworkCapabilities networkCapabilities) {
+        // Try to get the correct subscription id.
+        final int subId = getSubId(networkCapabilities, mCachedDefaultSubscriptionId);
+        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+            return TelephonyManager.UNKNOWN_CARRIER_ID;
+        }
+        return mCachedCarrierIdPerSubId.get(subId, TelephonyManager.UNKNOWN_CARRIER_ID);
     }
 
     private int getTransportTypes(@NonNull NetworkCapabilities networkCapabilities) {
@@ -313,7 +391,7 @@
 
         final KeepaliveStats newKeepaliveStats =
                 new KeepaliveStats(
-                        getCarrierId(), getTransportTypes(nc), intervalSeconds, timeNow);
+                        getCarrierId(nc), getTransportTypes(nc), intervalSeconds, timeNow);
 
         mKeepaliveStatsPerId.put(keepaliveId, newKeepaliveStats);
     }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 595b786..bb55aee 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -355,6 +355,7 @@
 import android.provider.Settings;
 import android.security.Credentials;
 import android.system.Os;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.telephony.data.EpsBearerQosSessionAttributes;
 import android.telephony.data.NrQosSessionAttributes;
@@ -616,6 +617,7 @@
     @Mock BroadcastOptionsShim mBroadcastOptionsShim;
     @Mock ActivityManager mActivityManager;
     @Mock DestroySocketsWrapper mDestroySocketsWrapper;
+    @Mock SubscriptionManager mSubscriptionManager;
 
     // BatteryStatsManager is final and cannot be mocked with regular mockito, so just mock the
     // underlying binder calls.
@@ -740,6 +742,7 @@
             if (Context.PAC_PROXY_SERVICE.equals(name)) return mPacProxyManager;
             if (Context.TETHERING_SERVICE.equals(name)) return mTetheringManager;
             if (Context.ACTIVITY_SERVICE.equals(name)) return mActivityManager;
+            if (Context.TELEPHONY_SUBSCRIPTION_SERVICE.equals(name)) return mSubscriptionManager;
             return super.getSystemService(name);
         }
 
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index e3d5c64..0aecd64 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -36,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.longThat;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.ignoreStubs;
@@ -67,6 +68,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.telephony.SubscriptionManager;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.Log;
 
@@ -121,6 +123,7 @@
     @Mock Context mCtx;
     @Mock AlarmManager mAlarmManager;
     @Mock NetworkAgentInfo mNai;
+    @Mock SubscriptionManager mSubscriptionManager;
 
     TestKeepaliveTracker mKeepaliveTracker;
     AOOTestHandler mTestHandler;
@@ -298,10 +301,22 @@
         }
     }
 
+    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+        doReturn(serviceName).when(mCtx).getSystemServiceName(serviceClass);
+        doReturn(service).when(mCtx).getSystemService(serviceName);
+        if (mCtx.getSystemService(serviceClass) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mCtx).getSystemService(serviceClass);
+        }
+    }
+
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
+        mockService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class,
+                mSubscriptionManager);
+
         mNai.networkCapabilities =
                 new NetworkCapabilities.Builder().addTransportType(TRANSPORT_CELLULAR).build();
         mNai.networkInfo = new NetworkInfo(TYPE_MOBILE, 0 /* subtype */, "LTE", "LTE");
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 369894d..b469ccd 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 
 import static com.android.testutils.HandlerUtils.visibleOnHandlerThread;
 
@@ -25,13 +26,24 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
+import android.content.Context;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.wifi.WifiInfo;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
 import android.telephony.TelephonyManager;
 
 import androidx.test.filters.SmallTest;
@@ -41,39 +53,68 @@
 import com.android.metrics.DurationPerNumOfKeepalive;
 import com.android.metrics.KeepaliveLifetimeForCarrier;
 import com.android.metrics.KeepaliveLifetimePerCarrier;
+import com.android.modules.utils.BackgroundThread;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
 public class KeepaliveStatsTrackerTest {
+    private static final int TIMEOUT_MS = 30_000;
+
     private static final int TEST_SLOT = 1;
     private static final int TEST_SLOT2 = 2;
     private static final int TEST_KEEPALIVE_INTERVAL_SEC = 10;
     private static final int TEST_KEEPALIVE_INTERVAL2_SEC = 20;
-    // Carrier id not yet implemented, assume it returns unknown for now.
-    private static final int TEST_CARRIER_ID = TelephonyManager.UNKNOWN_CARRIER_ID;
+    private static final int TEST_SUB_ID_1 = 1;
+    private static final int TEST_SUB_ID_2 = 2;
+    private static final int TEST_CARRIER_ID_1 = 135;
+    private static final int TEST_CARRIER_ID_2 = 246;
     private static final Network TEST_NETWORK = new Network(123);
     private static final NetworkCapabilities TEST_NETWORK_CAPABILITIES =
-            new NetworkCapabilities.Builder().addTransportType(TRANSPORT_CELLULAR).build();
+            buildCellNetworkCapabilitiesWithSubId(TEST_SUB_ID_1);
+    private static final NetworkCapabilities TEST_NETWORK_CAPABILITIES_2 =
+            buildCellNetworkCapabilitiesWithSubId(TEST_SUB_ID_2);
+
+    private static NetworkCapabilities buildCellNetworkCapabilitiesWithSubId(int subId) {
+        final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
+                new TelephonyNetworkSpecifier.Builder().setSubscriptionId(subId).build();
+        return new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                .setNetworkSpecifier(telephonyNetworkSpecifier)
+                .build();
+    }
 
     private HandlerThread mHandlerThread;
     private Handler mTestHandler;
 
     private KeepaliveStatsTracker mKeepaliveStatsTracker;
 
+    @Mock private Context mContext;
     @Mock private KeepaliveStatsTracker.Dependencies mDependencies;
+    @Mock private SubscriptionManager mSubscriptionManager;
+
+    private OnSubscriptionsChangedListener getOnSubscriptionsChangedListener() {
+        final ArgumentCaptor<OnSubscriptionsChangedListener> listenerCaptor =
+                ArgumentCaptor.forClass(OnSubscriptionsChangedListener.class);
+        verify(mSubscriptionManager)
+                .addOnSubscriptionsChangedListener(any(), listenerCaptor.capture());
+        return listenerCaptor.getValue();
+    }
 
     private static final class KeepaliveCarrierStats {
         public final int carrierId;
@@ -116,23 +157,53 @@
     // Use the default test carrier id, transportType and keepalive interval.
     private KeepaliveCarrierStats getDefaultCarrierStats(int lifetimeMs, int activeLifetimeMs) {
         return new KeepaliveCarrierStats(
-                TEST_CARRIER_ID,
+                TEST_CARRIER_ID_1,
                 /* transportTypes= */ (1 << TRANSPORT_CELLULAR),
                 TEST_KEEPALIVE_INTERVAL_SEC * 1000,
                 lifetimeMs,
                 activeLifetimeMs);
     }
 
+    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
+        doReturn(serviceName).when(mContext).getSystemServiceName(serviceClass);
+        doReturn(service).when(mContext).getSystemService(serviceName);
+        if (mContext.getSystemService(serviceClass) == null) {
+            // Test is using mockito-extended
+            doCallRealMethod().when(mContext).getSystemService(serviceClass);
+        }
+    }
+
+    private SubscriptionInfo makeSubInfoMock(int subId, int carrierId) {
+        final SubscriptionInfo subInfo = mock(SubscriptionInfo.class);
+        doReturn(subId).when(subInfo).getSubscriptionId();
+        doReturn(carrierId).when(subInfo).getCarrierId();
+        return subInfo;
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mockService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class,
+                mSubscriptionManager);
+
+        final SubscriptionInfo subInfo1 = makeSubInfoMock(TEST_SUB_ID_1, TEST_CARRIER_ID_1);
+        final SubscriptionInfo subInfo2 = makeSubInfoMock(TEST_SUB_ID_2, TEST_CARRIER_ID_2);
+
+        doReturn(List.of(subInfo1, subInfo2))
+                .when(mSubscriptionManager)
+                .getActiveSubscriptionInfoList();
 
         mHandlerThread = new HandlerThread("KeepaliveStatsTrackerTest");
         mHandlerThread.start();
         mTestHandler = new Handler(mHandlerThread.getLooper());
 
         setUptimeMillis(0);
-        mKeepaliveStatsTracker = new KeepaliveStatsTracker(mTestHandler, mDependencies);
+        mKeepaliveStatsTracker = new KeepaliveStatsTracker(mContext, mTestHandler, mDependencies);
+        HandlerUtils.waitForIdle(BackgroundThread.getHandler(), TIMEOUT_MS);
+
+        // Initial onSubscriptionsChanged.
+        getOnSubscriptionsChangedListener().onSubscriptionsChanged();
+        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
     }
 
     private void setUptimeMillis(long time) {
@@ -158,13 +229,18 @@
     }
 
     private void onStartKeepalive(long time, int slot, int intervalSeconds) {
+        onStartKeepalive(time, slot, TEST_NETWORK_CAPABILITIES, intervalSeconds);
+    }
+
+    private void onStartKeepalive(long time, int slot, NetworkCapabilities nc) {
+        onStartKeepalive(time, slot, nc, TEST_KEEPALIVE_INTERVAL_SEC);
+    }
+
+    private void onStartKeepalive(
+            long time, int slot, NetworkCapabilities nc, int intervalSeconds) {
         setUptimeMillis(time);
         visibleOnHandlerThread(mTestHandler, () ->
-                mKeepaliveStatsTracker.onStartKeepalive(
-                        TEST_NETWORK,
-                        slot,
-                        TEST_NETWORK_CAPABILITIES,
-                        intervalSeconds));
+                mKeepaliveStatsTracker.onStartKeepalive(TEST_NETWORK, slot, nc, intervalSeconds));
     }
 
     private void onPauseKeepalive(long time, int slot) {
@@ -732,7 +808,8 @@
 
         onStartKeepalive(startTime1, TEST_SLOT);
 
-        onStartKeepalive(startTime2, TEST_SLOT2, TEST_KEEPALIVE_INTERVAL2_SEC);
+        onStartKeepalive(startTime2, TEST_SLOT2, TEST_NETWORK_CAPABILITIES_2,
+                TEST_KEEPALIVE_INTERVAL2_SEC);
 
         onStopKeepalive(stopTime1, TEST_SLOT);
 
@@ -759,7 +836,7 @@
                 getDefaultCarrierStats(stopTime1 - startTime1, stopTime1 - startTime1);
         final KeepaliveCarrierStats expectKeepaliveCarrierStats2 =
                 new KeepaliveCarrierStats(
-                        TEST_CARRIER_ID,
+                        TEST_CARRIER_ID_2,
                         /* transportTypes= */ (1 << TRANSPORT_CELLULAR),
                         TEST_KEEPALIVE_INTERVAL2_SEC * 1000,
                         writeTime - startTime2,
@@ -783,7 +860,7 @@
         // Only the keepalive with interval of intervalSec2 is present.
         final KeepaliveCarrierStats expectKeepaliveCarrierStats3 =
                 new KeepaliveCarrierStats(
-                        TEST_CARRIER_ID,
+                        TEST_CARRIER_ID_2,
                         /* transportTypes= */ (1 << TRANSPORT_CELLULAR),
                         TEST_KEEPALIVE_INTERVAL2_SEC * 1000,
                         writeTime2 - writeTime,
@@ -861,4 +938,86 @@
                     getDefaultCarrierStats(expectRegisteredDurations[1], expectActiveDurations[1])
                 });
     }
+
+    @Test
+    public void testCarrierIdChange_changeBeforeStart() {
+        // Update the list to only have sub_id_2 with carrier_id_1.
+        final SubscriptionInfo subInfo = makeSubInfoMock(TEST_SUB_ID_2, TEST_CARRIER_ID_1);
+        doReturn(List.of(subInfo)).when(mSubscriptionManager).getActiveSubscriptionInfoList();
+
+        getOnSubscriptionsChangedListener().onSubscriptionsChanged();
+        HandlerUtils.waitForIdle(mTestHandler, TIMEOUT_MS);
+
+        final int startTime = 1000;
+        final int writeTime = 5000;
+
+        onStartKeepalive(startTime, TEST_SLOT, TEST_NETWORK_CAPABILITIES);
+        onStartKeepalive(startTime, TEST_SLOT2, TEST_NETWORK_CAPABILITIES_2);
+
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                buildKeepaliveMetrics(writeTime);
+
+        // The network with sub_id_1 has an unknown carrier id.
+        final KeepaliveCarrierStats expectKeepaliveCarrierStats1 =
+                new KeepaliveCarrierStats(
+                        TelephonyManager.UNKNOWN_CARRIER_ID,
+                        /* transportTypes= */ (1 << TRANSPORT_CELLULAR),
+                        TEST_KEEPALIVE_INTERVAL_SEC * 1000,
+                        writeTime - startTime,
+                        writeTime - startTime);
+
+        // The network with sub_id_2 has carrier_id_1.
+        final KeepaliveCarrierStats expectKeepaliveCarrierStats2 =
+                new KeepaliveCarrierStats(
+                        TEST_CARRIER_ID_1,
+                        /* transportTypes= */ (1 << TRANSPORT_CELLULAR),
+                        TEST_KEEPALIVE_INTERVAL_SEC * 1000,
+                        writeTime - startTime,
+                        writeTime - startTime);
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                /* expectRegisteredDurations= */ new int[] {startTime, 0, writeTime - startTime},
+                /* expectActiveDurations= */ new int[] {startTime, 0, writeTime - startTime},
+                new KeepaliveCarrierStats[] {
+                    expectKeepaliveCarrierStats1, expectKeepaliveCarrierStats2
+                });
+    }
+
+    @Test
+    public void testCarrierIdFromWifiInfo() {
+        final int startTime = 1000;
+        final int writeTime = 5000;
+
+        final WifiInfo wifiInfo = mock(WifiInfo.class);
+        final WifiInfo wifiInfoCopy = mock(WifiInfo.class);
+
+        // Building NetworkCapabilities stores a copy of the WifiInfo with makeCopy.
+        doReturn(wifiInfoCopy).when(wifiInfo).makeCopy(anyLong());
+        doReturn(TEST_SUB_ID_1).when(wifiInfo).getSubscriptionId();
+        doReturn(TEST_SUB_ID_1).when(wifiInfoCopy).getSubscriptionId();
+        final NetworkCapabilities nc =
+                new NetworkCapabilities.Builder()
+                        .addTransportType(TRANSPORT_WIFI)
+                        .setTransportInfo(wifiInfo)
+                        .build();
+
+        onStartKeepalive(startTime, TEST_SLOT, nc);
+
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported =
+                buildKeepaliveMetrics(writeTime);
+
+        final KeepaliveCarrierStats expectKeepaliveCarrierStats =
+                new KeepaliveCarrierStats(
+                        TEST_CARRIER_ID_1,
+                        /* transportTypes= */ (1 << TRANSPORT_WIFI),
+                        TEST_KEEPALIVE_INTERVAL_SEC * 1000,
+                        writeTime - startTime,
+                        writeTime - startTime);
+
+        assertDailyKeepaliveInfoReported(
+                dailyKeepaliveInfoReported,
+                /* expectRegisteredDurations= */ new int[] {startTime, writeTime - startTime},
+                /* expectActiveDurations= */ new int[] {startTime, writeTime - startTime},
+                new KeepaliveCarrierStats[] {expectKeepaliveCarrierStats});
+    }
 }