diff options
11 files changed, 716 insertions, 75 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 88eccb5ba47f..681dc6fc13a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -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 @@ -34,5 +36,8 @@ abstract class StatusBarPipelineModule { abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable @Binds + abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository + + @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index 2a89309f7200..88d8a86d39f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/ConnectivitySlots.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/ConnectivitySlots.kt new file mode 100644 index 000000000000..d52d0fb91b82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/model/ConnectivitySlots.kt @@ -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, +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt new file mode 100644 index 000000000000..6b1750d11562 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt @@ -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() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index afe19afff55f..952525d243f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -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 } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index 7607ddf4fe8c..4fad3274d12f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -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) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 4fdcc44f1802..1987528319cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -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) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt new file mode 100644 index 000000000000..6dbee2f26ff9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt @@ -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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt new file mode 100644 index 000000000000..bd70034b13de --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt @@ -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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt index 9d8b4bcabc8a..e896749d9a94 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt @@ -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") } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index f0a775b52297..6c6d9e1e2311 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -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,75 @@ 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) + .wifiIcon + .onEach { latest = it } + .launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } - assertThat(latest).isEqualTo(WIFI_NO_NETWORK) + @Test + 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 wifiIconResId_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { - repository.setWifiNetwork(WifiNetworkModel.CarrierMerged) + fun wifiIcon_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) - var latest: Int? = null + var latest: Icon? = null val job = underTest - .wifiIconResId + .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 wifiIconResId_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) { - repository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null)) + fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null)) - var latest: Int? = null + var latest: Icon? = null val job = underTest - .wifiIconResId + .wifiIcon .onEach { latest = it } .launchIn(this) @@ -113,10 +151,10 @@ class WifiViewModelTest : SysuiTestCase() { } @Test - fun wifiIconResId_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) { + 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 |