Merge "[SB Refactor] Show space between wifi and RAT icon if needed." into udc-dev

This commit is contained in:
Caitlin Shkuratov
2023-04-12 18:49:06 +00:00
committed by Android (Google) Code Review
10 changed files with 299 additions and 28 deletions

View File

@@ -35,6 +35,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupR
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
@@ -53,6 +54,9 @@ import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import kotlinx.coroutines.flow.Flow
import java.util.function.Supplier
import javax.inject.Named
@Module
abstract class StatusBarPipelineModule {
@@ -113,6 +117,17 @@ abstract class StatusBarPipelineModule {
}
}
@Provides
@SysUISingleton
@Named(FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON)
fun provideFirstMobileSubShowingNetworkTypeIconProvider(
mobileIconsViewModel: MobileIconsViewModel,
): Supplier<Flow<Boolean>> {
return Supplier<Flow<Boolean>> {
mobileIconsViewModel.firstMobileSubShowingNetworkTypeIcon
}
}
@Provides
@SysUISingleton
@WifiInputLog
@@ -168,5 +183,8 @@ abstract class StatusBarPipelineModule {
fun provideVerboseMobileViewLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("VerboseMobileViewLog", 100)
}
const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
"FirstMobileSubShowingNetworkTypeIcon"
}
}

View File

@@ -32,6 +32,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -63,22 +66,36 @@ constructor(
}
.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> =
subscriptionIdsFlow
.map {
if (it.isEmpty()) {
null
} else {
// Mobile icons get reversed by [StatusBarIconController], so the last element
// in this list will show up visually first.
commonViewModelForSub(it.last())
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
/**
* A flow that emits `true` if the mobile sub that's displayed first visually is showing its
* network type icon and `false` otherwise.
*/
val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> =
firstMobileSubViewModel
.flatMapLatest { firstMobileSubViewModel ->
firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
init {
scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } }
}
fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel {
val common =
mobileIconSubIdCache[subId]
?: MobileIconViewModel(
subId,
interactor.createMobileConnectionInteractorForSubId(subId),
airplaneModeInteractor,
constants,
scope,
)
.also { mobileIconSubIdCache[subId] = it }
val common = commonViewModelForSub(subId)
return LocationBasedMobileViewModel.viewModelForLocation(
common,
statusBarPipelineFlags,
@@ -87,6 +104,18 @@ constructor(
)
}
private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon {
return mobileIconSubIdCache[subId]
?: MobileIconViewModel(
subId,
interactor.createMobileConnectionInteractorForSubId(subId),
airplaneModeInteractor,
constants,
scope,
)
.also { mobileIconSubIdCache[subId] = it }
}
private fun removeInvalidModelsFromCache(subIds: List<Int>) {
val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) }
subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }

View File

@@ -36,7 +36,6 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWi
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -64,6 +63,7 @@ object WifiViewBinder {
val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
val activityContainerView = view.requireViewById<View>(R.id.inout_container)
val airplaneSpacer = view.requireViewById<View>(R.id.wifi_airplane_spacer)
val signalSpacer = view.requireViewById<View>(R.id.wifi_signal_spacer)
view.isVisible = true
iconView.isVisible = true
@@ -133,6 +133,12 @@ object WifiViewBinder {
}
}
launch {
viewModel.isSignalSpacerVisible.distinctUntilChanged().collect { visible ->
signalSpacer.isVisible = visible
}
}
try {
awaitCancellation()
} finally {

View File

@@ -30,8 +30,8 @@ import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule.Companion.FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON
import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
@@ -39,7 +39,9 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import java.util.function.Supplier
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -53,24 +55,24 @@ import kotlinx.coroutines.flow.stateIn
/**
* Models the UI state for the status bar wifi icon.
*
* This class exposes three view models, one per status bar location: [home], [keyguard], and [qs].
* In order to get the UI state for the wifi icon, you must use one of those view models (whichever
* is correct for your location).
*
* Internally, this class maintains the current state of the wifi icon and notifies those three view
* models of any changes.
* This is a singleton so that we don't have duplicate logs and should *not* be used directly to
* control views. Instead, use an instance of [LocationBasedWifiViewModel]. See
* [LocationBasedWifiViewModel.viewModelForLocation].
*/
@SysUISingleton
class WifiViewModel
@Inject
constructor(
airplaneModeViewModel: AirplaneModeViewModel,
// TODO(b/238425913): The wifi icon shouldn't need to consume mobile information. A
// container-level view model should do the work instead.
@Named(FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON)
shouldShowSignalSpacerProvider: Supplier<Flow<Boolean>>,
connectivityConstants: ConnectivityConstants,
private val context: Context,
@WifiTableLog wifiTableLogBuffer: TableLogBuffer,
interactor: WifiInteractor,
@Application private val scope: CoroutineScope,
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiConstants: WifiConstants,
) : WifiViewModelCommon {
/** Returns the icon to use based on the given network. */
@@ -183,6 +185,8 @@ constructor(
override val isAirplaneSpacerVisible: Flow<Boolean> =
airplaneModeViewModel.isAirplaneModeIconVisible
override val isSignalSpacerVisible: Flow<Boolean> = shouldShowSignalSpacerProvider.get()
companion object {
@StringRes
@VisibleForTesting

View File

@@ -39,4 +39,7 @@ interface WifiViewModelCommon {
/** True if the airplane spacer view should be visible. */
val isAirplaneSpacerVisible: Flow<Boolean>
/** True if the spacer between the wifi icon and the RAT icon should be visible. */
val isSignalSpacerVisible: Flow<Boolean>
}

View File

@@ -49,6 +49,8 @@ class FakeMobileIconsInteractor(
FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
)
private val interactorCache: MutableMap<Int, FakeMobileIconInteractor> = mutableMapOf()
override val isDefaultConnectionFailed = MutableStateFlow(false)
override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
@@ -75,7 +77,15 @@ class FakeMobileIconsInteractor(
/** Always returns a new fake interactor */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
return FakeMobileIconInteractor(tableLogBuffer)
return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it }
}
/**
* Returns the most recently created interactor for the given subId, or null if an interactor
* has never been created for that sub.
*/
fun getInteractorForSubId(subId: Int): FakeMobileIconInteractor? {
return interactorCache[subId]
}
companion object {

View File

@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
@@ -24,6 +25,7 @@ import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirp
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
@@ -135,8 +137,179 @@ class MobileIconsViewModelTest : SysuiTestCase() {
assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl)
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = emptyList()
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1)
// The unknown icon group doesn't show a RAT
interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1)
// The 3G icon group will show a RAT
interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1)
val sub1Interactor = interactor.getInteractorForSubId(1)!!
sub1Interactor.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
assertThat(latest).isTrue()
sub1Interactor.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
assertThat(latest).isFalse()
sub1Interactor.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.LTE)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
assertThat(latest).isTrue()
// WHEN the sub list gets new subscriptions where the last subscription is not showing
// the network type icon
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3)
interactor.getInteractorForSubId(3)!!.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
// THEN the flow updates
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() =
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this)
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
// Immediately switch the order so that we've created both interactors
interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
val sub1Interactor = interactor.getInteractorForSubId(1)!!
val sub2Interactor = interactor.getInteractorForSubId(2)!!
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
sub1Interactor.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN)
sub2Interactor.networkTypeIconGroup.value =
NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G)
assertThat(latest).isTrue()
// WHEN sub1 becomes last and sub1 has no network type icon
interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1)
// THEN the flow updates
assertThat(latest).isFalse()
// WHEN sub2 becomes last and sub2 has a network type icon
interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
// THEN the flow updates
assertThat(latest).isTrue()
job.cancel()
}
companion object {
private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false)
private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false)
private val SUB_3 = SubscriptionModel(subscriptionId = 3, isOpportunistic = false)
}
}

View File

@@ -50,6 +50,7 @@ import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -98,12 +99,12 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
val viewModelCommon =
WifiViewModel(
airplaneModeViewModel,
shouldShowSignalSpacerProvider = { MutableStateFlow(false) },
connectivityConstants,
context,
tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
wifiConstants,
)
viewModel =

View File

@@ -27,7 +27,6 @@ import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
@@ -46,6 +45,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
@@ -66,7 +66,6 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase
private lateinit var underTest: WifiViewModel
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var connectivityConstants: ConnectivityConstants
@Mock private lateinit var wifiConstants: WifiConstants
@@ -121,12 +120,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase
underTest =
WifiViewModel(
airplaneModeViewModel,
shouldShowSignalSpacerProvider = { MutableStateFlow(false) },
connectivityConstants,
context,
tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
wifiConstants,
)

View File

@@ -39,8 +39,8 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWi
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
@@ -53,7 +53,6 @@ import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class WifiViewModelTest : SysuiTestCase() {
@@ -68,6 +67,7 @@ class WifiViewModelTest : SysuiTestCase() {
private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor
private lateinit var airplaneModeViewModel: AirplaneModeViewModel
private val shouldShowSignalSpacerProviderFlow = MutableStateFlow(false)
private lateinit var scope: CoroutineScope
@Before
@@ -473,6 +473,34 @@ class WifiViewModelTest : SysuiTestCase() {
job.cancel()
}
@Test
fun signalSpacer_firstSubNotShowingNetworkTypeIcon_outputsFalse() =
runBlocking(IMMEDIATE) {
var latest: Boolean? = null
val job = underTest.isSignalSpacerVisible.onEach { latest = it }.launchIn(this)
shouldShowSignalSpacerProviderFlow.value = false
yield()
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun signalSpacer_firstSubIsShowingNetworkTypeIcon_outputsTrue() =
runBlocking(IMMEDIATE) {
var latest: Boolean? = null
val job = underTest.isSignalSpacerVisible.onEach { latest = it }.launchIn(this)
shouldShowSignalSpacerProviderFlow.value = true
yield()
assertThat(latest).isTrue()
job.cancel()
}
private fun createAndSetViewModel() {
// [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow
// creations rely on certain config values that we mock out in individual tests. This method
@@ -480,12 +508,12 @@ class WifiViewModelTest : SysuiTestCase() {
underTest =
WifiViewModel(
airplaneModeViewModel,
{ shouldShowSignalSpacerProviderFlow },
connectivityConstants,
context,
tableLogBuffer,
interactor,
scope,
statusBarPipelineFlags,
wifiConstants,
)
}