diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index fa712872eb134..ea77163f05564 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -60,6 +60,13 @@ interface MobileConnectionsRepository { */ val mobileIsDefault: StateFlow + /** + * True if the device currently has a carrier merged connection. + * + * See [CarrierMergedConnectionRepository] for more info. + */ + val hasCarrierMergedConnection: Flow + /** True if the default network connection is validated and false otherwise. */ val defaultConnectionIsValidated: StateFlow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index 44b5b3fa25911..eb20bba0d21f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -159,6 +159,15 @@ constructor( .flatMapLatest { it.mobileIsDefault } .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.mobileIsDefault.value) + override val hasCarrierMergedConnection: StateFlow = + activeRepo + .flatMapLatest { it.hasCarrierMergedConnection } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realRepository.hasCarrierMergedConnection.value, + ) + override val defaultConnectionIsValidated: StateFlow = activeRepo .flatMapLatest { it.defaultConnectionIsValidated } 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 737bc6826d08b..0e4ceebcc8548 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 @@ -159,6 +159,9 @@ constructor( // TODO(b/261029387): not yet supported override val mobileIsDefault: StateFlow = MutableStateFlow(true) + // TODO(b/261029387): not yet supported + override val hasCarrierMergedConnection = MutableStateFlow(false) + // TODO(b/261029387): not yet supported override val defaultConnectionIsValidated: StateFlow = MutableStateFlow(true) 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 8c93bf7c21985..71928b8beb52c 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 @@ -58,6 +58,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -257,18 +258,32 @@ constructor( override val mobileIsDefault: StateFlow = connectivityRepository.defaultConnections - // Because carrier merged networks are displayed as mobile networks, they're - // part of the `isDefault` calculation. See b/272586234. - .map { it.mobile.isDefault || it.carrierMerged.isDefault } + .map { it.mobile.isDefault } .distinctUntilChanged() .logDiffsForTable( tableLogger, - columnPrefix = "", + columnPrefix = LOGGING_PREFIX, columnName = "mobileIsDefault", initialValue = false, ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val hasCarrierMergedConnection: StateFlow = + combine( + connectivityRepository.defaultConnections, + carrierMergedSubId, + ) { defaultConnections, carrierMergedSubId -> + defaultConnections.carrierMerged.isDefault || carrierMergedSubId != null + } + .distinctUntilChanged() + .logDiffsForTable( + tableLogger, + columnPrefix = LOGGING_PREFIX, + columnName = "hasCarrierMergedConnection", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val defaultConnectionIsValidated: StateFlow = connectivityRepository.defaultConnections .map { it.isValidated } 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 6c8310ac3d294..a39bd45a6b7a5 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 @@ -114,7 +114,22 @@ constructor( @Application private val scope: CoroutineScope, ) : MobileIconsInteractor { - override val mobileIsDefault = mobileConnectionsRepo.mobileIsDefault + override val mobileIsDefault = + combine( + mobileConnectionsRepo.mobileIsDefault, + mobileConnectionsRepo.hasCarrierMergedConnection, + ) { mobileIsDefault, hasCarrierMergedConnection -> + // Because carrier merged networks are displayed as mobile networks, they're part of + // the `isDefault` calculation. See b/272586234. + mobileIsDefault || hasCarrierMergedConnection + } + .logDiffsForTable( + tableLogger, + LOGGING_PREFIX, + columnName = "mobileIsDefault", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val activeDataConnectionHasDataEnabled: StateFlow = mobileConnectionsRepo.activeMobileDataRepository 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 f9c72d5236738..3591c17403295 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 @@ -67,6 +67,8 @@ class FakeMobileConnectionsRepository( override val mobileIsDefault = MutableStateFlow(false) + override val hasCarrierMergedConnection = MutableStateFlow(false) + override val defaultConnectionIsValidated = MutableStateFlow(false) private val subIdRepos = mutableMapOf() 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 a259074a7a79d..c40e9237bdb7e 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 @@ -67,6 +67,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.After import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue @@ -75,7 +76,6 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock -import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -727,28 +727,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } - /** Regression test for b/272586234. */ - @Test - fun mobileIsDefault_carrierMergedViaWifi_isDefault() = - runBlocking(IMMEDIATE) { - val carrierMergedInfo = - mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } - val caps = - mock().also { - whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(carrierMergedInfo) - } - - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - - assertThat(latest).isTrue() - - job.cancel() - } - @Test fun mobileIsDefault_carrierMergedViaMobile_isDefault() = runBlocking(IMMEDIATE) { @@ -770,122 +748,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } - /** Regression test for b/272586234. */ - @Test - fun mobileIsDefault_carrierMergedViaWifiWithVcnTransport_isDefault() = - runBlocking(IMMEDIATE) { - val carrierMergedInfo = - mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } - val caps = - mock().also { - whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) - } - - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - - assertThat(latest).isTrue() - - job.cancel() - } - - @Test - fun mobileIsDefault_carrierMergedViaMobileWithVcnTransport_isDefault() = - runBlocking(IMMEDIATE) { - val carrierMergedInfo = - mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } - val caps = - mock().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) - } - - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - - assertThat(latest).isTrue() - - job.cancel() - } - - @Test - fun mobileIsDefault_isCarrierMergedViaUnderlyingWifi_isDefault() = - runBlocking(IMMEDIATE) { - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - - val underlyingNetwork = mock() - val carrierMergedInfo = - mock().apply { - whenever(this.isCarrierMerged).thenReturn(true) - } - val underlyingWifiCapabilities = - mock().also { - whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - whenever(it.transportInfo).thenReturn(carrierMergedInfo) - } - whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork)) - .thenReturn(underlyingWifiCapabilities) - - // WHEN the main capabilities have an underlying carrier merged network via WIFI - // transport and WifiInfo - val mainCapabilities = - mock().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(null) - whenever(it.underlyingNetworks).thenReturn(listOf(underlyingNetwork)) - } - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) - - // THEN carrier merged is default, so mobile is default - assertThat(latest).isTrue() - - job.cancel() - } - - @Test - fun mobileIsDefault_isCarrierMergedViaUnderlyingCellular_isDefault() = - runBlocking(IMMEDIATE) { - var latest: Boolean? = null - val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) - - val underlyingCarrierMergedNetwork = mock() - val carrierMergedInfo = - mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } - val underlyingCapabilities = - mock().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) - } - whenever( - connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork) - ) - .thenReturn(underlyingCapabilities) - - // WHEN the main capabilities have an underlying carrier merged network via CELLULAR - // transport and VcnTransportInfo - val mainCapabilities = - mock().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) - whenever(it.transportInfo).thenReturn(null) - whenever(it.underlyingNetworks) - .thenReturn(listOf(underlyingCarrierMergedNetwork)) - } - - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) - - // THEN carrier merged is default, so mobile is default - assertThat(latest).isTrue() - - job.cancel() - } - @Test fun mobileIsDefault_wifiDefault_mobileNotDefault() = runBlocking(IMMEDIATE) { @@ -922,6 +784,195 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + /** Regression test for b/272586234. */ + @Test + fun hasCarrierMergedConnection_carrierMergedViaWifi_isTrue() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } + + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun hasCarrierMergedConnection_carrierMergedViaMobile_isTrue() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } + + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + /** Regression test for b/272586234. */ + @Test + fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + } + + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun hasCarrierMergedConnection_carrierMergedViaMobileWithVcnTransport_isTrue() = + runBlocking(IMMEDIATE) { + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + } + + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingWifi_isTrue() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + val underlyingNetwork = mock() + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val underlyingWifiCapabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } + whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork)) + .thenReturn(underlyingWifiCapabilities) + + // WHEN the main capabilities have an underlying carrier merged network via WIFI + // transport and WifiInfo + val mainCapabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(null) + whenever(it.underlyingNetworks).thenReturn(listOf(underlyingNetwork)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) + yield() + + // THEN there's a carrier merged connection + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingCellular_isTrue() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + val underlyingCarrierMergedNetwork = mock() + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } + val underlyingCapabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo)) + } + whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork)) + .thenReturn(underlyingCapabilities) + + // WHEN the main capabilities have an underlying carrier merged network via CELLULAR + // transport and VcnTransportInfo + val mainCapabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true) + whenever(it.transportInfo).thenReturn(null) + whenever(it.underlyingNetworks) + .thenReturn(listOf(underlyingCarrierMergedNetwork)) + } + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) + yield() + + // THEN there's a carrier merged connection + assertThat(latest).isTrue() + + job.cancel() + } + + /** Regression test for b/272586234. */ + @Test + fun hasCarrierMergedConnection_defaultNotCarrierMerged_butWifiRepoHasCarrierMerged_isTrue() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.hasCarrierMergedConnection.onEach { latest = it }.launchIn(this) + + // WHEN the default callback isn't carrier merged + val carrierMergedInfo = + mock().apply { whenever(this.isCarrierMerged).thenReturn(false) } + val caps = + mock().also { + whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) + whenever(it.transportInfo).thenReturn(carrierMergedInfo) + } + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + yield() + + // BUT the wifi repo has gotten updates that it *is* carrier merged + wifiRepository.setWifiNetwork(WIFI_NETWORK_CM) + yield() + + // THEN hasCarrierMergedConnection is true + assertThat(latest).isTrue() + + job.cancel() + } + @Test fun defaultConnectionIsValidated_startsAsFalse() { assertThat(underTest.defaultConnectionIsValidated.value).isFalse() 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 898e897703946..0ccf3f387f900 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 @@ -455,7 +455,50 @@ class MobileIconsInteractorTest : SysuiTestCase() { } @Test - fun mobileIsDefault_usesRepoValue() = + fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + connectionsRepository.mobileIsDefault.value = false + connectionsRepository.hasCarrierMergedConnection.value = false + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + connectionsRepository.mobileIsDefault.value = true + connectionsRepository.hasCarrierMergedConnection.value = false + + assertThat(latest).isTrue() + + job.cancel() + } + + /** Regression test for b/272586234. */ + @Test + fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = + testScope.runTest { + var latest: Boolean? = null + val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) + + connectionsRepository.mobileIsDefault.value = false + connectionsRepository.hasCarrierMergedConnection.value = true + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun mobileIsDefault_updatesWhenRepoUpdates() = testScope.runTest { var latest: Boolean? = null val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this) @@ -466,7 +509,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { connectionsRepository.mobileIsDefault.value = false assertThat(latest).isFalse() - connectionsRepository.mobileIsDefault.value = true + connectionsRepository.hasCarrierMergedConnection.value = true assertThat(latest).isTrue() job.cancel()