[Sb refactor] Support NetworkName in new mobile pipeline

The network name can come from one of 3 places:
1. TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED, which passes an
   intent containing a network name which we can derive
2. ServiceState.getOperatorAlphaShort(), from the service state changed
   telephony callback
3. A default value, set in the framework config
   `lockscreen_carrier_default`

Based on the implementation that currently lives in
`MobileSignalController`, we were only using `NetworkName` a.k.a.
`EXTRA_SPN`, and not `NetworkNameData` a.k.a. `EXTRA_DATA_SPN`.
Therefore this CL only adds support for the former.

The front end does not consume this piece of data yet, since it is only
needed for QuickSettings. Future CLs will add support for a
LocationBasedMobileViewModel (similar to Wi-Fi), which can then start
consuming the values.

Test: MobileConnectionRepositoryTest
Test: DemoMobileConnectionsRepositoryTest
Test: MobileIconInteractorTest
Bug: 238425913
Change-Id: I04fa90bf7b52aa992f9721c7301f541f6085cf68
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
index e1e9540..6c37f94 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt
@@ -39,33 +39,42 @@
  * threading complex system objects through the pipeline.
  */
 data class MobileConnectionModel(
-    /** From [ServiceStateListener.onServiceStateChanged] */
+    /** Fields below are from [ServiceStateListener.onServiceStateChanged] */
     val isEmergencyOnly: Boolean = false,
     val isRoaming: Boolean = false,
+    /**
+     * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the
+     * current registered operator name in short alphanumeric format. In some cases this name might
+     * be preferred over other methods of calculating the network name
+     */
+    val operatorAlphaShort: String? = null,
 
-    /** From [SignalStrengthsListener.onSignalStrengthsChanged] */
+    /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */
     val isGsm: Boolean = false,
     @IntRange(from = 0, to = 4)
     val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
     @IntRange(from = 0, to = 4)
     val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN,
 
-    /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */
+    /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */
     val dataConnectionState: DataConnectionState = Disconnected,
 
-    /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */
+    /**
+     * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the
+     * values
+     */
     val dataActivityDirection: DataActivityModel =
         DataActivityModel(
             hasActivityIn = false,
             hasActivityOut = false,
         ),
 
-    /** From [CarrierNetworkListener.onCarrierNetworkChange] */
+    /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */
     val carrierNetworkChangeActive: Boolean = false,
 
+    /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */
+
     /**
-     * From [DisplayInfoListener.onDisplayInfoChanged].
-     *
      * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
      * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
      */
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
new file mode 100644
index 0000000..a8cf35a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 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.systemui.statusbar.pipeline.mobile.data.model
+
+import android.content.Intent
+import android.telephony.TelephonyManager.EXTRA_DATA_SPN
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+
+/**
+ * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from
+ * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED].
+ */
+sealed interface NetworkNameModel {
+    val name: String
+
+    /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */
+    data class Default(override val name: String) : NetworkNameModel
+
+    /**
+     * This name has been derived from telephony intents. see
+     * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]
+     */
+    data class Derived(override val name: String) : NetworkNameModel
+}
+
+fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? {
+    val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false)
+    val spn = getStringExtra(EXTRA_DATA_SPN)
+    val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false)
+    val plmn = getStringExtra(EXTRA_PLMN)
+
+    val str = StringBuilder()
+    val strData = StringBuilder()
+    if (showPlmn && plmn != null) {
+        str.append(plmn)
+        strData.append(plmn)
+    }
+    if (showSpn && spn != null) {
+        if (str.isNotEmpty()) {
+            str.append(separator)
+        }
+        str.append(spn)
+    }
+
+    return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null
+}
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 fc59f6e..2fd415e6 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
@@ -21,6 +21,7 @@
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyManager
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 
@@ -58,4 +59,7 @@
      * True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF]
      */
     val cdmaRoaming: StateFlow<Boolean>
+
+    /** The service provider name for this network connection, or the default name */
+    val networkName: StateFlow<NetworkNameModel>
 }
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 44eeb7c..d3ee85f 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
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+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.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -187,6 +188,7 @@
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
         connection.isDefaultDataSubscription.value = state.dataType != null
+        connection.networkName.value = NetworkNameModel.Derived(state.name)
 
         connection.cdmaRoaming.value = state.roaming
         connection.connectionInfo.value = state.toMobileConnectionModel()
@@ -266,4 +268,6 @@
     override val isDefaultDataSubscription = MutableStateFlow(true)
 
     override val cdmaRoaming = MutableStateFlow(false)
+
+    override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network"))
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
index 2cdbc19..a1ae8ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt
@@ -99,6 +99,7 @@
         val activity = getString("activity")?.toActivity()
         val carrierNetworkChange = getString("carriernetworkchange") == "show"
         val roaming = getString("roam") == "show"
+        val name = getString("networkname") ?: "demo mode"
 
         return Mobile(
             level = level,
@@ -109,6 +110,7 @@
             activity = activity,
             carrierNetworkChange = carrierNetworkChange,
             roaming = roaming,
+            name = name,
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index b8543ec..8b03f71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -35,6 +35,7 @@
         @DataActivityType val activity: Int?,
         val carrierNetworkChange: Boolean,
         val roaming: Boolean,
+        val name: String,
     ) : FakeNetworkEventModel
 
     data class MobileDisabled(
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 a9e50f6..7e9a9ce 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
@@ -17,26 +17,32 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
 import android.content.Context
+import android.content.IntentFilter
 import android.database.ContentObserver
 import android.provider.Settings.Global
 import android.telephony.CellSignalStrength
 import android.telephony.CellSignalStrengthCdma
 import android.telephony.ServiceState
 import android.telephony.SignalStrength
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
 import android.telephony.TelephonyManager
 import android.telephony.TelephonyManager.ERI_OFF
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 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.toDataConnectionType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -63,8 +69,11 @@
 class MobileConnectionRepositoryImpl(
     private val context: Context,
     override val subId: Int,
+    defaultNetworkName: NetworkNameModel,
+    networkNameSeparator: String,
     private val telephonyManager: TelephonyManager,
     private val globalSettings: GlobalSettings,
+    broadcastDispatcher: BroadcastDispatcher,
     defaultDataSubId: StateFlow<Int>,
     globalMobileDataSettingChangedEvent: Flow<Unit>,
     mobileMappingsProxy: MobileMappingsProxy,
@@ -101,6 +110,7 @@
                                 state.copy(
                                     isEmergencyOnly = serviceState.isEmergencyOnly,
                                     isRoaming = serviceState.roaming,
+                                    operatorAlphaShort = serviceState.operatorAlphaShort,
                                 )
                             trySend(state)
                         }
@@ -222,6 +232,19 @@
             .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF }
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
+    override val networkName: StateFlow<NetworkNameModel> =
+        broadcastDispatcher
+            .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
+                intent,
+                _ ->
+                if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) {
+                    defaultNetworkName
+                } else {
+                    intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+
     override val dataEnabled: StateFlow<Boolean> =
         telephonyPollingEvent
             .mapLatest { dataConnectionAllowed() }
@@ -237,6 +260,7 @@
     class Factory
     @Inject
     constructor(
+        private val broadcastDispatcher: BroadcastDispatcher,
         private val context: Context,
         private val telephonyManager: TelephonyManager,
         private val logger: ConnectivityPipelineLogger,
@@ -247,14 +271,19 @@
     ) {
         fun build(
             subId: Int,
+            defaultNetworkName: NetworkNameModel,
+            networkNameSeparator: String,
             defaultDataSubId: StateFlow<Int>,
             globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
             return MobileConnectionRepositoryImpl(
                 context,
                 subId,
+                defaultNetworkName,
+                networkNameSeparator,
                 telephonyManager.createForSubscriptionId(subId),
                 globalSettings,
+                broadcastDispatcher,
                 defaultDataSubId,
                 globalMobileDataSettingChangedEvent,
                 mobileMappingsProxy,
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 483df47..a9b3d18 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
@@ -38,12 +38,14 @@
 import com.android.internal.telephony.PhoneConstants
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
+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 com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
@@ -88,6 +90,14 @@
 ) : MobileConnectionsRepository {
     private var subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
 
+    private val defaultNetworkName =
+        NetworkNameModel.Default(
+            context.getString(com.android.internal.R.string.lockscreen_carrier_default)
+        )
+
+    private val networkNameSeparator: String =
+        context.getString(R.string.status_bar_network_name_separator)
+
     /**
      * State flow that emits the set of mobile data subscriptions, each represented by its own
      * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
@@ -243,6 +253,8 @@
     private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
         return mobileConnectionRepositoryFactory.build(
             subId,
+            defaultNetworkName,
+            networkNameSeparator,
             defaultDataSubId,
             globalMobileDataSettingChangedEvent,
         )
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 ae39d12..76e6a96a 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
@@ -20,6 +20,7 @@
 import com.android.settingslib.SignalIcon.MobileIconGroup
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.util.CarrierConfigTracker
@@ -56,6 +57,15 @@
     /** Observable for RAT type (network type) indicator */
     val networkTypeIconGroup: StateFlow<MobileIconGroup>
 
+    /**
+     * 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 derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED]
+     * 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]
+     */
+    val networkName: StateFlow<NetworkNameModel>
+
     /** True if this line of service is emergency-only */
     val isEmergencyOnly: StateFlow<Boolean>
 
@@ -93,6 +103,22 @@
 
     override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
 
+    override val networkName =
+        combine(connectionInfo, connectionRepository.networkName) { connection, networkName ->
+                if (
+                    networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
+                ) {
+                    NetworkNameModel.Derived(connection.operatorAlphaShort)
+                } else {
+                    networkName
+                }
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                connectionRepository.networkName.value
+            )
+
     /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
     override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
         combine(
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 7b9929d..59eec53 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
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
 // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository
@@ -32,6 +33,9 @@
 
     override val cdmaRoaming = MutableStateFlow(false)
 
+    override val networkName =
+        MutableStateFlow<NetworkNameModel>(NetworkNameModel.Default("default"))
+
     fun setConnectionInfo(model: MobileConnectionModel) {
         _connectionInfo.value = model
     }
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 9b602cc..3d5316d 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
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.util.mockito.mock
@@ -98,6 +99,7 @@
                     activity = testCase.activity,
                     carrierNetworkChange = testCase.carrierNetworkChange,
                     roaming = testCase.roaming,
+                    name = "demo name",
                 )
 
             fakeNetworkEventFlow.value = networkModel
@@ -121,6 +123,7 @@
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
                 assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -144,6 +147,7 @@
         @Annotation.DataActivityType val activity: Int,
         val carrierNetworkChange: Boolean,
         val roaming: Boolean,
+        val name: String,
     ) {
         override fun toString(): String {
             return "INPUT(level=$level, " +
@@ -153,7 +157,8 @@
                 "inflateStrength=$inflateStrength, " +
                 "activity=$activity, " +
                 "carrierNetworkChange=$carrierNetworkChange, " +
-                "roaming=$roaming)"
+                "roaming=$roaming, " +
+                "name=$name)"
         }
 
         // Convenience for iterating test data and creating new cases
@@ -166,6 +171,7 @@
             @Annotation.DataActivityType activity: Int? = null,
             carrierNetworkChange: Boolean? = null,
             roaming: Boolean? = null,
+            name: String? = null,
         ): TestCase =
             TestCase(
                 level = level ?: this.level,
@@ -176,6 +182,7 @@
                 activity = activity ?: this.activity,
                 carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
                 roaming = roaming ?: this.roaming,
+                name = name ?: this.name,
             )
     }
 
@@ -204,6 +211,7 @@
         private val carrierNetworkChange = booleanList
         // false first so the base case doesn't have roaming set (more common)
         private val roaming = listOf(false, true)
+        private val names = listOf("name 1", "name 2")
 
         @Parameters(name = "{0}") @JvmStatic fun data() = testData()
 
@@ -239,6 +247,7 @@
                     activity.first(),
                     carrierNetworkChange.first(),
                     roaming.first(),
+                    names.first(),
                 )
 
             val tail =
@@ -249,7 +258,8 @@
                         inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
                         activity.map { baseCase.modifiedBy(activity = it) },
                         carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
-                        roaming.map { baseCase.modifiedBy(roaming = it) }
+                        roaming.map { baseCase.modifiedBy(roaming = it) },
+                        names.map { baseCase.modifiedBy(name = it) },
                     )
                     .flatten()
 
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 8714a9e..34f30eb 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
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+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.demo.model.FakeNetworkEventModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
@@ -296,6 +297,7 @@
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
                 assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
+                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -328,4 +330,5 @@
         activity = activity,
         carrierNetworkChange = carrierNetworkChange,
         roaming = roaming,
+        name = "demo name",
     )
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 ebfe4f0..7fa8065 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
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
+import android.content.Intent
 import android.os.UserHandle
 import android.provider.Settings
 import android.telephony.CellSignalStrengthCdma
@@ -40,15 +41,22 @@
 import android.telephony.TelephonyManager.DATA_UNKNOWN
 import android.telephony.TelephonyManager.ERI_OFF
 import android.telephony.TelephonyManager.ERI_ON
+import android.telephony.TelephonyManager.EXTRA_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN
+import android.telephony.TelephonyManager.EXTRA_SHOW_SPN
+import android.telephony.TelephonyManager.EXTRA_SPN
+import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
 import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
 import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 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.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -98,8 +106,11 @@
             MobileConnectionRepositoryImpl(
                 context,
                 SUB_1_ID,
+                DEFAULT_NAME,
+                SEP,
                 telephonyManager,
                 globalSettings,
+                fakeBroadcastDispatcher,
                 connectionsRepo.defaultDataSubId,
                 connectionsRepo.globalMobileDataSettingChangedEvent,
                 mobileMappings,
@@ -506,6 +517,74 @@
             job.cancel()
         }
 
+    @Test
+    fun `network name - default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - uses broadcast info - returns derived`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val intent = spnIntent()
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intent)
+            }
+
+            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - broadcast not for this sub id - returns default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val intent = spnIntent(subId = 101)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intent)
+            }
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - operatorAlphaShort - tracked`() =
+        runBlocking(IMMEDIATE) {
+            var latest: String? = null
+
+            val job =
+                underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this)
+
+            val shortName = "short name"
+            val serviceState = ServiceState()
+            serviceState.setOperatorName(
+                /* longName */ "long name",
+                /* shortName */ shortName,
+                /* numeric */ "12345",
+            )
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest).isEqualTo(shortName)
+
+            job.cancel()
+        }
+
     private fun getTelephonyCallbacks(): List<TelephonyCallback> {
         val callbackCaptor = argumentCaptor<TelephonyCallback>()
         Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
@@ -531,10 +610,31 @@
         return signalStrength
     }
 
+    private fun spnIntent(
+        subId: Int = SUB_1_ID,
+        showSpn: Boolean = true,
+        spn: String = SPN,
+        showPlmn: Boolean = true,
+        plmn: String = PLMN,
+    ): Intent =
+        Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply {
+            putExtra(EXTRA_SUBSCRIPTION_ID, subId)
+            putExtra(EXTRA_SHOW_SPN, showSpn)
+            putExtra(EXTRA_SPN, spn)
+            putExtra(EXTRA_SHOW_PLMN, showPlmn)
+            putExtra(EXTRA_PLMN, plmn)
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private val DEFAULT_NAME = NetworkNameModel.Default("default name")
+        private const 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/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6d80acb..3cc1e8b 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
@@ -91,6 +91,7 @@
 
         connectionFactory =
             MobileConnectionRepositoryImpl.Factory(
+                fakeBroadcastDispatcher,
                 context = context,
                 telephonyManager = telephonyManager,
                 bgDispatcher = IMMEDIATE,
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 27a0b55..c3519b7 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
@@ -19,6 +19,7 @@
 import android.telephony.CellSignalStrength
 import com.android.settingslib.SignalIcon
 import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -36,6 +37,8 @@
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
     override val networkTypeIconGroup = _iconGroup
 
+    override val networkName = MutableStateFlow(NetworkNameModel.Derived("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/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 9b6f6df..4dca780 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
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 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.repository.FakeMobileConnectionRepository
@@ -392,6 +393,41 @@
             job.cancel()
         }
 
+    @Test
+    fun `network name - uses operatorAlphaShot when non null and repo is default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val testOperatorName = "operatorAlphaShort"
+
+            // Default network name, operator name is non-null, uses the operator name
+            connectionRepository.networkName.value = DEFAULT_NAME
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(operatorAlphaShort = testOperatorName)
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName))
+
+            // Default network name, operator name is null, uses the default
+            connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null))
+            yield()
+
+            assertThat(latest).isEqualTo(DEFAULT_NAME)
+
+            // Derived network name, operator name non-null, uses the derived name
+            connectionRepository.networkName.value = DERIVED_NAME
+            connectionRepository.setConnectionInfo(
+                MobileConnectionModel(operatorAlphaShort = testOperatorName)
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(DERIVED_NAME)
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -401,5 +437,8 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
+        private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
     }
 }