diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt index a6b04e4557b0b..6c37f94007cba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.annotation.IntRange -import android.telephony.Annotation.DataActivityType import android.telephony.CellSignalStrength import android.telephony.TelephonyCallback.CarrierNetworkListener import android.telephony.TelephonyCallback.DataActivityListener @@ -28,6 +27,7 @@ import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel /** * Data class containing all of the relevant information for a particular line of service, known as @@ -39,29 +39,42 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionS * threading complex system objects through the pipeline. */ data class MobileConnectionModel( - /** From [ServiceStateListener.onServiceStateChanged] */ + /** Fields below are from [ServiceStateListener.onServiceStateChanged] */ val isEmergencyOnly: Boolean = false, val isRoaming: Boolean = false, + /** + * See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the + * current registered operator name in short alphanumeric format. In some cases this name might + * be preferred over other methods of calculating the network name + */ + val operatorAlphaShort: String? = null, - /** From [SignalStrengthsListener.onSignalStrengthsChanged] */ + /** Fields below from [SignalStrengthsListener.onSignalStrengthsChanged] */ val isGsm: Boolean = false, @IntRange(from = 0, to = 4) val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, @IntRange(from = 0, to = 4) val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, - /** Mapped from [DataConnectionStateListener.onDataConnectionStateChanged] */ + /** Fields below from [DataConnectionStateListener.onDataConnectionStateChanged] */ val dataConnectionState: DataConnectionState = Disconnected, - /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */ - @DataActivityType val dataActivityDirection: Int? = null, + /** + * Fields below from [DataActivityListener.onDataActivity]. See [TelephonyManager] for the + * values + */ + val dataActivityDirection: DataActivityModel = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ), - /** From [CarrierNetworkListener.onCarrierNetworkChange] */ + /** Fields below from [CarrierNetworkListener.onCarrierNetworkChange] */ val carrierNetworkChangeActive: Boolean = false, + /** Fields below from [DisplayInfoListener.onDisplayInfoChanged]. */ + /** - * From [DisplayInfoListener.onDisplayInfoChanged]. - * * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt new file mode 100644 index 0000000000000..a8cf35ad30298 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import android.content.Intent +import android.telephony.TelephonyManager.EXTRA_DATA_SPN +import android.telephony.TelephonyManager.EXTRA_PLMN +import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN +import android.telephony.TelephonyManager.EXTRA_SHOW_SPN + +/** + * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from + * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]. + */ +sealed interface NetworkNameModel { + val name: String + + /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */ + data class Default(override val name: String) : NetworkNameModel + + /** + * This name has been derived from telephony intents. see + * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED] + */ + data class Derived(override val name: String) : NetworkNameModel +} + +fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? { + val showSpn = getBooleanExtra(EXTRA_SHOW_SPN, false) + val spn = getStringExtra(EXTRA_DATA_SPN) + val showPlmn = getBooleanExtra(EXTRA_SHOW_PLMN, false) + val plmn = getStringExtra(EXTRA_PLMN) + + val str = StringBuilder() + val strData = StringBuilder() + if (showPlmn && plmn != null) { + str.append(plmn) + strData.append(plmn) + } + if (showSpn && spn != null) { + if (str.isNotEmpty()) { + str.append(separator) + } + str.append(spn) + } + + return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index fc59f6e1eccf0..2fd415e6777f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -21,6 +21,7 @@ import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -58,4 +59,7 @@ interface MobileConnectionRepository { * True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF] */ val cdmaRoaming: StateFlow + + /** The service provider name for this network connection, or the default name */ + val networkName: StateFlow } 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 98b47e4c80e52..d3ee85f19347c 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 @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo import android.content.Context import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID +import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import android.util.Log import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings @@ -26,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel @@ -34,6 +36,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -185,6 +188,7 @@ constructor( // This is always true here, because we split out disabled states at the data-source level connection.dataEnabled.value = true connection.isDefaultDataSubscription.value = state.dataType != null + connection.networkName.value = NetworkNameModel.Derived(state.name) connection.cdmaRoaming.value = state.roaming connection.connectionInfo.value = state.toMobileConnectionModel() @@ -236,7 +240,7 @@ constructor( primaryLevel = level ?: 0, dataConnectionState = DataConnectionState.Connected, // TODO(b/261029387): not yet supported - dataActivityDirection = activity, + dataActivityDirection = (activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel(), carrierNetworkChangeActive = carrierNetworkChange, resolvedNetworkType = dataType.toResolvedNetworkType() ) @@ -264,4 +268,6 @@ class DemoMobileConnectionRepository(override val subId: Int) : MobileConnection override val isDefaultDataSubscription = MutableStateFlow(true) override val cdmaRoaming = MutableStateFlow(false) + + override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network")) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt index 2cdbc191f7ad8..a1ae8ed7329a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt @@ -99,6 +99,7 @@ constructor( val activity = getString("activity")?.toActivity() val carrierNetworkChange = getString("carriernetworkchange") == "show" val roaming = getString("roam") == "show" + val name = getString("networkname") ?: "demo mode" return Mobile( level = level, @@ -109,6 +110,7 @@ constructor( activity = activity, carrierNetworkChange = carrierNetworkChange, roaming = roaming, + name = name, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt index b8543eca69a85..8b03f71a27293 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt @@ -35,6 +35,7 @@ sealed interface FakeNetworkEventModel { @DataActivityType val activity: Int?, val carrierNetworkChange: Boolean, val roaming: Boolean, + val name: String, ) : FakeNetworkEventModel data class MobileDisabled( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 295e0dc04c6de..7e9a9cea9b95b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -17,30 +17,37 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod import android.content.Context +import android.content.IntentFilter import android.database.ContentObserver import android.provider.Settings.Global import android.telephony.CellSignalStrength import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.TelephonyCallback import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE import android.telephony.TelephonyManager import android.telephony.TelephonyManager.ERI_OFF +import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType +import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -62,8 +69,11 @@ import kotlinx.coroutines.flow.stateIn class MobileConnectionRepositoryImpl( private val context: Context, override val subId: Int, + defaultNetworkName: NetworkNameModel, + networkNameSeparator: String, private val telephonyManager: TelephonyManager, private val globalSettings: GlobalSettings, + broadcastDispatcher: BroadcastDispatcher, defaultDataSubId: StateFlow, globalMobileDataSettingChangedEvent: Flow, mobileMappingsProxy: MobileMappingsProxy, @@ -100,6 +110,7 @@ class MobileConnectionRepositoryImpl( state.copy( isEmergencyOnly = serviceState.isEmergencyOnly, isRoaming = serviceState.roaming, + operatorAlphaShort = serviceState.operatorAlphaShort, ) trySend(state) } @@ -137,7 +148,10 @@ class MobileConnectionRepositoryImpl( } override fun onDataActivity(direction: Int) { - state = state.copy(dataActivityDirection = direction) + state = + state.copy( + dataActivityDirection = direction.toMobileDataActivityModel() + ) trySend(state) } @@ -218,6 +232,19 @@ class MobileConnectionRepositoryImpl( .mapLatest { telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber != ERI_OFF } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val networkName: StateFlow = + broadcastDispatcher + .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) { + intent, + _ -> + if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) { + defaultNetworkName + } else { + intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + override val dataEnabled: StateFlow = telephonyPollingEvent .mapLatest { dataConnectionAllowed() } @@ -233,6 +260,7 @@ class MobileConnectionRepositoryImpl( class Factory @Inject constructor( + private val broadcastDispatcher: BroadcastDispatcher, private val context: Context, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, @@ -243,14 +271,19 @@ class MobileConnectionRepositoryImpl( ) { fun build( subId: Int, + defaultNetworkName: NetworkNameModel, + networkNameSeparator: String, defaultDataSubId: StateFlow, globalMobileDataSettingChangedEvent: Flow, ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( context, subId, + defaultNetworkName, + networkNameSeparator, telephonyManager.createForSubscriptionId(subId), globalSettings, + broadcastDispatcher, defaultDataSubId, globalMobileDataSettingChangedEvent, mobileMappingsProxy, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 483df4759d28f..a9b3d18774fd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -38,12 +38,14 @@ import androidx.annotation.VisibleForTesting import com.android.internal.telephony.PhoneConstants import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository @@ -88,6 +90,14 @@ constructor( ) : MobileConnectionsRepository { private var subIdRepositoryCache: MutableMap = mutableMapOf() + private val defaultNetworkName = + NetworkNameModel.Default( + context.getString(com.android.internal.R.string.lockscreen_carrier_default) + ) + + private val networkNameSeparator: String = + context.getString(R.string.status_bar_network_name_separator) + /** * State flow that emits the set of mobile data subscriptions, each represented by its own * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each @@ -243,6 +253,8 @@ constructor( private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { return mobileConnectionRepositoryFactory.build( subId, + defaultNetworkName, + networkNameSeparator, defaultDataSubId, globalMobileDataSettingChangedEvent, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 15b70f9c1d32b..76e6a96a19d75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -20,10 +20,13 @@ import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.util.CarrierConfigTracker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -32,6 +35,9 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn interface MobileIconInteractor { + /** The current mobile data activity */ + val activity: Flow + /** Only true if mobile is the default transport but is not validated, otherwise false */ val isDefaultConnectionFailed: StateFlow @@ -51,6 +57,15 @@ interface MobileIconInteractor { /** Observable for RAT type (network type) indicator */ val networkTypeIconGroup: 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 derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED] + * 3. Or, in the case where the repository sends us the default network name, we check for an + * override in [connectionInfo.operatorAlphaShort], a value that is derived from [ServiceState] + */ + val networkName: StateFlow + /** True if this line of service is emergency-only */ val isEmergencyOnly: StateFlow @@ -82,10 +97,28 @@ class MobileIconInteractorImpl( ) : MobileIconInteractor { private val connectionInfo = connectionRepository.connectionInfo + override val activity = connectionInfo.mapLatest { it.dataActivityDirection } + override val isDataEnabled: StateFlow = connectionRepository.dataEnabled override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled + override val networkName = + combine(connectionInfo, connectionRepository.networkName) { connection, networkName -> + if ( + networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null + ) { + NetworkNameModel.Derived(connection.operatorAlphaShort) + } else { + networkName + } + } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + connectionRepository.networkName.value + ) + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: StateFlow = combine( 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 4455801fe8e10..545e624273f16 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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import android.content.res.ColorStateList +import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup @@ -40,6 +41,9 @@ object MobileIconBinder { view: ViewGroup, viewModel: MobileIconViewModel, ) { + val activityContainer = view.requireViewById(R.id.inout_container) + val activityIn = view.requireViewById(R.id.mobile_in) + val activityOut = view.requireViewById(R.id.mobile_out) val networkTypeView = view.requireViewById(R.id.mobile_type) val iconView = view.requireViewById(R.id.mobile_signal) val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } @@ -74,6 +78,15 @@ object MobileIconBinder { } } + // Set the activity indicators + launch { viewModel.activityInVisible.collect { activityIn.isVisible = it } } + + launch { viewModel.activityOutVisible.collect { activityOut.isVisible = it } } + + launch { + viewModel.activityContainerVisible.collect { activityContainer.isVisible = it } + } + // Set the tint launch { viewModel.tint.collect { tint -> @@ -81,6 +94,8 @@ object MobileIconBinder { iconView.imageTintList = tintList networkTypeView.imageTintList = tintList roamingView.imageTintList = tintList + activityIn.imageTintList = tintList + activityOut.imageTintList = tintList } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index f4d6111c775a7..961283f57def3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -22,13 +22,16 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon 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.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest /** @@ -48,6 +51,7 @@ constructor( val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, + constants: ConnectivityConstants, ) { /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: Flow = @@ -89,5 +93,17 @@ constructor( val roaming: Flow = iconInteractor.isRoaming + private val activity: Flow = + if (!constants.shouldShowActivityConfig) { + flowOf(null) + } else { + iconInteractor.activity + } + + val activityInVisible: Flow = activity.map { it?.hasActivityIn ?: false } + val activityOutVisible: Flow = activity.map { it?.hasActivityOut ?: false } + val activityContainerVisible: Flow = + activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } + val tint: Flow = flowOf(Color.CYAN) } 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 2349cb7c5d801..0b41d319f9dc2 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 @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import javax.inject.Inject import kotlinx.coroutines.InternalCoroutinesApi @@ -36,13 +37,15 @@ constructor( val subscriptionIdsFlow: StateFlow>, private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, + private val constants: ConnectivityConstants, ) { /** TODO: do we need to cache these? */ fun viewModelForSub(subId: Int): MobileIconViewModel = MobileIconViewModel( subId, interactor.createMobileConnectionInteractorForSubId(subId), - logger + logger, + constants, ) class Factory @@ -50,12 +53,14 @@ constructor( constructor( private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, + private val constants: ConnectivityConstants, ) { fun create(subscriptionIdsFlow: StateFlow>): MobileIconsViewModel { return MobileIconsViewModel( subscriptionIdsFlow, interactor, logger, + constants, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt index 6efb10f2a6f24..0c9b86cb2b118 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityConstants.kt @@ -16,8 +16,10 @@ package com.android.systemui.statusbar.pipeline.shared +import android.content.Context import android.telephony.TelephonyManager import com.android.systemui.Dumpable +import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG @@ -32,15 +34,25 @@ import javax.inject.Inject @SysUISingleton class ConnectivityConstants @Inject -constructor(dumpManager: DumpManager, telephonyManager: TelephonyManager) : Dumpable { +constructor( + context: Context, + dumpManager: DumpManager, + telephonyManager: TelephonyManager, +) : Dumpable { init { - dumpManager.registerDumpable("${SB_LOGGING_TAG}Constants", this) + dumpManager.registerNormalDumpable("${SB_LOGGING_TAG}Constants", this) } /** True if this device has the capability for data connections and false otherwise. */ val hasDataCapabilities = telephonyManager.isDataCapable + /** True if we should show the activityIn/activityOut icons and false otherwise */ + val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity) + override fun dump(pw: PrintWriter, args: Array) { - pw.apply { println("hasDataCapabilities=$hasDataCapabilities") } + pw.apply { + println("hasDataCapabilities=$hasDataCapabilities") + println("shouldShowActivityConfig=$shouldShowActivityConfig") + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt index 9b415674d5231..05d07144f8afb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/DataActivityModel.kt @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.pipeline.shared.data.model +import android.net.wifi.WifiManager +import android.telephony.Annotation +import android.telephony.TelephonyManager import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger @@ -44,3 +47,25 @@ data class DataActivityModel( const val ACTIVITY_PREFIX = "dataActivity" private const val COL_ACTIVITY_IN = "in" private const val COL_ACTIVITY_OUT = "out" + +fun @receiver:Annotation.DataActivityType Int.toMobileDataActivityModel(): DataActivityModel = + when (this) { + TelephonyManager.DATA_ACTIVITY_IN -> + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + TelephonyManager.DATA_ACTIVITY_OUT -> + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + TelephonyManager.DATA_ACTIVITY_INOUT -> + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false) + } + +fun Int.toWifiDataActivityModel(): DataActivityModel = + when (this) { + WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN -> + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT -> + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT -> + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + else -> DataActivityModel(hasActivityIn = false, hasActivityOut = false) + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 81441980abf7f..5ccd6f45770d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import java.util.concurrent.Executor import javax.inject.Inject @@ -237,7 +238,7 @@ class WifiRepositoryImpl @Inject constructor( conflatedCallbackFlow { val callback = TrafficStateCallback { state -> logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state)) - trySend(trafficStateToDataActivityModel(state)) + trySend(state.toWifiDataActivityModel()) } wifiManager.registerTrafficStateCallback(mainExecutor, callback) awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } @@ -266,15 +267,6 @@ class WifiRepositoryImpl @Inject constructor( // NetworkCallback inside [wifiNetwork] for our wifi network information. val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive - private fun trafficStateToDataActivityModel(state: Int): DataActivityModel { - return DataActivityModel( - hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN || - state == TrafficStateCallback.DATA_ACTIVITY_INOUT, - hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT || - state == TrafficStateCallback.DATA_ACTIVITY_INOUT, - ) - } - private fun networkCapabilitiesToWifiInfo( networkCapabilities: NetworkCapabilities ): WifiInfo? { 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 7b9929d963d3d..59eec5327c12b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository @@ -32,6 +33,9 @@ class FakeMobileConnectionRepository(override val subId: Int) : MobileConnection override val cdmaRoaming = MutableStateFlow(false) + override val networkName = + MutableStateFlow(NetworkNameModel.Default("default")) + fun setConnectionInfo(model: MobileConnectionModel) { _connectionInfo.value = model } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index b2423da00dab4..3d5316d1f19d7 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 @@ -18,13 +18,16 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo import android.telephony.Annotation import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -96,6 +99,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC activity = testCase.activity, carrierNetworkChange = testCase.carrierNetworkChange, roaming = testCase.roaming, + name = "demo name", ) fakeNetworkEventFlow.value = networkModel @@ -114,10 +118,12 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC assertThat(conn.subId).isEqualTo(model.subId) assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity) + assertThat(connectionInfo.dataActivityDirection) + .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) assertThat(connectionInfo.carrierNetworkChangeActive) .isEqualTo(model.carrierNetworkChange) assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name)) // TODO(b/261029387): check these once we start handling them assertThat(connectionInfo.isEmergencyOnly).isFalse() @@ -141,6 +147,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC @Annotation.DataActivityType val activity: Int, val carrierNetworkChange: Boolean, val roaming: Boolean, + val name: String, ) { override fun toString(): String { return "INPUT(level=$level, " + @@ -150,7 +157,8 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC "inflateStrength=$inflateStrength, " + "activity=$activity, " + "carrierNetworkChange=$carrierNetworkChange, " + - "roaming=$roaming)" + "roaming=$roaming, " + + "name=$name)" } // Convenience for iterating test data and creating new cases @@ -163,6 +171,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC @Annotation.DataActivityType activity: Int? = null, carrierNetworkChange: Boolean? = null, roaming: Boolean? = null, + name: String? = null, ): TestCase = TestCase( level = level ?: this.level, @@ -173,6 +182,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC activity = activity ?: this.activity, carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange, roaming = roaming ?: this.roaming, + name = name ?: this.name, ) } @@ -201,6 +211,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC private val carrierNetworkChange = booleanList // false first so the base case doesn't have roaming set (more common) private val roaming = listOf(false, true) + private val names = listOf("name 1", "name 2") @Parameters(name = "{0}") @JvmStatic fun data() = testData() @@ -236,6 +247,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC activity.first(), carrierNetworkChange.first(), roaming.first(), + names.first(), ) val tail = @@ -246,7 +258,8 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) }, activity.map { baseCase.modifiedBy(activity = it) }, carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) }, - roaming.map { baseCase.modifiedBy(roaming = it) } + roaming.map { baseCase.modifiedBy(roaming = it) }, + names.map { baseCase.modifiedBy(name = it) }, ) .flatten() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index e4f29e2038723..34f30eb7c0a69 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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT +import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon @@ -24,9 +25,11 @@ import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -289,10 +292,12 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(conn.subId).isEqualTo(model.subId) assertThat(connectionInfo.cdmaLevel).isEqualTo(model.level) assertThat(connectionInfo.primaryLevel).isEqualTo(model.level) - assertThat(connectionInfo.dataActivityDirection).isEqualTo(model.activity) + assertThat(connectionInfo.dataActivityDirection) + .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()) assertThat(connectionInfo.carrierNetworkChangeActive) .isEqualTo(model.carrierNetworkChange) assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming) + assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name)) // TODO(b/261029387) check these once we start handling them assertThat(connectionInfo.isEmergencyOnly).isFalse() @@ -325,4 +330,5 @@ fun validMobileEvent( activity = activity, carrierNetworkChange = carrierNetworkChange, roaming = roaming, + name = "demo name", ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 0b3e5b5bedea2..7fa80653f29c9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod +import android.content.Intent import android.os.UserHandle import android.provider.Settings import android.telephony.CellSignalStrengthCdma @@ -23,10 +24,16 @@ import android.telephony.ServiceState import android.telephony.SignalStrength import android.telephony.SubscriptionInfo import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.DataActivityListener import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA import android.telephony.TelephonyManager +import android.telephony.TelephonyManager.DATA_ACTIVITY_DORMANT +import android.telephony.TelephonyManager.DATA_ACTIVITY_IN +import android.telephony.TelephonyManager.DATA_ACTIVITY_INOUT +import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE +import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT import android.telephony.TelephonyManager.DATA_CONNECTED import android.telephony.TelephonyManager.DATA_CONNECTING import android.telephony.TelephonyManager.DATA_DISCONNECTED @@ -34,18 +41,27 @@ import android.telephony.TelephonyManager.DATA_DISCONNECTING import android.telephony.TelephonyManager.DATA_UNKNOWN import android.telephony.TelephonyManager.ERI_OFF import android.telephony.TelephonyManager.ERI_ON +import android.telephony.TelephonyManager.EXTRA_PLMN +import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN +import android.telephony.TelephonyManager.EXTRA_SHOW_SPN +import android.telephony.TelephonyManager.EXTRA_SPN +import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock @@ -90,8 +106,11 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { MobileConnectionRepositoryImpl( context, SUB_1_ID, + DEFAULT_NAME, + SEP, telephonyManager, globalSettings, + fakeBroadcastDispatcher, connectionsRepo.defaultDataSubId, connectionsRepo.globalMobileDataSettingChangedEvent, mobileMappings, @@ -249,10 +268,11 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { var latest: MobileConnectionModel? = null val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this) - val callback = getTelephonyCallbackForType() - callback.onDataActivity(3) + val callback = getTelephonyCallbackForType() + callback.onDataActivity(DATA_ACTIVITY_INOUT) - assertThat(latest?.dataActivityDirection).isEqualTo(3) + assertThat(latest?.dataActivityDirection) + .isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) job.cancel() } @@ -459,6 +479,112 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun `activity - updates from callback`() = + runBlocking(IMMEDIATE) { + var latest: DataActivityModel? = null + val job = + underTest.connectionInfo.onEach { latest = it.dataActivityDirection }.launchIn(this) + + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + + val cb = getTelephonyCallbackForType() + cb.onDataActivity(DATA_ACTIVITY_IN) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false)) + + cb.onDataActivity(DATA_ACTIVITY_OUT) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true)) + + cb.onDataActivity(DATA_ACTIVITY_INOUT) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true)) + + cb.onDataActivity(DATA_ACTIVITY_NONE) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + + cb.onDataActivity(DATA_ACTIVITY_DORMANT) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + + cb.onDataActivity(1234) + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + + job.cancel() + } + + @Test + fun `network name - default`() = + runBlocking(IMMEDIATE) { + var latest: NetworkNameModel? = null + val job = underTest.networkName.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(DEFAULT_NAME) + + job.cancel() + } + + @Test + fun `network name - uses broadcast info - returns derived`() = + runBlocking(IMMEDIATE) { + var latest: NetworkNameModel? = null + val job = underTest.networkName.onEach { latest = it }.launchIn(this) + + val intent = spnIntent() + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive(context, intent) + } + + assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP)) + + job.cancel() + } + + @Test + fun `network name - broadcast not for this sub id - returns default`() = + runBlocking(IMMEDIATE) { + var latest: NetworkNameModel? = null + val job = underTest.networkName.onEach { latest = it }.launchIn(this) + + val intent = spnIntent(subId = 101) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive(context, intent) + } + + assertThat(latest).isEqualTo(DEFAULT_NAME) + + job.cancel() + } + + @Test + fun `network name - operatorAlphaShort - tracked`() = + runBlocking(IMMEDIATE) { + var latest: String? = null + + val job = + underTest.connectionInfo.onEach { latest = it.operatorAlphaShort }.launchIn(this) + + val shortName = "short name" + val serviceState = ServiceState() + serviceState.setOperatorName( + /* longName */ "long name", + /* shortName */ shortName, + /* numeric */ "12345", + ) + + getTelephonyCallbackForType().onServiceStateChanged(serviceState) + + assertThat(latest).isEqualTo(shortName) + + job.cancel() + } + private fun getTelephonyCallbacks(): List { val callbackCaptor = argumentCaptor() Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) @@ -484,10 +610,31 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { return signalStrength } + private fun spnIntent( + subId: Int = SUB_1_ID, + showSpn: Boolean = true, + spn: String = SPN, + showPlmn: Boolean = true, + plmn: String = PLMN, + ): Intent = + Intent(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED).apply { + putExtra(EXTRA_SUBSCRIPTION_ID, subId) + putExtra(EXTRA_SHOW_SPN, showSpn) + putExtra(EXTRA_SPN, spn) + putExtra(EXTRA_SHOW_PLMN, showPlmn) + putExtra(EXTRA_PLMN, plmn) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 private val SUB_1 = mock().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private val DEFAULT_NAME = NetworkNameModel.Default("default name") + private const val SEP = "-" + + private const val SPN = "testSpn" + private const val PLMN = "testPlmn" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 6d80acbb65ac0..3cc1e8b74668e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -91,6 +91,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { connectionFactory = MobileConnectionRepositoryImpl.Factory( + fakeBroadcastDispatcher, context = context, telephonyManager = telephonyManager, bgDispatcher = IMMEDIATE, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 0e2c38e9a0716..c3519b7c81767 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -19,14 +19,26 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileIconInteractor : MobileIconInteractor { override val alwaysShowDataRatIcon = MutableStateFlow(false) + override val activity = + MutableStateFlow( + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ) + ) + private val _iconGroup = MutableStateFlow(TelephonyIcons.THREE_G) override val networkTypeIconGroup = _iconGroup + override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo mode")) + private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 9b6f6df90174b..4dca780425e54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -25,6 +25,7 @@ import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -392,6 +393,41 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun `network name - uses operatorAlphaShot when non null and repo is default`() = + runBlocking(IMMEDIATE) { + var latest: NetworkNameModel? = null + val job = underTest.networkName.onEach { latest = it }.launchIn(this) + + val testOperatorName = "operatorAlphaShort" + + // Default network name, operator name is non-null, uses the operator name + connectionRepository.networkName.value = DEFAULT_NAME + connectionRepository.setConnectionInfo( + MobileConnectionModel(operatorAlphaShort = testOperatorName) + ) + yield() + + assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName)) + + // Default network name, operator name is null, uses the default + connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null)) + yield() + + assertThat(latest).isEqualTo(DEFAULT_NAME) + + // Derived network name, operator name non-null, uses the derived name + connectionRepository.networkName.value = DERIVED_NAME + connectionRepository.setConnectionInfo( + MobileConnectionModel(operatorAlphaShort = testOperatorName) + ) + yield() + + assertThat(latest).isEqualTo(DERIVED_NAME) + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate @@ -401,5 +437,8 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val SUB_1_ID = 1 private val SUB_1 = mock().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private val DEFAULT_NAME = NetworkNameModel.Default("test default name") + private val DERIVED_NAME = NetworkNameModel.Derived("test derived name") } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 2c8f0a7d9e74c..415ce75345b25 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -23,7 +23,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -40,6 +43,7 @@ class MobileIconViewModelTest : SysuiTestCase() { private lateinit var underTest: MobileIconViewModel private val interactor = FakeMobileIconInteractor() @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants @Before fun setUp() { @@ -53,7 +57,7 @@ class MobileIconViewModelTest : SysuiTestCase() { setNumberOfLevels(4) isDataConnected.value = true } - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) + underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) } @Test @@ -250,6 +254,92 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun `data activity - null when config is off`() = + runBlocking(IMMEDIATE) { + // Create a new view model here so the constants are properly read + whenever(constants.shouldShowActivityConfig).thenReturn(false) + underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + + var inVisible: Boolean? = null + val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + + var outVisible: Boolean? = null + val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this) + + var containerVisible: Boolean? = null + val containerJob = + underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this) + + interactor.activity.value = + DataActivityModel( + hasActivityIn = true, + hasActivityOut = true, + ) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + + inJob.cancel() + outJob.cancel() + containerJob.cancel() + } + + @Test + fun `data activity - config on - test indicators`() = + runBlocking(IMMEDIATE) { + // Create a new view model here so the constants are properly read + whenever(constants.shouldShowActivityConfig).thenReturn(true) + underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + + var inVisible: Boolean? = null + val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + + var outVisible: Boolean? = null + val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this) + + var containerVisible: Boolean? = null + val containerJob = + underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) + + interactor.activity.value = + DataActivityModel( + hasActivityIn = true, + hasActivityOut = false, + ) + + yield() + + assertThat(inVisible).isTrue() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() + + interactor.activity.value = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = true, + ) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isTrue() + assertThat(containerVisible).isTrue() + + interactor.activity.value = + DataActivityModel( + hasActivityIn = false, + hasActivityOut = false, + ) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + + inJob.cancel() + outJob.cancel() + containerJob.cancel() + } + /** Convenience constructor for these tests */ private fun defaultSignal( level: Int = 1,