diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 2526fa679066a..b51768f20f0bb 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -369,7 +369,7 @@ object Flags { // TODO(b/292533677): Tracking Bug val WIFI_TRACKER_LIB_FOR_WIFI_ICON = - unreleasedFlag(613, "wifi_tracker_lib_for_wifi_icon") + unreleasedFlag(613, "wifi_tracker_lib_for_wifi_icon", teamfood = true) // TODO(b/293863612): Tracking Bug @JvmField val INCOMPATIBLE_CHARGING_BATTERY_ICON = @@ -378,6 +378,9 @@ object Flags { // TODO(b/293585143): Tracking Bug val INSTANT_TETHER = unreleasedFlag(615, "instant_tether") + // TODO(b/294588085): Tracking Bug + val WIFI_SECONDARY_NETWORKS = unreleasedFlag(617, "wifi_secondary_networks") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip") 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 b29d46174bd17..b5b99a76ded84 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 @@ -29,9 +29,18 @@ interface WifiRepository { /** Observable for the current wifi default status. */ val isWifiDefault: StateFlow - /** Observable for the current wifi network. */ + /** Observable for the current primary wifi network. */ val wifiNetwork: StateFlow + /** + * Observable for secondary wifi networks (if any). Should specifically exclude the primary + * network emitted by [wifiNetwork]. + * + * This isn't used by phones/tablets, which only display the primary network, but may be used by + * other variants like Car. + */ + val secondaryNetworks: StateFlow> + /** Observable for the current wifi network activity. */ val wifiActivity: StateFlow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt index e96288ab9ef9f..80091ac7bdba3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt @@ -115,6 +115,11 @@ constructor( .flatMapLatest { it.wifiNetwork } .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value) + override val secondaryNetworks: StateFlow> = + activeRepo + .flatMapLatest { it.secondaryNetworks } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.secondaryNetworks.value) + override val wifiActivity: StateFlow = activeRepo .flatMapLatest { it.wifiActivity } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt index 99b680056d7f5..4b19c3a153ed5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -48,6 +48,9 @@ constructor( private val _wifiNetwork = MutableStateFlow(WifiNetworkModel.Inactive) override val wifiNetwork: StateFlow = _wifiNetwork + private val _secondaryNetworks = MutableStateFlow>(emptyList()) + override val secondaryNetworks: StateFlow> = _secondaryNetworks + private val _wifiActivity = MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) override val wifiActivity: StateFlow = _wifiActivity diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt index 9ed7c6a83cee1..36c46a90c731b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt @@ -43,6 +43,9 @@ class DisabledWifiRepository @Inject constructor() : override val wifiNetwork: StateFlow = MutableStateFlow(NETWORK).asStateFlow() + override val secondaryNetworks: StateFlow> = + MutableStateFlow(emptyList()).asStateFlow() + override val wifiActivity: StateFlow = MutableStateFlow(ACTIVITY).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index afd15765d163d..7c7b58d00e3bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -56,8 +56,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest @@ -216,6 +218,10 @@ constructor( initialValue = WIFI_NETWORK_DEFAULT, ) + // Secondary networks can only be supported by [WifiRepositoryViaTrackerLib]. + override val secondaryNetworks: StateFlow> = + MutableStateFlow(emptyList()).asStateFlow() + override val wifiActivity: StateFlow = WifiRepositoryHelper.createActivityFlow( wifiManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt index 175563bb07640..d4f40dd03a322 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt @@ -93,7 +93,8 @@ constructor( WifiPickerTrackerInfo( state = WIFI_STATE_DEFAULT, isDefault = false, - network = WIFI_NETWORK_DEFAULT, + primaryNetwork = WIFI_NETWORK_DEFAULT, + secondaryNetworks = emptyList(), ) callbackFlow { val callback = @@ -102,6 +103,17 @@ constructor( val connectedEntry = wifiPickerTracker?.connectedWifiEntry logOnWifiEntriesChanged(connectedEntry) + val secondaryNetworks = + if (featureFlags.isEnabled(Flags.WIFI_SECONDARY_NETWORKS)) { + val activeNetworks = + wifiPickerTracker?.activeWifiEntries ?: emptyList() + activeNetworks + .filter { it != connectedEntry && !it.isPrimaryNetwork } + .map { it.toWifiNetworkModel() } + } else { + emptyList() + } + // [WifiPickerTracker.connectedWifiEntry] will return the same instance // but with updated internals. For example, when its validation status // changes from false to true, the same instance is re-used but with the @@ -112,8 +124,9 @@ constructor( // into our internal model immediately. [toWifiNetworkModel] always // returns a new instance, so the flow is guaranteed to emit. send( - newNetwork = connectedEntry?.toWifiNetworkModel() + newPrimaryNetwork = connectedEntry?.toPrimaryWifiNetworkModel() ?: WIFI_NETWORK_DEFAULT, + newSecondaryNetworks = secondaryNetworks, newIsDefault = connectedEntry?.isDefaultNetwork ?: false, ) } @@ -131,9 +144,17 @@ constructor( private fun send( newState: Int = current.state, newIsDefault: Boolean = current.isDefault, - newNetwork: WifiNetworkModel = current.network, + newPrimaryNetwork: WifiNetworkModel = current.primaryNetwork, + newSecondaryNetworks: List = + current.secondaryNetworks, ) { - val new = WifiPickerTrackerInfo(newState, newIsDefault, newNetwork) + val new = + WifiPickerTrackerInfo( + newState, + newIsDefault, + newPrimaryNetwork, + newSecondaryNetworks, + ) current = new trySend(new) } @@ -170,7 +191,7 @@ constructor( override val wifiNetwork: StateFlow = wifiPickerTrackerInfo - .map { it.network } + .map { it.primaryNetwork } .distinctUntilChanged() .logDiffsForTable( wifiTrackerLibTableLogBuffer, @@ -179,11 +200,32 @@ constructor( ) .stateIn(scope, SharingStarted.Eagerly, WIFI_NETWORK_DEFAULT) + override val secondaryNetworks: StateFlow> = + wifiPickerTrackerInfo + .map { it.secondaryNetworks } + .distinctUntilChanged() + .logDiffsForTable( + wifiTrackerLibTableLogBuffer, + columnPrefix = "", + columnName = "secondaryNetworks", + initialValue = emptyList(), + ) + .stateIn(scope, SharingStarted.Eagerly, emptyList()) + + /** + * Converts WifiTrackerLib's [WifiEntry] into our internal model only if the entry is the + * primary network. Returns an inactive network if it's not primary. + */ + private fun WifiEntry.toPrimaryWifiNetworkModel(): WifiNetworkModel { + return if (!this.isPrimaryNetwork) { + WIFI_NETWORK_DEFAULT + } else { + this.toWifiNetworkModel() + } + } + /** Converts WifiTrackerLib's [WifiEntry] into our internal model. */ private fun WifiEntry.toWifiNetworkModel(): WifiNetworkModel { - if (!this.isPrimaryNetwork) { - return WIFI_NETWORK_DEFAULT - } return if (this is MergedCarrierEntry) { this.convertCarrierMergedToModel() } else { @@ -291,7 +333,9 @@ constructor( /** True if wifi is currently the default connection and false otherwise. */ val isDefault: Boolean, /** The currently primary wifi network. */ - val network: WifiNetworkModel, + val primaryNetwork: WifiNetworkModel, + /** The current secondary network(s), if any. Specifically excludes the primary network. */ + val secondaryNetworks: List ) @SysUISingleton diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt index 1c8dac14b0899..4f7bb724aae31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt @@ -34,6 +34,8 @@ class FakeWifiRepository : WifiRepository { MutableStateFlow(WifiNetworkModel.Inactive) override val wifiNetwork: StateFlow = _wifiNetwork + override val secondaryNetworks = MutableStateFlow>(emptyList()) + private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT) override val wifiActivity: StateFlow = _wifiActivity diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 2dbeb7aa7e906..bea1154eeb345 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -1003,6 +1003,27 @@ class WifiRepositoryImplTest : SysuiTestCase() { assertThat(latest2Active.ssid).isEqualTo(SSID) } + @Test + fun secondaryNetworks_alwaysEmpty() = + testScope.runTest { + val latest by collectLastValue(underTest.secondaryNetworks) + collectLastValue(underTest.wifiNetwork) + + // Even WHEN we do have non-primary wifi info + val wifiInfo = + mock().apply { + whenever(this.ssid).thenReturn(SSID) + whenever(this.isPrimary).thenReturn(false) + } + val network = mock().apply { whenever(this.getNetId()).thenReturn(NETWORK_ID) } + + getNetworkCallback() + .onCapabilitiesChanged(network, createWifiNetworkCapabilities(wifiInfo)) + + // THEN the secondary networks list is empty because this repo doesn't support it + assertThat(latest).isEmpty() + } + @Test fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt index 9959e00fd3f37..662e36a55b9b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt @@ -101,6 +101,7 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { @Before fun setUp() { featureFlags.set(Flags.INSTANT_TETHER, false) + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, false) whenever(wifiPickerTrackerFactory.create(any(), capture(callbackCaptor))) .thenReturn(wifiPickerTracker) } @@ -762,6 +763,197 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { assertThat(latest2Active.ssid).isEqualTo(TITLE) } + @Test + fun secondaryNetworks_activeEntriesEmpty_isEmpty() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf()) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).isEmpty() + } + + @Test + fun secondaryNetworks_oneActiveEntry_hasOne() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val wifiEntry = mock() + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(wifiEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(1) + } + + @Test + fun secondaryNetworks_multipleActiveEntries_hasMultiple() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val wifiEntry1 = mock() + val wifiEntry2 = mock() + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(wifiEntry1, wifiEntry2)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(2) + } + + @Test + fun secondaryNetworks_mapsToInactive() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val inactiveEntry = + mock().apply { whenever(this.level).thenReturn(WIFI_LEVEL_UNREACHABLE) } + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(inactiveEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(WifiNetworkModel.Inactive::class.java) + } + + @Test + fun secondaryNetworks_mapsToActive() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val activeEntry = mock().apply { whenever(this.level).thenReturn(2) } + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(activeEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(WifiNetworkModel.Active::class.java) + assertThat((latest!![0] as WifiNetworkModel.Active).level).isEqualTo(2) + } + + @Test + fun secondaryNetworks_mapsToCarrierMerged() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val carrierMergedEntry = + mock().apply { whenever(this.level).thenReturn(3) } + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(carrierMergedEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(1) + assertThat(latest!![0]).isInstanceOf(WifiNetworkModel.CarrierMerged::class.java) + assertThat((latest!![0] as WifiNetworkModel.CarrierMerged).level).isEqualTo(3) + } + + @Test + fun secondaryNetworks_mapsMultipleInOrder() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val activeEntry = mock().apply { whenever(this.level).thenReturn(2) } + val carrierMergedEntry = + mock().apply { whenever(this.level).thenReturn(3) } + whenever(wifiPickerTracker.activeWifiEntries) + .thenReturn(listOf(activeEntry, carrierMergedEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest!![0]).isInstanceOf(WifiNetworkModel.Active::class.java) + assertThat((latest!![0] as WifiNetworkModel.Active).level).isEqualTo(2) + assertThat(latest!![1]).isInstanceOf(WifiNetworkModel.CarrierMerged::class.java) + assertThat((latest!![1] as WifiNetworkModel.CarrierMerged).level).isEqualTo(3) + } + + @Test + fun secondaryNetworks_filtersOutConnectedEntry() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val connectedEntry = mock().apply { whenever(this.level).thenReturn(1) } + val secondaryEntry1 = mock().apply { whenever(this.level).thenReturn(2) } + val secondaryEntry2 = mock().apply { whenever(this.level).thenReturn(3) } + // WHEN the active list has both a primary and secondary networks + whenever(wifiPickerTracker.activeWifiEntries) + .thenReturn(listOf(connectedEntry, secondaryEntry1, secondaryEntry2)) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(connectedEntry) + + getCallback().onWifiEntriesChanged() + + // THEN only the secondary networks are included + assertThat(latest).hasSize(2) + assertThat((latest!![0] as WifiNetworkModel.Active).level).isEqualTo(2) + assertThat((latest!![1] as WifiNetworkModel.Active).level).isEqualTo(3) + } + + @Test + fun secondaryNetworks_noConnectedEntry_hasAllActiveEntries() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val secondaryEntry1 = mock().apply { whenever(this.level).thenReturn(2) } + val secondaryEntry2 = mock().apply { whenever(this.level).thenReturn(3) } + whenever(wifiPickerTracker.activeWifiEntries) + .thenReturn(listOf(secondaryEntry1, secondaryEntry2)) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).hasSize(2) + assertThat((latest!![0] as WifiNetworkModel.Active).level).isEqualTo(2) + assertThat((latest!![1] as WifiNetworkModel.Active).level).isEqualTo(3) + } + + @Test + fun secondaryNetworks_filtersOutPrimaryNetwork() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) + val latest by collectLastValue(underTest.secondaryNetworks) + + val primaryEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(1) + } + val secondaryEntry1 = mock().apply { whenever(this.level).thenReturn(2) } + val secondaryEntry2 = mock().apply { whenever(this.level).thenReturn(3) } + // WHEN the active list has both a primary and secondary networks + whenever(wifiPickerTracker.activeWifiEntries) + .thenReturn(listOf(secondaryEntry1, primaryEntry, secondaryEntry2)) + + getCallback().onWifiEntriesChanged() + + // THEN only the secondary networks are included + assertThat(latest).hasSize(2) + assertThat((latest!![0] as WifiNetworkModel.Active).level).isEqualTo(2) + assertThat((latest!![1] as WifiNetworkModel.Active).level).isEqualTo(3) + } + + @Test + fun secondaryNetworks_flagOff_noNetworks() = + testScope.runTest { + featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, false) + val latest by collectLastValue(underTest.secondaryNetworks) + + val wifiEntry = mock() + whenever(wifiPickerTracker.activeWifiEntries).thenReturn(listOf(wifiEntry)) + + getCallback().onWifiEntriesChanged() + + assertThat(latest).isEmpty() + } + @Test fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = testScope.runTest {