Merge "[SB Refactor] Implement the TunerService callback for the icon hide list and pipe it through to the wifi icon." into tm-qpr-dev

This commit is contained in:
Caitlin Shkuratov
2022-09-08 14:01:35 +00:00
committed by Android (Google) Code Review
11 changed files with 735 additions and 94 deletions

View File

@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.dagger
import com.android.systemui.CoreStartable
import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
import dagger.Binds
@@ -33,6 +35,9 @@ abstract class StatusBarPipelineModule {
@ClassKey(ConnectivityInfoProcessor::class)
abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable
@Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds
abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
}

View File

@@ -34,7 +34,7 @@ class ConnectivityPipelineLogger @Inject constructor(
/**
* Logs a change in one of the **raw inputs** to the connectivity pipeline.
*/
fun logInputChange(callbackName: String, changeInfo: String) {
fun logInputChange(callbackName: String, changeInfo: String?) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,

View File

@@ -0,0 +1,56 @@
/*
* 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.shared.data.model
import android.content.Context
import com.android.internal.R
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
/**
* A container for all the different types of connectivity slots: wifi, mobile, etc.
*/
@SysUISingleton
class ConnectivitySlots @Inject constructor(context: Context) {
private val airplaneSlot: String = context.getString(R.string.status_bar_airplane)
private val mobileSlot: String = context.getString(R.string.status_bar_mobile)
private val wifiSlot: String = context.getString(R.string.status_bar_wifi)
private val ethernetSlot: String = context.getString(R.string.status_bar_ethernet)
private val slotByName: Map<String, ConnectivitySlot> = mapOf(
airplaneSlot to ConnectivitySlot.AIRPLANE,
mobileSlot to ConnectivitySlot.MOBILE,
wifiSlot to ConnectivitySlot.WIFI,
ethernetSlot to ConnectivitySlot.ETHERNET
)
/**
* Given a string name of a slot, returns the instance of [ConnectivitySlot] that it corresponds
* to, or null if we couldn't find that slot name.
*/
fun getSlotFromName(slotName: String): ConnectivitySlot? {
return slotByName[slotName]
}
}
/** The different types of connectivity slots. */
enum class ConnectivitySlot {
AIRPLANE,
ETHERNET,
MOBILE,
WIFI,
}

View File

@@ -0,0 +1,120 @@
/*
* 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.shared.data.repository
import android.content.Context
import androidx.annotation.ArrayRes
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
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.dump.DumpManager
import com.android.systemui.statusbar.phone.StatusBarIconController
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.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
import com.android.systemui.tuner.TunerService
import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
/**
* Provides data related to the connectivity state that needs to be shared across multiple different
* types of connectivity (wifi, mobile, ethernet, etc.)
*/
interface ConnectivityRepository {
/**
* Observable for the current set of connectivity icons that should be force-hidden.
*/
val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>>
}
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class ConnectivityRepositoryImpl @Inject constructor(
private val connectivitySlots: ConnectivitySlots,
context: Context,
dumpManager: DumpManager,
logger: ConnectivityPipelineLogger,
@Application scope: CoroutineScope,
tunerService: TunerService,
) : ConnectivityRepository, Dumpable {
init {
dumpManager.registerDumpable("$SB_LOGGING_TAG:ConnectivityRepository", this)
}
// The default set of hidden icons to use if we don't get any from [TunerService].
private val defaultHiddenIcons: Set<ConnectivitySlot> =
context.resources.getStringArray(DEFAULT_HIDDEN_ICONS_RESOURCE)
.asList()
.toSlotSet(connectivitySlots)
override val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>> = conflatedCallbackFlow {
val callback = object : TunerService.Tunable {
override fun onTuningChanged(key: String, newHideList: String?) {
if (key != HIDDEN_ICONS_TUNABLE_KEY) {
return
}
logger.logInputChange("onTuningChanged", newHideList)
val outputList = newHideList?.split(",")?.toSlotSet(connectivitySlots)
?: defaultHiddenIcons
trySend(outputList)
}
}
tunerService.addTunable(callback, HIDDEN_ICONS_TUNABLE_KEY)
awaitClose { tunerService.removeTunable(callback) }
}
.stateIn(
scope,
started = SharingStarted.WhileSubscribed(),
initialValue = defaultHiddenIcons
)
override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply {
println("defaultHiddenIcons=$defaultHiddenIcons")
}
}
companion object {
@VisibleForTesting
internal const val HIDDEN_ICONS_TUNABLE_KEY = StatusBarIconController.ICON_HIDE_LIST
@VisibleForTesting
@ArrayRes
internal val DEFAULT_HIDDEN_ICONS_RESOURCE = R.array.config_statusBarIconsToExclude
/** Converts a list of string slot names to a set of [ConnectivitySlot] instances. */
private fun List<String>.toSlotSet(
connectivitySlots: ConnectivitySlots
): Set<ConnectivitySlot> {
return this
.filter { it.isNotBlank() }
.mapNotNull { connectivitySlots.getSlotFromName(it) }
.toSet()
}
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
import android.net.wifi.WifiManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import javax.inject.Inject
@@ -33,9 +35,10 @@ import kotlinx.coroutines.flow.map
*/
@SysUISingleton
class WifiInteractor @Inject constructor(
repository: WifiRepository,
connectivityRepository: ConnectivityRepository,
wifiRepository: WifiRepository,
) {
private val ssid: Flow<String?> = repository.wifiNetwork.map { info ->
private val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
when (info) {
is WifiNetworkModel.Inactive -> null
is WifiNetworkModel.CarrierMerged -> null
@@ -49,10 +52,16 @@ class WifiInteractor @Inject constructor(
}
/** Our current wifi network. See [WifiNetworkModel]. */
val wifiNetwork: Flow<WifiNetworkModel> = repository.wifiNetwork
val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
/** True if we're configured to force-hide the wifi icon and false otherwise. */
val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
it.contains(ConnectivitySlot.WIFI)
}
/** True if our wifi network has activity in (download), and false otherwise. */
val hasActivityIn: Flow<Boolean> = combine(repository.wifiActivity, ssid) { activity, ssid ->
val hasActivityIn: Flow<Boolean> =
combine(wifiRepository.wifiActivity, ssid) { activity, ssid ->
activity.hasActivityIn && ssid != null
}
}

View File

@@ -24,6 +24,7 @@ import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import kotlinx.coroutines.InternalCoroutinesApi
@@ -54,14 +55,15 @@ object WifiViewBinder {
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.wifiIconResId.distinctUntilChanged().collect { iconResId ->
iconView.setImageDrawable(
if (iconResId != null && iconResId > 0) {
iconView.context.getDrawable(iconResId)
} else {
null
}
)
viewModel.wifiIcon.distinctUntilChanged().collect { wifiIcon ->
// TODO(b/238425913): Right now, if !isVisible, there's just an empty space
// where the wifi icon would be. We need to pipe isVisible through to
// [ModernStatusBarWifiView.isIconVisible], which is what actually makes
// the view GONE.
view.isVisible = wifiIcon != null
wifiIcon?.let {
IconViewBinder.bind(wifiIcon, iconView)
}
}
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.graphics.Color
import androidx.annotation.DrawableRes
import com.android.systemui.common.shared.model.Icon
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
@@ -29,6 +30,7 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -46,7 +48,7 @@ class WifiViewModel @Inject constructor(
* The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon.
*/
@DrawableRes
val wifiIconResId: Flow<Int?> = interactor.wifiNetwork.map {
private val iconResId: Flow<Int?> = interactor.wifiNetwork.map {
when (it) {
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
@@ -59,6 +61,24 @@ class WifiViewModel @Inject constructor(
}
}
/**
* The wifi icon that should be displayed. Null if we shouldn't display any icon.
*/
val wifiIcon: Flow<Icon?> = combine(
interactor.isForceHidden,
iconResId
) { isForceHidden, iconResId ->
when {
isForceHidden ||
iconResId == null ||
iconResId <= 0 -> null
else -> Icon.Resource(iconResId)
}
}
/**
* True if the activity in icon should be displayed and false otherwise.
*/
val isActivityInVisible: Flow<Boolean>
get() =
if (!constants.shouldShowActivityConfig) {

View File

@@ -0,0 +1,295 @@
/*
* 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.shared.data.repository
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.DEFAULT_HIDDEN_ICONS_RESOURCE
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.HIDDEN_ICONS_TUNABLE_KEY
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
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.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class ConnectivityRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: ConnectivityRepositoryImpl
@Mock private lateinit var connectivitySlots: ConnectivitySlots
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
private lateinit var scope: CoroutineScope
@Mock private lateinit var tunerService: TunerService
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
scope = CoroutineScope(IMMEDIATE)
underTest = ConnectivityRepositoryImpl(
connectivitySlots,
context,
dumpManager,
logger,
scope,
tunerService,
)
}
@After
fun tearDown() {
scope.cancel()
}
@Test
fun forceHiddenSlots_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
context.getOrCreateTestableResources().addOverride(
DEFAULT_HIDDEN_ICONS_RESOURCE,
arrayOf(SLOT_WIFI, SLOT_ETHERNET)
)
// Re-create our [ConnectivityRepositoryImpl], since it fetches
// config_statusBarIconsToExclude when it's first constructed
underTest = ConnectivityRepositoryImpl(
connectivitySlots,
context,
dumpManager,
logger,
scope,
tunerService,
)
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
job.cancel()
}
@Test
fun forceHiddenSlots_slotNamesAdded_flowHasSlots() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
job.cancel()
}
@Test
fun forceHiddenSlots_wrongKey_doesNotUpdate() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
// WHEN onTuningChanged with the wrong key
getTunable().onTuningChanged("wrongKey", SLOT_WIFI)
yield()
// THEN we didn't update our value and still have the old one
assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
job.cancel()
}
@Test
fun forceHiddenSlots_slotNamesAddedThenNull_flowHasDefault() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
context.getOrCreateTestableResources().addOverride(
DEFAULT_HIDDEN_ICONS_RESOURCE,
arrayOf(SLOT_WIFI, SLOT_ETHERNET)
)
// Re-create our [ConnectivityRepositoryImpl], since it fetches
// config_statusBarIconsToExclude when it's first constructed
underTest = ConnectivityRepositoryImpl(
connectivitySlots,
context,
dumpManager,
logger,
scope,
tunerService,
)
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
// First, update the slots
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, SLOT_MOBILE)
assertThat(latest).containsExactly(ConnectivitySlot.MOBILE)
// WHEN we update to a null value
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, null)
yield()
// THEN we go back to our default value
assertThat(latest).containsExactly(ConnectivitySlot.ETHERNET, ConnectivitySlot.WIFI)
job.cancel()
}
@Test
fun forceHiddenSlots_someInvalidSlotNames_flowHasValidSlotsOnly() = runBlocking(IMMEDIATE) {
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
whenever(connectivitySlots.getSlotFromName(SLOT_WIFI))
.thenReturn(ConnectivitySlot.WIFI)
whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_MOBILE")
assertThat(latest).containsExactly(ConnectivitySlot.WIFI)
job.cancel()
}
@Test
fun forceHiddenSlots_someEmptySlotNames_flowHasValidSlotsOnly() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
// WHEN there's empty and blank slot names
getTunable().onTuningChanged(
HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_MOBILE, ,,$SLOT_WIFI"
)
// THEN we skip that slot but still process the other ones
assertThat(latest).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.MOBILE)
job.cancel()
}
@Test
fun forceHiddenSlots_allInvalidOrEmptySlotNames_flowHasEmpty() = runBlocking(IMMEDIATE) {
var latest: Set<ConnectivitySlot>? = null
val job = underTest
.forceHiddenSlots
.onEach { latest = it }
.launchIn(this)
whenever(connectivitySlots.getSlotFromName(SLOT_WIFI)).thenReturn(null)
whenever(connectivitySlots.getSlotFromName(SLOT_ETHERNET)).thenReturn(null)
whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE)).thenReturn(null)
getTunable().onTuningChanged(
HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_MOBILE,,$SLOT_WIFI,$SLOT_ETHERNET,,,"
)
assertThat(latest).isEmpty()
job.cancel()
}
@Test
fun forceHiddenSlots_newSubscriberGetsCurrentValue() = runBlocking(IMMEDIATE) {
setUpEthernetWifiMobileSlotNames()
var latest1: Set<ConnectivitySlot>? = null
val job1 = underTest
.forceHiddenSlots
.onEach { latest1 = it }
.launchIn(this)
getTunable().onTuningChanged(HIDDEN_ICONS_TUNABLE_KEY, "$SLOT_WIFI,$SLOT_ETHERNET")
assertThat(latest1).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
// WHEN we add a second subscriber after having already emitted a value
var latest2: Set<ConnectivitySlot>? = null
val job2 = underTest
.forceHiddenSlots
.onEach { latest2 = it }
.launchIn(this)
// THEN the second subscribe receives the already-emitted value
assertThat(latest2).containsExactly(ConnectivitySlot.WIFI, ConnectivitySlot.ETHERNET)
job1.cancel()
job2.cancel()
}
private fun getTunable(): TunerService.Tunable {
val callbackCaptor = argumentCaptor<TunerService.Tunable>()
Mockito.verify(tunerService).addTunable(callbackCaptor.capture(), any())
return callbackCaptor.value!!
}
private fun setUpEthernetWifiMobileSlotNames() {
whenever(connectivitySlots.getSlotFromName(SLOT_ETHERNET))
.thenReturn(ConnectivitySlot.ETHERNET)
whenever(connectivitySlots.getSlotFromName(SLOT_WIFI))
.thenReturn(ConnectivitySlot.WIFI)
whenever(connectivitySlots.getSlotFromName(SLOT_MOBILE))
.thenReturn(ConnectivitySlot.MOBILE)
}
companion object {
private const val SLOT_ETHERNET = "ethernet"
private const val SLOT_WIFI = "wifi"
private const val SLOT_MOBILE = "mobile"
private val IMMEDIATE = Dispatchers.Main.immediate
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.shared.data.repository
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/** Fake implementation of [ConnectivityRepository] exposing set methods for all the flows. */
class FakeConnectivityRepository : ConnectivityRepository {
private val _forceHiddenIcons: MutableStateFlow<Set<ConnectivitySlot>> =
MutableStateFlow(emptySet())
override val forceHiddenSlots: StateFlow<Set<ConnectivitySlot>> = _forceHiddenIcons
fun setForceHiddenIcons(hiddenIcons: Set<ConnectivitySlot>) {
_forceHiddenIcons.value = hiddenIcons
}
}

View File

@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
@@ -37,18 +39,22 @@ class WifiInteractorTest : SysuiTestCase() {
private lateinit var underTest: WifiInteractor
private lateinit var repository: FakeWifiRepository
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository
@Before
fun setUp() {
repository = FakeWifiRepository()
underTest = WifiInteractor(repository)
connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository()
underTest = WifiInteractor(connectivityRepository, wifiRepository)
}
@Test
fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
)
var latest: Boolean? = null
val job = underTest
@@ -63,8 +69,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
@@ -79,8 +87,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
var latest: Boolean? = null
val job = underTest
@@ -95,8 +105,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
@@ -111,8 +123,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null))
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
@@ -127,8 +141,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_inactiveNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.Inactive)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
@@ -143,8 +159,10 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_carrierMergedNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
@@ -159,7 +177,7 @@ class WifiInteractorTest : SysuiTestCase() {
@Test
fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
var latest: Boolean? = null
val job = underTest
@@ -168,23 +186,33 @@ class WifiInteractorTest : SysuiTestCase() {
.launchIn(this)
// Conduct a series of changes and verify we catch each of them in succession
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield()
assertThat(latest).isTrue()
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
)
yield()
assertThat(latest).isFalse()
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
yield()
assertThat(latest).isTrue()
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield()
assertThat(latest).isTrue()
repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
)
yield()
assertThat(latest).isFalse()
@@ -200,7 +228,7 @@ class WifiInteractorTest : SysuiTestCase() {
ssid = "AB",
passpointProviderFriendlyName = "friendly"
)
repository.setWifiNetwork(wifiNetwork)
wifiRepository.setWifiNetwork(wifiNetwork)
var latest: WifiNetworkModel? = null
val job = underTest
@@ -213,6 +241,36 @@ class WifiInteractorTest : SysuiTestCase() {
job.cancel()
}
@Test
fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
var latest: Boolean? = null
val job = underTest
.isForceHidden
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun isForceHidden_repoDoesNotHaveWifiHidden_outputsFalse() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf())
var latest: Boolean? = null
val job = underTest
.isForceHidden
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isFalse()
job.cancel()
}
companion object {
val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
}

View File

@@ -18,11 +18,14 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
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.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
@@ -50,14 +53,16 @@ class WifiViewModelTest : SysuiTestCase() {
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var constants: WifiConstants
private lateinit var repository: FakeWifiRepository
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
repository = FakeWifiRepository()
interactor = WifiInteractor(repository)
connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository()
interactor = WifiInteractor(connectivityRepository, wifiRepository)
underTest = WifiViewModel(
statusBarPipelineFlags,
@@ -68,42 +73,13 @@ class WifiViewModelTest : SysuiTestCase() {
}
@Test
fun wifiIconResId_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.Inactive)
fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
var latest: Int? = null
var latest: Icon? = null
val job = underTest
.wifiIconResId
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo(WIFI_NO_NETWORK)
job.cancel()
}
@Test
fun wifiIconResId_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
var latest: Int? = null
val job = underTest
.wifiIconResId
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isNull()
job.cancel()
}
@Test
fun wifiIconResId_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) {
repository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
var latest: Int? = null
val job = underTest
.wifiIconResId
.wifiIcon
.onEach { latest = it }
.launchIn(this)
@@ -113,10 +89,72 @@ class WifiViewModelTest : SysuiTestCase() {
}
@Test
fun wifiIconResId_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf())
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
var latest: Icon? = null
val job = underTest
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
job.cancel()
}
@Test
fun wifiIcon_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
var latest: Icon? = null
val job = underTest
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
assertThat((latest as Icon.Resource).res).isEqualTo(WIFI_NO_NETWORK)
job.cancel()
}
@Test
fun wifiIcon_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
var latest: Icon? = null
val job = underTest
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isNull()
job.cancel()
}
@Test
fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
var latest: Icon? = null
val job = underTest
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isNull()
job.cancel()
}
@Test
fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
val level = 1
repository.setWifiNetwork(
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
@@ -124,22 +162,23 @@ class WifiViewModelTest : SysuiTestCase() {
)
)
var latest: Int? = null
var latest: Icon? = null
val job = underTest
.wifiIconResId
.onEach { latest = it }
.launchIn(this)
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo(WIFI_FULL_ICONS[level])
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
assertThat((latest as Icon.Resource).res).isEqualTo(WIFI_FULL_ICONS[level])
job.cancel()
}
@Test
fun wifiIconResId_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) {
fun wifiIcon_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) {
val level = 4
repository.setWifiNetwork(
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = false,
@@ -147,13 +186,14 @@ class WifiViewModelTest : SysuiTestCase() {
)
)
var latest: Int? = null
var latest: Icon? = null
val job = underTest
.wifiIconResId
.onEach { latest = it }
.launchIn(this)
.wifiIcon
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
assertThat((latest as Icon.Resource).res).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
job.cancel()
}
@@ -161,7 +201,7 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun activityInVisible_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false)
repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
@@ -178,7 +218,7 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false)
repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
@@ -187,7 +227,9 @@ class WifiViewModelTest : SysuiTestCase() {
.launchIn(this)
// Update the repo to have activityIn
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield()
// Verify that we didn't update to activityIn=true (because our config is false)
@@ -199,7 +241,7 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun activityInVisible_showActivityConfigTrue_outputsUpdate() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
@@ -208,7 +250,9 @@ class WifiViewModelTest : SysuiTestCase() {
.launchIn(this)
// Update the repo to have activityIn
repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield()
// Verify that we updated to activityIn=true