From 1685e6b9319bf2a08aaeeec245ed10f1f2e3af91 Mon Sep 17 00:00:00 2001 From: Shawn Lee Date: Mon, 10 Jul 2023 15:09:23 -0700 Subject: [PATCH 1/2] [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 --- .../mobile/data/model/NetworkNameModel.kt | 13 +++ .../mobile/data/model/SubscriptionModel.kt | 3 + .../repository/MobileConnectionRepository.kt | 10 ++- .../demo/DemoMobileConnectionRepository.kt | 8 +- .../demo/DemoMobileConnectionsRepository.kt | 10 ++- .../prod/CarrierMergedConnectionRepository.kt | 2 + .../prod/FullMobileConnectionRepository.kt | 15 ++++ .../prod/MobileConnectionRepositoryImpl.kt | 12 +++ .../prod/MobileConnectionsRepositoryImpl.kt | 8 ++ .../domain/interactor/MobileIconInteractor.kt | 33 ++++++++ .../interactor/MobileIconsInteractor.kt | 15 ++++ .../FakeMobileConnectionRepository.kt | 11 ++- .../FakeMobileConnectionsRepository.kt | 6 +- .../MobileRepositorySwitcherTest.kt | 24 +++++- .../DemoMobileConnectionParameterizedTest.kt | 4 +- .../DemoMobileConnectionsRepositoryTest.kt | 3 + .../FullMobileConnectionRepositoryTest.kt | 82 ++++++++++++++----- .../prod/MobileConnectionRepositoryTest.kt | 62 ++++++++++++-- .../MobileConnectionTelephonySmokeTests.kt | 10 ++- .../prod/MobileConnectionsRepositoryTest.kt | 15 +++- .../interactor/FakeMobileIconInteractor.kt | 4 + .../interactor/FakeMobileIconsInteractor.kt | 2 + .../interactor/MobileIconInteractorTest.kt | 68 +++++++++++++-- .../interactor/MobileIconsInteractorTest.kt | 61 +++++++++++++- .../ui/viewmodel/MobileIconsViewModelTest.kt | 45 ++++++++-- 25 files changed, 472 insertions(+), 54 deletions(-) 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 78231e28803eb..99ed2d99c749b 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 @@ sealed interface NetworkNameModel : Diffable { } } + /** 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 16c4027ef6459..27f6df4c26e15 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 @@ data class SubscriptionModel( /** 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 c1af6df12bd12..a89b1b2db6b31 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,9 +115,17 @@ interface MobileConnectionRepository { */ val cdmaRoaming: StateFlow - /** 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 + /** + * 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 + /** * True if this type of connection is allowed while airplane mode is on, and false otherwise. */ 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 17d20c2978616..c576b822da158 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 @@ class DemoMobileConnectionRepository( 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 @@ class DemoMobileConnectionRepository( // 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 @@ class DemoMobileConnectionRepository( // 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 @@ class DemoMobileConnectionRepository( } 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 0e4ceebcc8548..ee13d93e735d2 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 @@ constructor( 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 @@ constructor( 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 65f4866838371..28be3be289284 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 @@ class CarrierMergedConnectionRepository( NetworkNameModel.SimDerived(telephonyManager.simOperatorName), ) + override val carrierName: StateFlow = networkName + override val numberOfLevels: StateFlow = 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 8ba7d2197c141..ee11c06ef3f5a 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.TableLogBuffer 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 @@ class FullMobileConnectionRepository( override val subId: Int, startingIsCarrierMerged: Boolean, override val tableLogBuffer: TableLogBuffer, + subscriptionModel: StateFlow, private val defaultNetworkName: NetworkNameModel, private val networkNameSeparator: String, @Application scope: CoroutineScope, @@ -80,6 +82,7 @@ class FullMobileConnectionRepository( mobileRepoFactory.build( subId, tableLogBuffer, + subscriptionModel, defaultNetworkName, networkNameSeparator, ) @@ -287,6 +290,16 @@ class FullMobileConnectionRepository( ) .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 @@ class FullMobileConnectionRepository( fun build( subId: Int, startingIsCarrierMerged: Boolean, + subscriptionModel: StateFlow, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, ): FullMobileConnectionRepository { @@ -317,6 +331,7 @@ class FullMobileConnectionRepository( 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 aadc975a10dea..1f1ac92b39568 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.NetworkNameMode 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 @@ import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( override val subId: Int, + subscriptionModel: StateFlow, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, private val telephonyManager: TelephonyManager, @@ -281,6 +283,14 @@ class MobileConnectionRepositoryImpl( } .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 @@ class MobileConnectionRepositoryImpl( fun build( subId: Int, mobileLogger: TableLogBuffer, + subscriptionModel: StateFlow, 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 54948a4a41c83..67b04db644639 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 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache + private fun subscriptionModelForSubId(subId: Int): StateFlow { + 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 @@ constructor( 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 1a138272d67c3..4cfde5bd5622e 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 @@ interface MobileIconInteractor { */ val networkName: StateFlow + /** + * 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 + + /** True if there is only one active subscription. */ + val isSingleCarrier: StateFlow + /** True if this line of service is emergency-only */ val isEmergencyOnly: StateFlow @@ -126,6 +142,7 @@ class MobileIconInteractorImpl( defaultSubscriptionHasDataEnabled: StateFlow, override val alwaysShowDataRatIcon: StateFlow, override val alwaysUseCdmaLevel: StateFlow, + override val isSingleCarrier: StateFlow, override val mobileIsDefault: StateFlow, defaultMobileIconMapping: StateFlow>, defaultMobileIconGroup: StateFlow, @@ -171,6 +188,22 @@ class MobileIconInteractorImpl( 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 = 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 e90f40c74cc5a..d08808b65399d 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 @@ interface MobileIconsInteractor { /** True if the CDMA level should be preferred over the primary level. */ val alwaysUseCdmaLevel: StateFlow + /** True if there is only one active subscription. */ + val isSingleCarrier: StateFlow + /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: StateFlow> @@ -252,6 +255,17 @@ constructor( .mapLatest { it.alwaysShowCdmaRssi } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isSingleCarrier: StateFlow = + 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 = mobileConnectionsRepo.defaultMobileIconGroup.stateIn( @@ -298,6 +312,7 @@ constructor( 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 50ee6a31d389e..ff2875355a6ac 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 @@ class FakeMobileConnectionRepository( override val cdmaRoaming = MutableStateFlow(false) - override val networkName = - MutableStateFlow(NetworkNameModel.Default("default")) + override val networkName: MutableStateFlow = + MutableStateFlow(NetworkNameModel.Default(DEFAULT_NETWORK_NAME)) + + override val carrierName: MutableStateFlow = + 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 3591c17403295..99e4030e11921 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 @@ class FakeMobileConnectionsRepository( 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 5a887ebcee800..d005972043d78 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 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { 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().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } - private val MODEL_1 = SubscriptionModel(subscriptionId = SUB_1_ID) + mock().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().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } - private val MODEL_2 = SubscriptionModel(subscriptionId = SUB_2_ID) + mock().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 7573b28c8a7f9..57f97ec66a004 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.ExperimentalCoroutinesApi 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 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC 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 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC 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 efaf15235b469..2712b70a97455 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { 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 3dd2eaff7bce5..9c0cb17700a61 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.coroutines.collectLastValue 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 com.google.common.truth.Truth.assertThat 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 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { private val mobileFactory = mock() private val carrierMergedFactory = mock() + 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 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { .build( SUB_ID, tableLogBuffer, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) } @@ -138,7 +164,11 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { 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 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -356,7 +387,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -388,7 +420,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = false, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -397,7 +430,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { factory.build( SUB_ID, startingIsCarrierMerged = true, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, ) @@ -623,7 +657,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { SUB_ID, startingIsCarrierMerged, tableLogBuffer, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, testScope.backgroundScope, mobileFactory, @@ -639,8 +674,9 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { val realRepo = MobileConnectionRepositoryImpl( SUB_ID, - defaultNetworkName = NetworkNameModel.Default("default"), - networkNameSeparator = SEP, + subscriptionModel, + DEFAULT_NAME_MODEL, + SEP, telephonyManager, systemUiCarrierConfig = mock(), fakeBroadcastDispatcher, @@ -654,7 +690,8 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { mobileFactory.build( eq(SUB_ID), any(), - eq(DEFAULT_NAME), + any(), + eq(DEFAULT_NAME_MODEL), eq(SEP), ) ) @@ -677,7 +714,13 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { 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 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() { 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 1ff737bfc137b..e50e5e31a786a 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.ResolvedNetwork 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.mock 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 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) + private val subscriptionModel: MutableStateFlow = + MutableStateFlow( + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = DEFAULT_NAME, + ) + ) + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -119,7 +129,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { underTest = MobileConnectionRepositoryImpl( SUB_1_ID, - DEFAULT_NAME, + subscriptionModel, + DEFAULT_NAME_MODEL, SEP, telephonyManager, systemUiCarrierConfig, @@ -179,6 +190,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { // gsmLevel updates, no change to cdmaLevel strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) assertThat(latest).isEqualTo(2) @@ -637,13 +649,52 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { job.cancel() } + @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 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intentWithoutInfo) - assertThat(latest).isEqualTo(DEFAULT_NAME) + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) job.cancel() } @@ -852,8 +903,9 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { 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 4f15aed00230b..ea60aa74f12bf 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.MobileInputLogger 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.mock 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 @@ class MobileConnectionTelephonySmokeTests : SysuiTestCase() { @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: MobileInputLogger @Mock private lateinit var tableLogger: TableLogBuffer + @Mock private lateinit var subscriptionModel: StateFlow private val mobileMappings = FakeMobileMappingsProxy() private val systemUiCarrierConfig = @@ -113,11 +116,16 @@ class MobileConnectionTelephonySmokeTests : SysuiTestCase() { 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 c8b6f13d69020..fd05cc4956921 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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { 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().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().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 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // 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().also { whenever(it.subscriptionId).thenReturn(SUB_CM_ID) } - private val MODEL_CM = SubscriptionModel(subscriptionId = SUB_CM_ID) + mock().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().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 8d1da69d68779..a3df785c5dae8 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 @@ class FakeMobileIconInteractor( override val mobileIsDefault = MutableStateFlow(true) + override val isSingleCarrier = MutableStateFlow(true) + override val networkTypeIconGroup = MutableStateFlow( NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) @@ -51,6 +53,8 @@ class FakeMobileIconInteractor( 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 b2bbcfd3d6efc..82b7ec41d1489 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 @@ class FakeMobileIconsInteractor( 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 58d3804b71551..e3c59adef529b 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.NetworkNameMode 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.mock 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 @@ class MobileIconInteractorTest : SysuiTestCase() { 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 @@ class MobileIconInteractorTest : SysuiTestCase() { } @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 @@ class MobileIconInteractorTest : SysuiTestCase() { 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 @@ class MobileIconInteractorTest : SysuiTestCase() { // 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) @@ -459,6 +499,21 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @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 { @@ -494,6 +549,7 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.alwaysShowDataRatIcon, mobileIconsInteractor.alwaysUseCdmaLevel, + mobileIconsInteractor.isSingleCarrier, mobileIconsInteractor.mobileIsDefault, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, @@ -510,7 +566,9 @@ class MobileIconInteractorTest : SysuiTestCase() { 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 1fb76b048d474..3e6f90931b87a 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 @@ -526,6 +526,57 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @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 { @@ -745,6 +796,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = subscriptionIds.first, isOpportunistic = opportunistic.first, groupUuid = groupUuid, + carrierName = "Carrier ${subscriptionIds.first}" ) val sub2 = @@ -752,6 +804,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = subscriptionIds.second, isOpportunistic = opportunistic.second, groupUuid = groupUuid, + carrierName = "Carrier ${opportunistic.second}" ) return Pair(sub1, sub2) @@ -760,11 +813,13 @@ class MobileIconsInteractorTest : SysuiTestCase() { 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 @@ class MobileIconsInteractorTest : SysuiTestCase() { 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 @@ class MobileIconsInteractorTest : SysuiTestCase() { 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 f0458fa83d387..e42515e5871de 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 @@ class MobileIconsViewModelTest : SysuiTestCase() { 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 @@ class MobileIconsViewModelTest : SysuiTestCase() { } 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", + ) } } From 062fbc4578dacfbe8a04d5299bc8bb9f7f1437fa Mon Sep 17 00:00:00 2001 From: Shawn Lee Date: Mon, 10 Jul 2023 15:09:23 -0700 Subject: [PATCH 2/2] [2/2] Migrating ShadeCarrierGroup mobile icons to modern status bar data pipeline Hooks up the ShadeCarrierGroup to depend on only the new data pipeline. The UI is left mostly as-is for now, with the change being the ShadeCarrier no longer having its own version of the mobile icon layout, and the text view for the carrier name has been moved inside the ModernStatusBarView. Bug: 288631206 Test: Manually verified no visual change with flag enabled, with both 1 and 2 sims Test: Added/updated unit tests Change-Id: Iaf91e5606640c5fad6e51ce3b6607e65f14d3bcb --- .../res-keyguard/layout/shade_carrier_new.xml | 42 +++++ .../src/com/android/systemui/flags/Flags.kt | 4 + .../systemui/shade/carrier/ShadeCarrier.java | 19 ++ .../carrier/ShadeCarrierGroupController.java | 149 +++++++++++++-- .../statusbar/phone/StatusBarLocation.kt | 2 + .../pipeline/StatusBarPipelineFlags.kt | 11 ++ .../pipeline/mobile/ui/MobileUiAdapter.kt | 9 + .../pipeline/mobile/ui/MobileViewLogger.kt | 6 +- .../mobile/ui/binder/MobileIconBinder.kt | 5 +- .../mobile/ui/binder/ShadeCarrierBinder.kt | 42 +++++ .../view/ModernShadeCarrierGroupMobileView.kt | 77 ++++++++ .../ui/view/ModernStatusBarMobileView.kt | 4 +- .../viewmodel/LocationBasedMobileViewModel.kt | 50 ++++- .../ui/viewmodel/MobileIconsViewModel.kt | 23 ++- .../viewmodel/LocationBasedWifiViewModel.kt | 11 ++ .../ShadeCarrierGroupControllerTest.java | 177 +++++++++++++++++- .../ui/viewmodel/MobileIconsViewModelTest.kt | 27 +++ 17 files changed, 623 insertions(+), 35 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt diff --git a/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml new file mode 100644 index 0000000000000..952f056b30239 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index b92bf6b616696..aec7974989160 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -398,6 +398,10 @@ object Flags { // TODO(b/294588085): Tracking Bug val WIFI_SECONDARY_NETWORKS = releasedFlag("wifi_secondary_networks") + // TODO(b/290676905): Tracking Bug + val NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS = + unreleasedFlag("new_shade_carrier_group_mobile_icons") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag("ongoing_call_status_bar_chip") diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java index 8586828af0cdd..8612cdf12c6e4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java @@ -34,6 +34,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; import com.android.systemui.util.LargeScreenUtils; import java.util.Objects; @@ -44,6 +45,7 @@ public class ShadeCarrier extends LinearLayout { private TextView mCarrierText; private ImageView mMobileSignal; private ImageView mMobileRoaming; + private ModernShadeCarrierGroupMobileView mModernMobileView; private View mSpacer; @Nullable private CellSignalState mLastSignalState; @@ -77,6 +79,23 @@ public class ShadeCarrier extends LinearLayout { updateResources(); } + /** Removes a ModernStatusBarMobileView from the ViewGroup. */ + public void removeModernMobileView() { + if (mModernMobileView != null) { + removeView(mModernMobileView); + mModernMobileView = null; + } + } + + /** Adds a ModernStatusBarMobileView to the ViewGroup. */ + public void addModernMobileView(ModernShadeCarrierGroupMobileView mobileView) { + mModernMobileView = mobileView; + mMobileGroup.setVisibility(View.GONE); + mSpacer.setVisibility(View.GONE); + mCarrierText.setVisibility(View.GONE); + addView(mobileView); + } + /** * Update the state of this view * @param state the current state of the signal for this view diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java index ad49b267c6ac9..98d8a53b83c8a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -16,6 +16,7 @@ package com.android.systemui.shade.carrier; +import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; import android.annotation.MainThread; @@ -46,8 +47,17 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.phone.StatusBarLocation; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; +import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -62,12 +72,16 @@ public class ShadeCarrierGroupController { private final ActivityStarter mActivityStarter; private final Handler mBgHandler; + private final Context mContext; private final NetworkController mNetworkController; private final CarrierTextManager mCarrierTextManager; private final TextView mNoSimTextView; // Non final for testing private H mMainHandler; private final Callback mCallback; + private final MobileIconsViewModel mMobileIconsViewModel; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; private boolean mListening; private final CellSignalState[] mInfos = new CellSignalState[SIM_SLOTS]; @@ -91,7 +105,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex); return; } - if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slotIndex == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId); return; } @@ -129,15 +143,25 @@ public class ShadeCarrierGroupController { } } - private ShadeCarrierGroupController(ShadeCarrierGroup view, ActivityStarter activityStarter, - @Background Handler bgHandler, @Main Looper mainLooper, + private ShadeCarrierGroupController( + ShadeCarrierGroup view, + ActivityStarter activityStarter, + @Background Handler bgHandler, + @Main Looper mainLooper, NetworkController networkController, - CarrierTextManager.Builder carrierTextManagerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { - + CarrierTextManager.Builder carrierTextManagerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { + mContext = context; mActivityStarter = activityStarter; mBgHandler = bgHandler; mNetworkController = networkController; + mStatusBarPipelineFlags = statusBarPipelineFlags; mCarrierTextManager = carrierTextManagerBuilder .setShowAirplaneMode(false) .setShowMissingSim(false) @@ -162,6 +186,14 @@ public class ShadeCarrierGroupController { mCarrierGroups[1] = view.getCarrier2View(); mCarrierGroups[2] = view.getCarrier3View(); + mMobileContextProvider = mobileContextProvider; + mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); + + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + mobileUiAdapter.setShadeCarrierGroupController(this); + MobileIconsBinder.bind(view, mMobileIconsViewModel); + } + mCarrierDividers[0] = view.getCarrierDivider1(); mCarrierDividers[1] = view.getCarrierDivider2(); @@ -193,6 +225,50 @@ public class ShadeCarrierGroupController { }); } + /** Updates the number of visible mobile icons using the new pipeline. */ + public void updateModernMobileIcons(List subIds) { + if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring new pipeline callback because new mobile icon is disabled"); + return; + } + + for (ShadeCarrier carrier : mCarrierGroups) { + carrier.removeModernMobileView(); + } + + List iconDataList = processSubIdList(subIds); + + for (IconData iconData : iconDataList) { + ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex]; + + Context mobileContext = + mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext); + ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView + .constructAndBind( + mobileContext, + mMobileIconsViewModel.getLogger(), + "mobile_carrier_shade_group", + (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel + .viewModelForSub(iconData.subId, + StatusBarLocation.SHADE_CARRIER_GROUP) + ); + carrier.addModernMobileView(modernMobileView); + } + } + + @VisibleForTesting + List processSubIdList(List subIds) { + return subIds + .stream() + .limit(SIM_SLOTS) + .map(subId -> new IconData(subId, getSlotIndex(subId))) + .filter(iconData -> + iconData.slotIndex < SIM_SLOTS + && iconData.slotIndex != INVALID_SIM_SLOT_INDEX + ) + .toList(); + } + @VisibleForTesting protected int getSlotIndex(int subscriptionId) { return mSlotIndexResolver.getSlotIndex(subscriptionId); @@ -269,8 +345,12 @@ public class ShadeCarrierGroupController { } } - for (int i = 0; i < SIM_SLOTS; i++) { - mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled"); + } else { + for (int i = 0; i < SIM_SLOTS; i++) { + mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + } } mCarrierDividers[0].setVisibility( @@ -306,7 +386,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "updateInfoCarrier - slot: " + slot); continue; } - if (slot == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slot == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + info.subscriptionIds[i]); @@ -385,12 +465,24 @@ public class ShadeCarrierGroupController { private final Context mContext; private final CarrierConfigTracker mCarrierConfigTracker; private final SlotIndexResolver mSlotIndexResolver; + private final MobileUiAdapter mMobileUiAdapter; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; @Inject - public Builder(ActivityStarter activityStarter, @Background Handler handler, - @Main Looper looper, NetworkController networkController, - CarrierTextManager.Builder carrierTextControllerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { + public Builder( + ActivityStarter activityStarter, + @Background Handler handler, + @Main Looper looper, + NetworkController networkController, + CarrierTextManager.Builder carrierTextControllerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { mActivityStarter = activityStarter; mHandler = handler; mLooper = looper; @@ -399,6 +491,9 @@ public class ShadeCarrierGroupController { mContext = context; mCarrierConfigTracker = carrierConfigTracker; mSlotIndexResolver = slotIndexResolver; + mMobileUiAdapter = mobileUiAdapter; + mMobileContextProvider = mobileContextProvider; + mStatusBarPipelineFlags = statusBarPipelineFlags; } public Builder setShadeCarrierGroup(ShadeCarrierGroup view) { @@ -407,9 +502,20 @@ public class ShadeCarrierGroupController { } public ShadeCarrierGroupController build() { - return new ShadeCarrierGroupController(mView, mActivityStarter, mHandler, mLooper, - mNetworkController, mCarrierTextControllerBuilder, mContext, - mCarrierConfigTracker, mSlotIndexResolver); + return new ShadeCarrierGroupController( + mView, + mActivityStarter, + mHandler, + mLooper, + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ); } } @@ -448,4 +554,15 @@ public class ShadeCarrierGroupController { return SubscriptionManager.getSlotIndex(subscriptionId); } } + + @VisibleForTesting + static class IconData { + public final int subId; + public final int slotIndex; + + IconData(int subId, int slotIndex) { + this.subId = subId; + this.slotIndex = slotIndex; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt index 5ace22695ec37..32e5c355889a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt @@ -24,4 +24,6 @@ enum class StatusBarLocation { KEYGUARD, /** Quick settings (inside the shade). */ QS, + /** ShadeCarrierGroup (above QS status bar in expanded mode). */ + SHADE_CARRIER_GROUP, } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 6e51ed0eba378..c695773086082 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.pipeline import android.content.Context import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.shade.carrier.ShadeCarrierGroup import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @@ -26,10 +29,18 @@ class StatusBarPipelineFlags @Inject constructor( context: Context, + private val featureFlags: FeatureFlags, ) { private val mobileSlot = context.getString(com.android.internal.R.string.status_bar_mobile) private val wifiSlot = context.getString(com.android.internal.R.string.status_bar_wifi) + /** + * True if we should display the mobile icons in the [ShadeCarrierGroup] using the new status + * bar Data pipeline. + */ + fun useNewShadeCarrierGroupMobileIcons(): Boolean = + featureFlags.isEnabled(Flags.NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS) + /** * For convenience in the StatusBarIconController, we want to gate some actions based on slot * name and the flag together. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index d7fcf4876c285..02e50a007f50d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.carrier.ShadeCarrierGroupController import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -49,6 +50,8 @@ constructor( private var isCollecting: Boolean = false private var lastValue: List? = null + private var shadeCarrierGroupController: ShadeCarrierGroupController? = null + override fun start() { // Start notifying the icon controller of subscriptions scope.launch { @@ -57,10 +60,16 @@ constructor( logger.logUiAdapterSubIdsSentToIconController(it) lastValue = it iconController.setNewMobileIconSubIds(it) + shadeCarrierGroupController?.updateModernMobileIcons(it) } } } + /** Set the [ShadeCarrierGroupController] to notify of subscription updates */ + fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) { + shadeCarrierGroupController = controller + } + override fun dump(pw: PrintWriter, args: Array) { pw.println("isCollecting=$isCollecting") pw.println("Last values sent to icon controller: $lastValue") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt index cea6654a48de0..2af6795b39c41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -57,7 +57,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -71,7 +71,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -85,7 +85,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 55bc8d58be23e..4b2fb4373957a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -50,6 +50,7 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, + @StatusBarIconView.VisibleState initialVisibilityState: Int = STATE_HIDDEN, logger: MobileViewLogger, ): ModernStatusBarViewBinding { val mobileGroupView = view.requireViewById(R.id.mobile_group) @@ -68,12 +69,12 @@ object MobileIconBinder { // TODO(b/238425913): We should log this visibility state. @StatusBarIconView.VisibleState - val visibilityState: MutableStateFlow = MutableStateFlow(STATE_HIDDEN) + val visibilityState: MutableStateFlow = MutableStateFlow(initialVisibilityState) val iconTint: MutableStateFlow = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow = MutableStateFlow(viewModel.defaultColor) - var isCollecting: Boolean = false + var isCollecting = false view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt new file mode 100644 index 0000000000000..081e1015e26e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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.ui.binder + +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView +import kotlinx.coroutines.launch + +object ShadeCarrierBinder { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @JvmStatic + fun bind( + carrierTextView: AutoMarqueeTextView, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ) { + carrierTextView.isVisible = true + + carrierTextView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { viewModel.carrierName.collect { carrierTextView.text = it } } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt new file mode 100644 index 0000000000000..f407127f3161e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 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.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView + +/** + * ViewGroup containing a mobile carrier name and icon in the Shade Header. Can be multiple + * instances as children under [ShadeCarrierGroup] + */ +class ModernShadeCarrierGroupMobileView( + context: Context, + attrs: AttributeSet?, +) : LinearLayout(context, attrs) { + + var subId: Int = -1 + + override fun toString(): String { + return "ModernShadeCarrierGroupMobileView(" + + "subId=$subId, " + + "viewString=${super.toString()}" + } + + companion object { + + /** + * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel], + * and returns it. + */ + @JvmStatic + fun constructAndBind( + context: Context, + logger: MobileViewLogger, + slot: String, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ): ModernShadeCarrierGroupMobileView { + return (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null) + as ModernShadeCarrierGroupMobileView) + .also { + it.subId = viewModel.subscriptionId + + val iconView = it.requireViewById(R.id.mobile_combo) + iconView.initView(slot) { + MobileIconBinder.bind(iconView, viewModel, STATE_ICON, logger) + } + logger.logNewViewBinding(it, viewModel) + + val textView = it.requireViewById(R.id.mobile_carrier_text) + ShadeCarrierBinder.bind(textView, viewModel) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index 4144293d5ccd4..68d02de51dc9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -60,7 +60,9 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { it.subId = viewModel.subscriptionId - it.initView(slot) { MobileIconBinder.bind(it, viewModel, logger) } + it.initView(slot) { + MobileIconBinder.bind(view = it, viewModel = viewModel, logger = logger) + } logger.logNewViewBinding(it, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index a51982c41255b..e7c311d7ec74d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -18,7 +18,13 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -26,12 +32,12 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger * * @param commonImpl for convenience, this class wraps a base interface that can provides all of the * common implementations between locations. See [MobileIconViewModel] - * @property locationName the name of the location of this VM, used for logging. + * @property location the [StatusBarLocation] of this VM. * @property verboseLogger an optional logger to log extremely verbose view updates. */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, - val locationName: String, + val location: StatusBarLocation, val verboseLogger: VerboseMobileViewLogger?, ) : MobileIconViewModelCommon by commonImpl { val defaultColor: Int = Color.WHITE @@ -39,10 +45,12 @@ abstract class LocationBasedMobileViewModel( companion object { fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, verboseMobileViewLogger: VerboseMobileViewLogger, - loc: StatusBarLocation, + location: StatusBarLocation, + scope: CoroutineScope, ): LocationBasedMobileViewModel = - when (loc) { + when (location) { StatusBarLocation.HOME -> HomeMobileIconViewModel( commonImpl, @@ -50,6 +58,12 @@ abstract class LocationBasedMobileViewModel( ) StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + ShadeCarrierGroupMobileIconViewModel( + commonImpl, + interactor, + scope, + ) } } } @@ -61,7 +75,7 @@ class HomeMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Home", + location = StatusBarLocation.HOME, verboseMobileViewLogger, ) @@ -71,18 +85,40 @@ class QsMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "QS", + location = StatusBarLocation.QS, // Only do verbose logging for the Home location. verboseLogger = null, ) +class ShadeCarrierGroupMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, + scope: CoroutineScope, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel( + commonImpl, + location = StatusBarLocation.SHADE_CARRIER_GROUP, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) { + private val isSingleCarrier = interactor.isSingleCarrier + val carrierName = interactor.carrierName + + override val isVisible: StateFlow = + combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier -> + if (isSingleCarrier) false else isVisible + } + .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value) +} + class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, ) : MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Keyguard", + location = StatusBarLocation.KEYGUARD, // Only do verbose logging for the Home location. verboseLogger = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 5cf887e4d41ac..216afb987f919 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger @@ -58,6 +59,8 @@ constructor( private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { @VisibleForTesting val mobileIconSubIdCache = mutableMapOf() + @VisibleForTesting + val mobileIconInteractorSubIdCache = mutableMapOf() val subscriptionIdsFlow: StateFlow> = interactor.filteredSubscriptions @@ -91,15 +94,17 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) init { - scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } } + scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } } fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { val common = commonViewModelForSub(subId) return LocationBasedMobileViewModel.viewModelForLocation( common, + mobileIconInteractorForSub(subId), verboseLogger, location, + scope, ) } @@ -107,7 +112,7 @@ constructor( return mobileIconSubIdCache[subId] ?: MobileIconViewModel( subId, - interactor.createMobileConnectionInteractorForSubId(subId), + mobileIconInteractorForSub(subId), airplaneModeInteractor, constants, scope, @@ -115,8 +120,20 @@ constructor( .also { mobileIconSubIdCache[subId] = it } } - private fun removeInvalidModelsFromCache(subIds: List) { + @VisibleForTesting + fun mobileIconInteractorForSub(subId: Int): MobileIconInteractor { + return mobileIconInteractorSubIdCache[subId] + ?: interactor.createMobileConnectionInteractorForSubId(subId).also { + mobileIconInteractorSubIdCache[subId] = it + } + } + + private fun invalidateCaches(subIds: List) { val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } + + mobileIconInteractorSubIdCache.keys + .filter { !subIds.contains(it) } + .forEach { subId -> mobileIconInteractorSubIdCache.remove(subId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index cd5b92cf24c74..00bd616e04eac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import java.lang.IllegalArgumentException /** * A view model for a wifi icon in a specific location. This allows us to control parameters that @@ -43,6 +44,8 @@ abstract class LocationBasedWifiViewModel( StatusBarLocation.HOME -> HomeWifiViewModel(commonImpl) StatusBarLocation.KEYGUARD -> KeyguardWifiViewModel(commonImpl) StatusBarLocation.QS -> QsWifiViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + throw IllegalArgumentException("invalid location for WifiViewModel: $location") } } } @@ -64,3 +67,11 @@ class KeyguardWifiViewModel( class QsWifiViewModel( commonImpl: WifiViewModelCommon, ) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) + +/** + * A view model for the wifi icon in the shade carrier group (visible when quick settings is fully + * expanded, and in large screen shade). Currently unused. + */ +class ShadeCarrierGroupWifiViewModel( + commonImpl: WifiViewModelCommon, +) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index 31bfa3fdf8cba..5fa6b3a15d351 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -50,6 +51,12 @@ import com.android.systemui.statusbar.connectivity.IconState; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; import com.android.systemui.utils.leaks.LeakCheckedTest; import com.android.systemui.utils.os.FakeHandler; @@ -61,6 +68,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest @@ -95,6 +106,18 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { private TestableLooper mTestableLooper; @Mock private ShadeCarrierGroupController.OnSingleCarrierChangedListener mOnSingleCarrierChangedListener; + @Mock + private MobileUiAdapter mMobileUiAdapter; + @Mock + private MobileIconsViewModel mMobileIconsViewModel; + @Mock + private ShadeCarrierGroupMobileIconViewModel mShadeCarrierGroupMobileIconViewModel; + @Mock + private MobileViewLogger mMobileViewLogger; + @Mock + private MobileContextProvider mMobileContextProvider; + @Mock + private StatusBarPipelineFlags mStatusBarPipelineFlags; private FakeSlotIndexResolver mSlotIndexResolver; private ClickListenerTextView mNoCarrierTextView; @@ -133,16 +156,35 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { mSlotIndexResolver = new FakeSlotIndexResolver(); + when(mMobileUiAdapter.getMobileIconsViewModel()).thenReturn(mMobileIconsViewModel); + mShadeCarrierGroupController = new ShadeCarrierGroupController.Builder( - mActivityStarter, handler, TestableLooper.get(this).getLooper(), - mNetworkController, mCarrierTextControllerBuilder, mContext, mCarrierConfigTracker, - mSlotIndexResolver) + mActivityStarter, + handler, + TestableLooper.get(this).getLooper(), + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ) .setShadeCarrierGroup(mShadeCarrierGroup) .build(); mShadeCarrierGroupController.setListening(true); } + private void setupWithNewPipeline() { + when(mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()).thenReturn(true); + when(mMobileContextProvider.getMobileContextForSub(anyInt(), any())).thenReturn(mContext); + when(mMobileIconsViewModel.getLogger()).thenReturn(mMobileViewLogger); + when(mMobileIconsViewModel.viewModelForSub(anyInt(), any())) + .thenReturn(mShadeCarrierGroupMobileIconViewModel); + } + @Test public void testInitiallyMultiCarrier() { assertFalse(mShadeCarrierGroupController.isSingleCarrier()); @@ -406,6 +448,129 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { verify(mOnSingleCarrierChangedListener, never()).onSingleCarrierChanged(anyBoolean()); } + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_addSubscription() { + setupWithNewPipeline(); + + mShadeCarrier1.setVisibility(View.GONE); + mShadeCarrier2.setVisibility(View.GONE); + mShadeCarrier3.setVisibility(View.GONE); + + List subIds = new ArrayList<>(); + subIds.add(0); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscription() { + setupWithNewPipeline(); + + List subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + verify(mShadeCarrier2, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscriptionOutOfOrder() { + setupWithNewPipeline(); + + List subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + subIds.add(2); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).removeModernMobileView(); + verify(mShadeCarrier2).removeModernMobileView(); + verify(mShadeCarrier3).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_moreSubsThanSimSlots_listLimitedToMax() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 1, 2, 2); + + assertThat(mShadeCarrierGroupController.processSubIdList(subIds).size()).isEqualTo(3); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_invalidSimSlotIndexFilteredOut() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 1, -1); + + List processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(2); + assertThat(processedSubs.get(0).subId).isNotEqualTo(-1); + assertThat(processedSubs.get(1).subId).isNotEqualTo(-1); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_indexGreaterThanSimSlotsFilteredOut() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 4); + + List processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(1); + assertThat(processedSubs.get(0).subId).isNotEqualTo(4); + } + + @Test public void testOnlyInternalViewsHaveClickableListener() { ArgumentCaptor captor = @@ -447,6 +612,12 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { .isEqualTo(Settings.ACTION_WIRELESS_SETTINGS); } + private void resetShadeCarriers() { + reset(mShadeCarrier1); + reset(mShadeCarrier2); + reset(mShadeCarrier3); + } + private class FakeSlotIndexResolver implements ShadeCarrierGroupController.SlotIndexResolver { public boolean overrideInvalid; 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 e42515e5871de..065dfbabf051c 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 @@ -153,6 +153,33 @@ class MobileIconsViewModelTest : SysuiTestCase() { assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl) } + @Test + fun caching_mobileIconInteractorIsReusedForSameSubId() = + testScope.runTest { + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(1) + + assertThat(interactor1).isSameInstanceAs(interactor2) + } + + @Test + fun caching_invalidInteractorssAreRemovedFromCacheWhenSubDisappears() = + testScope.runTest { + // Retrieve interactors to trigger caching + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(2) + + // Both impls are cached + assertThat(underTest.mobileIconInteractorSubIdCache) + .containsExactly(1, interactor1, 2, interactor2) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.mobileIconInteractorSubIdCache).containsExactly(2, interactor2) + } + @Test fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = testScope.runTest {