[1/2] Update new status bar data pipeline to support names for multiple sims

Updates the carrier name to use SubscriptionInfo instead of the ACTION_SERVICE_PROVIDERS_UPDATED intent as its source in order to support names for multiple subscriptions. Split from ag/24012407

Bug: 288631206
Test: Added/updated unit tests
Change-Id: Ib40dcf0a14e267c40a69dc4817761dab0e25142c
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
index 78231e2..99ed2d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -60,6 +60,19 @@
         }
     }
 
+    /** This name has been derived from SubscriptionModel. see [SubscriptionModel] */
+    data class SubscriptionDerived(override val name: String) : NetworkNameModel {
+        override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+            if (prevVal !is SubscriptionDerived || prevVal.name != name) {
+                row.logChange(COL_NETWORK_NAME, "SubscriptionDerived($name)")
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_NAME, "SubscriptionDerived($name)")
+        }
+    }
+
     /**
      * This name has been derived from the sim via
      * [android.telephony.TelephonyManager.getSimOperatorName].
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
index 16c4027..27f6df4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
@@ -34,4 +34,7 @@
 
     /** Subscriptions in the same group may be filtered or treated as a single subscription */
     val groupUuid: ParcelUuid? = null,
+
+    /** Text representing the name for this connection */
+    val carrierName: String,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index c1af6df..a89b1b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -115,10 +115,18 @@
      */
     val cdmaRoaming: StateFlow<Boolean>
 
-    /** The service provider name for this network connection, or the default name */
+    /** The service provider name for this network connection, or the default name. */
     val networkName: StateFlow<NetworkNameModel>
 
     /**
+     * The service provider name for this network connection, or the default name.
+     *
+     * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
+     *   provided is identical
+     */
+    val carrierName: StateFlow<NetworkNameModel>
+
+    /**
      * True if this type of connection is allowed while airplane mode is on, and false otherwise.
      */
     val isAllowedDuringAirplaneMode: StateFlow<Boolean>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
index 17d20c2..c576b82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt
@@ -184,7 +184,10 @@
 
     override val cdmaRoaming = MutableStateFlow(false)
 
-    override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network"))
+    override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived(DEMO_CARRIER_NAME))
+
+    override val carrierName =
+        MutableStateFlow(NetworkNameModel.SubscriptionDerived(DEMO_CARRIER_NAME))
 
     override val isAllowedDuringAirplaneMode = MutableStateFlow(false)
 
@@ -200,6 +203,7 @@
         // This is always true here, because we split out disabled states at the data-source level
         dataEnabled.value = true
         networkName.value = NetworkNameModel.IntentDerived(event.name)
+        carrierName.value = NetworkNameModel.SubscriptionDerived("${event.name} ${event.subId}")
 
         _carrierId.value = event.carrierId ?: INVALID_SUBSCRIPTION_ID
 
@@ -227,6 +231,7 @@
         // This is always true here, because we split out disabled states at the data-source level
         dataEnabled.value = true
         networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME)
+        carrierName.value = NetworkNameModel.SubscriptionDerived(CARRIER_MERGED_NAME)
         // TODO(b/276943904): is carrierId a thing with carrier merged networks?
         _carrierId.value = INVALID_SUBSCRIPTION_ID
         numberOfLevels.value = event.numberOfLevels
@@ -248,6 +253,7 @@
     }
 
     companion object {
+        private const val DEMO_CARRIER_NAME = "Demo Carrier"
         private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 0e4ceeb..ee13d93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -92,9 +92,12 @@
 
     private fun maybeCreateSubscription(subId: Int) {
         if (!subscriptionInfoCache.containsKey(subId)) {
-            SubscriptionModel(subscriptionId = subId, isOpportunistic = false).also {
-                subscriptionInfoCache[subId] = it
-            }
+            SubscriptionModel(
+                    subscriptionId = subId,
+                    isOpportunistic = false,
+                    carrierName = DEFAULT_CARRIER_NAME,
+                )
+                .also { subscriptionInfoCache[subId] = it }
 
             _subscriptions.value = subscriptionInfoCache.values.toList()
         }
@@ -327,6 +330,7 @@
         private const val TAG = "DemoMobileConnectionsRepo"
 
         private const val DEFAULT_SUB_ID = 1
+        private const val DEFAULT_CARRIER_NAME = "demo carrier"
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index 65f4866..28be3be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -108,6 +108,8 @@
                 NetworkNameModel.SimDerived(telephonyManager.simOperatorName),
             )
 
+    override val carrierName: StateFlow<NetworkNameModel> = networkName
+
     override val numberOfLevels: StateFlow<Int> =
         wifiRepository.wifiNetwork
             .map {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 8ba7d21..ee11c06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -47,6 +48,7 @@
     override val subId: Int,
     startingIsCarrierMerged: Boolean,
     override val tableLogBuffer: TableLogBuffer,
+    subscriptionModel: StateFlow<SubscriptionModel?>,
     private val defaultNetworkName: NetworkNameModel,
     private val networkNameSeparator: String,
     @Application scope: CoroutineScope,
@@ -80,6 +82,7 @@
         mobileRepoFactory.build(
             subId,
             tableLogBuffer,
+            subscriptionModel,
             defaultNetworkName,
             networkNameSeparator,
         )
@@ -287,6 +290,16 @@
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
 
+    override val carrierName =
+        activeRepo
+            .flatMapLatest { it.carrierName }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                initialValue = activeRepo.value.carrierName.value,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value)
+
     override val isAllowedDuringAirplaneMode =
         activeRepo
             .flatMapLatest { it.isAllowedDuringAirplaneMode }
@@ -307,6 +320,7 @@
         fun build(
             subId: Int,
             startingIsCarrierMerged: Boolean,
+            subscriptionModel: StateFlow<SubscriptionModel?>,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
         ): FullMobileConnectionRepository {
@@ -317,6 +331,7 @@
                 subId,
                 startingIsCarrierMerged,
                 mobileLogger,
+                subscriptionModel,
                 defaultNetworkName,
                 networkNameSeparator,
                 scope,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index aadc975..1f1ac92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
@@ -80,6 +81,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class MobileConnectionRepositoryImpl(
     override val subId: Int,
+    subscriptionModel: StateFlow<SubscriptionModel?>,
     defaultNetworkName: NetworkNameModel,
     networkNameSeparator: String,
     private val telephonyManager: TelephonyManager,
@@ -281,6 +283,14 @@
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
 
+    override val carrierName =
+        subscriptionModel
+            .map {
+                it?.let { model -> NetworkNameModel.SubscriptionDerived(model.carrierName) }
+                    ?: defaultNetworkName
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
     /**
      * There are a few cases where we will need to poll [TelephonyManager] so we can update some
      * internal state where callbacks aren't provided. Any of those events should be merged into
@@ -350,11 +360,13 @@
         fun build(
             subId: Int,
             mobileLogger: TableLogBuffer,
+            subscriptionModel: StateFlow<SubscriptionModel?>,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
         ): MobileConnectionRepository {
             return MobileConnectionRepositoryImpl(
                 subId,
+                subscriptionModel,
                 defaultNetworkName,
                 networkNameSeparator,
                 telephonyManager.createForSubscriptionId(subId),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 54948a4..67b04db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -319,10 +319,17 @@
 
     @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
 
+    private fun subscriptionModelForSubId(subId: Int): StateFlow<SubscriptionModel?> {
+        return subscriptions
+            .map { list -> list.firstOrNull { model -> model.subscriptionId == subId } }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+    }
+
     private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
         return fullMobileRepoFactory.build(
             subId,
             isCarrierMerged(subId),
+            subscriptionModelForSubId(subId),
             defaultNetworkName,
             networkNameSeparator,
         )
@@ -373,6 +380,7 @@
             subscriptionId = subscriptionId,
             isOpportunistic = isOpportunistic,
             groupUuid = groupUuid,
+            carrierName = carrierName.toString(),
         )
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 1a13827..4cfde5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -92,6 +92,22 @@
      */
     val networkName: StateFlow<NetworkNameModel>
 
+    /**
+     * Provider name for this network connection. The name can be one of 3 values:
+     * 1. The default network name, if one is configured
+     * 2. A name provided by the [SubscriptionModel] of this network connection
+     * 3. Or, in the case where the repository sends us the default network name, we check for an
+     *    override in [connectionInfo.operatorAlphaShort], a value that is derived from
+     *    [ServiceState]
+     *
+     * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
+     *   provided is identical
+     */
+    val carrierName: StateFlow<String>
+
+    /** True if there is only one active subscription. */
+    val isSingleCarrier: StateFlow<Boolean>
+
     /** True if this line of service is emergency-only */
     val isEmergencyOnly: StateFlow<Boolean>
 
@@ -126,6 +142,7 @@
     defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
     override val alwaysShowDataRatIcon: StateFlow<Boolean>,
     override val alwaysUseCdmaLevel: StateFlow<Boolean>,
+    override val isSingleCarrier: StateFlow<Boolean>,
     override val mobileIsDefault: StateFlow<Boolean>,
     defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
     defaultMobileIconGroup: StateFlow<MobileIconGroup>,
@@ -171,6 +188,22 @@
                 connectionRepository.networkName.value
             )
 
+    override val carrierName =
+        combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) {
+                operatorAlphaShort,
+                networkName ->
+                if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
+                    operatorAlphaShort
+                } else {
+                    networkName.name
+                }
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                connectionRepository.carrierName.value.name
+            )
+
     /** What the mobile icon would be before carrierId overrides */
     private val defaultNetworkType: StateFlow<MobileIconGroup> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index e90f40c7..d08808b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -76,6 +76,9 @@
     /** True if the CDMA level should be preferred over the primary level. */
     val alwaysUseCdmaLevel: StateFlow<Boolean>
 
+    /** True if there is only one active subscription. */
+    val isSingleCarrier: StateFlow<Boolean>
+
     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
     val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
 
@@ -252,6 +255,17 @@
             .mapLatest { it.alwaysShowCdmaRssi }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
+    override val isSingleCarrier: StateFlow<Boolean> =
+        mobileConnectionsRepo.subscriptions
+            .map { it.size == 1 }
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = LOGGING_PREFIX,
+                columnName = "isSingleCarrier",
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
     override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
         mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
@@ -298,6 +312,7 @@
             activeDataConnectionHasDataEnabled,
             alwaysShowDataRatIcon,
             alwaysUseCdmaLevel,
+            isSingleCarrier,
             mobileIsDefault,
             defaultMobileIconMapping,
             defaultMobileIconGroup,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
index 50ee6a3..ff28753 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -52,12 +52,19 @@
 
     override val cdmaRoaming = MutableStateFlow(false)
 
-    override val networkName =
-        MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default"))
+    override val networkName: MutableStateFlow<NetworkNameModel> =
+        MutableStateFlow(NetworkNameModel.Default(DEFAULT_NETWORK_NAME))
+
+    override val carrierName: MutableStateFlow<NetworkNameModel> =
+        MutableStateFlow(NetworkNameModel.Default(DEFAULT_NETWORK_NAME))
 
     override val isAllowedDuringAirplaneMode = MutableStateFlow(false)
 
     fun setDataEnabled(enabled: Boolean) {
         _dataEnabled.value = enabled
     }
+
+    companion object {
+        const val DEFAULT_NETWORK_NAME = "default name"
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 3591c17..99e4030 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -75,7 +75,11 @@
 
     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
         return subIdRepos[subId]
-            ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it }
+            ?: FakeMobileConnectionRepository(
+                    subId,
+                    tableLogBuffer,
+                )
+                .also { subIdRepos[subId] = it }
     }
 
     override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 5a887eb..d005972 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -243,13 +243,29 @@
         private val IMMEDIATE = Dispatchers.Main.immediate
 
         private const val SUB_1_ID = 1
+        private const val SUB_1_NAME = "Carrier $SUB_1_ID"
         private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-        private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_1_ID)
+                whenever(it.carrierName).thenReturn(SUB_1_NAME)
+            }
+        private val MODEL_1 =
+            SubscriptionModel(
+                subscriptionId = SUB_1_ID,
+                carrierName = SUB_1_NAME,
+            )
 
         private const val SUB_2_ID = 2
+        private const val SUB_2_NAME = "Carrier $SUB_2_ID"
         private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
-        private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_2_ID)
+                whenever(it.carrierName).thenReturn(SUB_2_NAME)
+            }
+        private val MODEL_2 =
+            SubscriptionModel(
+                subscriptionId = SUB_2_ID,
+                carrierName = SUB_2_NAME,
+            )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index 7573b28..57f97ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -38,7 +38,6 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -140,6 +139,7 @@
             launch { conn.carrierNetworkChangeActive.collect {} }
             launch { conn.isRoaming.collect {} }
             launch { conn.networkName.collect {} }
+            launch { conn.carrierName.collect {} }
             launch { conn.isEmergencyOnly.collect {} }
             launch { conn.dataConnectionState.collect {} }
         }
@@ -163,6 +163,8 @@
                 assertThat(conn.isRoaming.value).isEqualTo(model.roaming)
                 assertThat(conn.networkName.value)
                     .isEqualTo(NetworkNameModel.IntentDerived(model.name))
+                assertThat(conn.carrierName.value)
+                    .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(conn.isEmergencyOnly.value).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index efaf152..2712b70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -546,6 +546,7 @@
             launch { conn.carrierNetworkChangeActive.collect {} }
             launch { conn.isRoaming.collect {} }
             launch { conn.networkName.collect {} }
+            launch { conn.carrierName.collect {} }
             launch { conn.isEmergencyOnly.collect {} }
             launch { conn.dataConnectionState.collect {} }
         }
@@ -571,6 +572,8 @@
                 assertThat(conn.isRoaming.value).isEqualTo(model.roaming)
                 assertThat(conn.networkName.value)
                     .isEqualTo(NetworkNameModel.IntentDerived(model.name))
+                assertThat(conn.carrierName.value)
+                    .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(conn.isEmergencyOnly.value).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
index 3dd2eaf..9c0cb17 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY
@@ -43,6 +44,7 @@
 import java.io.PrintWriter
 import java.io.StringWriter
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -79,28 +81,51 @@
     private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
     private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
 
+    private val subscriptionModel =
+        MutableStateFlow(
+            SubscriptionModel(
+                subscriptionId = SUB_ID,
+                carrierName = DEFAULT_NAME,
+            )
+        )
+
     private lateinit var mobileRepo: FakeMobileConnectionRepository
     private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
 
     @Before
     fun setUp() {
-        mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
+        mobileRepo =
+            FakeMobileConnectionRepository(
+                SUB_ID,
+                tableLogBuffer,
+            )
         carrierMergedRepo =
-            FakeMobileConnectionRepository(SUB_ID, tableLogBuffer).apply {
-                // Mimicks the real carrier merged repository
-                this.isAllowedDuringAirplaneMode.value = true
-            }
+            FakeMobileConnectionRepository(
+                    SUB_ID,
+                    tableLogBuffer,
+                )
+                .apply {
+                    // Mimicks the real carrier merged repository
+                    this.isAllowedDuringAirplaneMode.value = true
+                }
 
         whenever(
                 mobileFactory.build(
                     eq(SUB_ID),
                     any(),
-                    eq(DEFAULT_NAME),
+                    any(),
+                    eq(DEFAULT_NAME_MODEL),
                     eq(SEP),
                 )
             )
             .thenReturn(mobileRepo)
-        whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(carrierMergedRepo)
+        whenever(
+                carrierMergedFactory.build(
+                    eq(SUB_ID),
+                    any(),
+                )
+            )
+            .thenReturn(carrierMergedRepo)
     }
 
     @Test
@@ -120,7 +145,8 @@
                 .build(
                     SUB_ID,
                     tableLogBuffer,
-                    DEFAULT_NAME,
+                    subscriptionModel,
+                    DEFAULT_NAME_MODEL,
                     SEP,
                 )
         }
@@ -138,7 +164,11 @@
 
             assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
             assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName)
-            verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer)
+            verify(carrierMergedFactory, never())
+                .build(
+                    SUB_ID,
+                    tableLogBuffer,
+                )
         }
 
     @Test
@@ -348,7 +378,8 @@
                 factory.build(
                     SUB_ID,
                     startingIsCarrierMerged = false,
-                    DEFAULT_NAME,
+                    subscriptionModel,
+                    DEFAULT_NAME_MODEL,
                     SEP,
                 )
 
@@ -356,7 +387,8 @@
                 factory.build(
                     SUB_ID,
                     startingIsCarrierMerged = false,
-                    DEFAULT_NAME,
+                    subscriptionModel,
+                    DEFAULT_NAME_MODEL,
                     SEP,
                 )
 
@@ -388,7 +420,8 @@
                 factory.build(
                     SUB_ID,
                     startingIsCarrierMerged = false,
-                    DEFAULT_NAME,
+                    subscriptionModel,
+                    DEFAULT_NAME_MODEL,
                     SEP,
                 )
 
@@ -397,7 +430,8 @@
                 factory.build(
                     SUB_ID,
                     startingIsCarrierMerged = true,
-                    DEFAULT_NAME,
+                    subscriptionModel,
+                    DEFAULT_NAME_MODEL,
                     SEP,
                 )
 
@@ -623,7 +657,8 @@
                 SUB_ID,
                 startingIsCarrierMerged,
                 tableLogBuffer,
-                DEFAULT_NAME,
+                subscriptionModel,
+                DEFAULT_NAME_MODEL,
                 SEP,
                 testScope.backgroundScope,
                 mobileFactory,
@@ -639,8 +674,9 @@
         val realRepo =
             MobileConnectionRepositoryImpl(
                 SUB_ID,
-                defaultNetworkName = NetworkNameModel.Default("default"),
-                networkNameSeparator = SEP,
+                subscriptionModel,
+                DEFAULT_NAME_MODEL,
+                SEP,
                 telephonyManager,
                 systemUiCarrierConfig = mock(),
                 fakeBroadcastDispatcher,
@@ -654,7 +690,8 @@
                 mobileFactory.build(
                     eq(SUB_ID),
                     any(),
-                    eq(DEFAULT_NAME),
+                    any(),
+                    eq(DEFAULT_NAME_MODEL),
                     eq(SEP),
                 )
             )
@@ -677,7 +714,13 @@
                 testScope.backgroundScope,
                 wifiRepository,
             )
-        whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(realRepo)
+        whenever(
+                carrierMergedFactory.build(
+                    eq(SUB_ID),
+                    any(),
+                )
+            )
+            .thenReturn(realRepo)
 
         return realRepo
     }
@@ -690,7 +733,8 @@
 
     private companion object {
         const val SUB_ID = 42
-        private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+        private val DEFAULT_NAME = "default name"
+        private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME)
         private const val SEP = "-"
         private const val BUFFER_SEPARATOR = "|"
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 1ff737b..e50e5e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -62,6 +62,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.configWithOverride
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig
@@ -78,6 +79,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -109,6 +111,14 @@
     private val testDispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(testDispatcher)
 
+    private val subscriptionModel: MutableStateFlow<SubscriptionModel?> =
+        MutableStateFlow(
+            SubscriptionModel(
+                subscriptionId = SUB_1_ID,
+                carrierName = DEFAULT_NAME,
+            )
+        )
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
@@ -119,7 +129,8 @@
         underTest =
             MobileConnectionRepositoryImpl(
                 SUB_1_ID,
-                DEFAULT_NAME,
+                subscriptionModel,
+                DEFAULT_NAME_MODEL,
                 SEP,
                 telephonyManager,
                 systemUiCarrierConfig,
@@ -179,6 +190,7 @@
 
             // gsmLevel updates, no change to cdmaLevel
             strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true)
+            callback.onSignalStrengthsChanged(strength)
 
             assertThat(latest).isEqualTo(2)
 
@@ -638,12 +650,51 @@
         }
 
     @Test
+    fun networkNameForSubId_updates() =
+        testScope.runTest {
+            var latest: NetworkNameModel? = null
+            val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
+
+            subscriptionModel.value =
+                SubscriptionModel(
+                    subscriptionId = SUB_1_ID,
+                    carrierName = DEFAULT_NAME,
+                )
+
+            assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
+
+            val updatedName = "Derived Carrier"
+            subscriptionModel.value =
+                SubscriptionModel(
+                    subscriptionId = SUB_1_ID,
+                    carrierName = updatedName,
+                )
+
+            assertThat(latest?.name).isEqualTo(updatedName)
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkNameForSubId_defaultWhenSubscriptionModelNull() =
+        testScope.runTest {
+            var latest: NetworkNameModel? = null
+            val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
+
+            subscriptionModel.value = null
+
+            assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
+
+            job.cancel()
+        }
+
+    @Test
     fun networkName_default() =
         testScope.runTest {
             var latest: NetworkNameModel? = null
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
 
-            assertThat(latest).isEqualTo(DEFAULT_NAME)
+            assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
 
             job.cancel()
         }
@@ -701,7 +752,7 @@
 
             fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intentWithoutInfo)
 
-            assertThat(latest).isEqualTo(DEFAULT_NAME)
+            assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
 
             job.cancel()
         }
@@ -852,8 +903,9 @@
     companion object {
         private const val SUB_1_ID = 1
 
-        private val DEFAULT_NAME = NetworkNameModel.Default("default name")
-        private const val SEP = "-"
+        private val DEFAULT_NAME = "Fake Mobile Network"
+        private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME)
+        private val SEP = "-"
 
         private const val SPN = "testSpn"
         private const val PLMN = "testPlmn"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
index 4f15aed..ea60aa7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
@@ -47,6 +48,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -97,6 +99,7 @@
     @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var logger: MobileInputLogger
     @Mock private lateinit var tableLogger: TableLogBuffer
+    @Mock private lateinit var subscriptionModel: StateFlow<SubscriptionModel?>
 
     private val mobileMappings = FakeMobileMappingsProxy()
     private val systemUiCarrierConfig =
@@ -113,11 +116,16 @@
         MockitoAnnotations.initMocks(this)
         whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
 
-        connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
+        connectionsRepo =
+            FakeMobileConnectionsRepository(
+                mobileMappings,
+                tableLogger,
+            )
 
         underTest =
             MobileConnectionRepositoryImpl(
                 SUB_1_ID,
+                subscriptionModel,
                 DEFAULT_NAME,
                 SEP,
                 telephonyManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index c8b6f13d..fd05cc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -1190,30 +1190,36 @@
     companion object {
         // Subscription 1
         private const val SUB_1_ID = 1
+        private const val SUB_1_NAME = "Carrier $SUB_1_ID"
         private val GROUP_1 = ParcelUuid(UUID.randomUUID())
         private val SUB_1 =
             mock<SubscriptionInfo>().also {
                 whenever(it.subscriptionId).thenReturn(SUB_1_ID)
                 whenever(it.groupUuid).thenReturn(GROUP_1)
+                whenever(it.carrierName).thenReturn(SUB_1_NAME)
             }
         private val MODEL_1 =
             SubscriptionModel(
                 subscriptionId = SUB_1_ID,
                 groupUuid = GROUP_1,
+                carrierName = SUB_1_NAME,
             )
 
         // Subscription 2
         private const val SUB_2_ID = 2
+        private const val SUB_2_NAME = "Carrier $SUB_2_ID"
         private val GROUP_2 = ParcelUuid(UUID.randomUUID())
         private val SUB_2 =
             mock<SubscriptionInfo>().also {
                 whenever(it.subscriptionId).thenReturn(SUB_2_ID)
                 whenever(it.groupUuid).thenReturn(GROUP_2)
+                whenever(it.carrierName).thenReturn(SUB_2_NAME)
             }
         private val MODEL_2 =
             SubscriptionModel(
                 subscriptionId = SUB_2_ID,
                 groupUuid = GROUP_2,
+                carrierName = SUB_2_NAME,
             )
 
         // Subs 3 and 4 are considered to be in the same group ------------------------------------
@@ -1242,9 +1248,14 @@
 
         // Carrier merged subscription
         private const val SUB_CM_ID = 5
+        private const val SUB_CM_NAME = "Carrier $SUB_CM_ID"
         private val SUB_CM =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) }
-        private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID)
+            mock<SubscriptionInfo>().also {
+                whenever(it.subscriptionId).thenReturn(SUB_CM_ID)
+                whenever(it.carrierName).thenReturn(SUB_CM_NAME)
+            }
+        private val MODEL_CM =
+            SubscriptionModel(subscriptionId = SUB_CM_ID, carrierName = SUB_CM_NAME)
 
         private val WIFI_INFO_CM =
             mock<WifiInfo>().apply {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 8d1da69..a3df785 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -44,6 +44,8 @@
 
     override val mobileIsDefault = MutableStateFlow(true)
 
+    override val isSingleCarrier = MutableStateFlow(true)
+
     override val networkTypeIconGroup =
         MutableStateFlow<NetworkTypeIconModel>(
             NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
@@ -51,6 +53,8 @@
 
     override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo mode"))
 
+    override val carrierName = MutableStateFlow("demo mode")
+
     private val _isEmergencyOnly = MutableStateFlow(false)
     override val isEmergencyOnly = _isEmergencyOnly
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index b2bbcfd..82b7ec4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -64,6 +64,8 @@
 
     override val mobileIsDefault = MutableStateFlow(false)
 
+    override val isSingleCarrier = MutableStateFlow(true)
+
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
     override val defaultMobileIconMapping = _defaultMobileIconMapping
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 58d3804..e3c59ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.CarrierMergedNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
@@ -40,6 +41,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -56,6 +58,15 @@
     private lateinit var underTest: MobileIconInteractor
     private val mobileMappingsProxy = FakeMobileMappingsProxy()
     private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock())
+
+    private val subscriptionModel =
+        MutableStateFlow(
+            SubscriptionModel(
+                subscriptionId = SUB_1_ID,
+                carrierName = DEFAULT_NAME,
+            )
+        )
+
     private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock())
 
     private val testDispatcher = UnconfinedTestDispatcher()
@@ -432,7 +443,7 @@
         }
 
     @Test
-    fun networkName_usesOperatorAlphaShotWhenNonNullAndRepoIsDefault() =
+    fun networkName_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
         testScope.runTest {
             var latest: NetworkNameModel? = null
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
@@ -440,7 +451,7 @@
             val testOperatorName = "operatorAlphaShort"
 
             // Default network name, operator name is non-null, uses the operator name
-            connectionRepository.networkName.value = DEFAULT_NAME
+            connectionRepository.networkName.value = DEFAULT_NAME_MODEL
             connectionRepository.operatorAlphaShort.value = testOperatorName
 
             assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
@@ -448,10 +459,39 @@
             // Default network name, operator name is null, uses the default
             connectionRepository.operatorAlphaShort.value = null
 
+            assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+
+            // Derived network name, operator name non-null, uses the derived name
+            connectionRepository.networkName.value = DERIVED_NAME_MODEL
+            connectionRepository.operatorAlphaShort.value = testOperatorName
+
+            assertThat(latest).isEqualTo(DERIVED_NAME_MODEL)
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkNameForSubId_usesOperatorAlphaShortWhenNonNullAndRepoIsDefault() =
+        testScope.runTest {
+            var latest: String? = null
+            val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
+
+            val testOperatorName = "operatorAlphaShort"
+
+            // Default network name, operator name is non-null, uses the operator name
+            connectionRepository.carrierName.value = DEFAULT_NAME_MODEL
+            connectionRepository.operatorAlphaShort.value = testOperatorName
+
+            assertThat(latest).isEqualTo(testOperatorName)
+
+            // Default network name, operator name is null, uses the default
+            connectionRepository.operatorAlphaShort.value = null
+
             assertThat(latest).isEqualTo(DEFAULT_NAME)
 
             // Derived network name, operator name non-null, uses the derived name
-            connectionRepository.networkName.value = DERIVED_NAME
+            connectionRepository.carrierName.value =
+                NetworkNameModel.SubscriptionDerived(DERIVED_NAME)
             connectionRepository.operatorAlphaShort.value = testOperatorName
 
             assertThat(latest).isEqualTo(DERIVED_NAME)
@@ -460,6 +500,21 @@
         }
 
     @Test
+    fun isSingleCarrier_matchesParent() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+
+            mobileIconsInteractor.isSingleCarrier.value = true
+            assertThat(latest).isTrue()
+
+            mobileIconsInteractor.isSingleCarrier.value = false
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun isForceHidden_matchesParent() =
         testScope.runTest {
             var latest: Boolean? = null
@@ -494,6 +549,7 @@
             mobileIconsInteractor.activeDataConnectionHasDataEnabled,
             mobileIconsInteractor.alwaysShowDataRatIcon,
             mobileIconsInteractor.alwaysUseCdmaLevel,
+            mobileIconsInteractor.isSingleCarrier,
             mobileIconsInteractor.mobileIsDefault,
             mobileIconsInteractor.defaultMobileIconMapping,
             mobileIconsInteractor.defaultMobileIconGroup,
@@ -510,7 +566,9 @@
 
         private const val SUB_1_ID = 1
 
-        private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
-        private val DERIVED_NAME = NetworkNameModel.IntentDerived("test derived name")
+        private val DEFAULT_NAME = "test default name"
+        private val DEFAULT_NAME_MODEL = NetworkNameModel.Default(DEFAULT_NAME)
+        private val DERIVED_NAME = "test derived name"
+        private val DERIVED_NAME_MODEL = NetworkNameModel.IntentDerived(DERIVED_NAME)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 1fb76b0..3e6f909 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -527,6 +527,57 @@
         }
 
     @Test
+    fun isSingleCarrier_zeroSubscriptions_false() =
+        testScope.runTest {
+            var latest: Boolean? = true
+            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setSubscriptions(emptyList())
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isSingleCarrier_oneSubscription_true() =
+        testScope.runTest {
+            var latest: Boolean? = false
+            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setSubscriptions(listOf(SUB_1))
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isSingleCarrier_twoSubscriptions_false() =
+        testScope.runTest {
+            var latest: Boolean? = true
+            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isSingleCarrier_updates() =
+        testScope.runTest {
+            var latest: Boolean? = false
+            val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+
+            connectionsRepository.setSubscriptions(listOf(SUB_1))
+            assertThat(latest).isTrue()
+
+            connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
         testScope.runTest {
             var latest: Boolean? = null
@@ -745,6 +796,7 @@
                 subscriptionId = subscriptionIds.first,
                 isOpportunistic = opportunistic.first,
                 groupUuid = groupUuid,
+                carrierName = "Carrier ${subscriptionIds.first}"
             )
 
         val sub2 =
@@ -752,6 +804,7 @@
                 subscriptionId = subscriptionIds.second,
                 isOpportunistic = opportunistic.second,
                 groupUuid = groupUuid,
+                carrierName = "Carrier ${opportunistic.second}"
             )
 
         return Pair(sub1, sub2)
@@ -760,11 +813,13 @@
     companion object {
 
         private const val SUB_1_ID = 1
-        private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID)
+        private val SUB_1 =
+            SubscriptionModel(subscriptionId = SUB_1_ID, carrierName = "Carrier $SUB_1_ID")
         private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, mock())
 
         private const val SUB_2_ID = 2
-        private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID)
+        private val SUB_2 =
+            SubscriptionModel(subscriptionId = SUB_2_ID, carrierName = "Carrier $SUB_2_ID")
         private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, mock())
 
         private const val SUB_3_ID = 3
@@ -773,6 +828,7 @@
                 subscriptionId = SUB_3_ID,
                 isOpportunistic = true,
                 groupUuid = ParcelUuid(UUID.randomUUID()),
+                carrierName = "Carrier $SUB_3_ID"
             )
         private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, mock())
 
@@ -782,6 +838,7 @@
                 subscriptionId = SUB_4_ID,
                 isOpportunistic = true,
                 groupUuid = ParcelUuid(UUID.randomUUID()),
+                carrierName = "Carrier $SUB_4_ID"
             )
         private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, mock())
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
index f0458fa..e42515e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -92,15 +92,31 @@
 
             interactor.filteredSubscriptions.value =
                 listOf(
-                    SubscriptionModel(subscriptionId = 1, isOpportunistic = false),
+                    SubscriptionModel(
+                        subscriptionId = 1,
+                        isOpportunistic = false,
+                        carrierName = "Carrier 1",
+                    ),
                 )
             assertThat(latest).isEqualTo(listOf(1))
 
             interactor.filteredSubscriptions.value =
                 listOf(
-                    SubscriptionModel(subscriptionId = 2, isOpportunistic = false),
-                    SubscriptionModel(subscriptionId = 5, isOpportunistic = true),
-                    SubscriptionModel(subscriptionId = 7, isOpportunistic = true),
+                    SubscriptionModel(
+                        subscriptionId = 2,
+                        isOpportunistic = false,
+                        carrierName = "Carrier 2",
+                    ),
+                    SubscriptionModel(
+                        subscriptionId = 5,
+                        isOpportunistic = true,
+                        carrierName = "Carrier 5",
+                    ),
+                    SubscriptionModel(
+                        subscriptionId = 7,
+                        isOpportunistic = true,
+                        carrierName = "Carrier 7",
+                    ),
                 )
             assertThat(latest).isEqualTo(listOf(2, 5, 7))
 
@@ -308,8 +324,23 @@
         }
 
     companion object {
-        private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false)
-        private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false)
-        private val SUB_3 = SubscriptionModel(subscriptionId = 3, isOpportunistic = false)
+        private val SUB_1 =
+            SubscriptionModel(
+                subscriptionId = 1,
+                isOpportunistic = false,
+                carrierName = "Carrier 1",
+            )
+        private val SUB_2 =
+            SubscriptionModel(
+                subscriptionId = 2,
+                isOpportunistic = false,
+                carrierName = "Carrier 2",
+            )
+        private val SUB_3 =
+            SubscriptionModel(
+                subscriptionId = 3,
+                isOpportunistic = false,
+                carrierName = "Carrier 3",
+            )
     }
 }