diff options
| author | 2022-10-11 21:46:17 +0000 | |
|---|---|---|
| committer | 2022-10-21 20:18:49 +0000 | |
| commit | 487ace16a445d7a9d6e02405e8d42d4669d9640a (patch) | |
| tree | 7cefb909ecea8801e3c94f2cd52d5a59f7d16b9d | |
| parent | b344c542bf422bd0cafab0c8a7ec4a361efe78e7 (diff) | |
[SB Refactor] Create an airplane mode repo/interactor/view model and use
it to determine when to show the wifi<->airplane spacer.
Bug: 238425913
Test: manual: Verify appropriate amount of space appears between wifi
icon and airplane mode icon
Test: statusbar.pipeline tests
Change-Id: Ie8fa9cca65919fd34eb02516449f14b38d465a4a
17 files changed, 682 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt new file mode 100644 index 000000000000..7aa5ee1389f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepository.kt @@ -0,0 +1,93 @@ +/* + * 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.airplane.data.repository + +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings.Global +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.dagger.qualifiers.Background +import com.android.systemui.qs.SettingObserver +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange +import com.android.systemui.util.settings.GlobalSettings +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.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Provides data related to airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. It is + * only used to help [com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel] + * determine what parts of the wifi icon view should be shown. + * + * TODO(b/238425913): Consider migrating the status bar airplane mode icon to use this repo. + */ +interface AirplaneModeRepository { + /** Observable for whether the device is currently in airplane mode. */ + val isAirplaneMode: StateFlow<Boolean> +} + +@SysUISingleton +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class AirplaneModeRepositoryImpl +@Inject +constructor( + @Background private val bgHandler: Handler, + private val globalSettings: GlobalSettings, + logger: ConnectivityPipelineLogger, + @Application scope: CoroutineScope, +) : AirplaneModeRepository { + // TODO(b/254848912): Replace this with a generic SettingObserver coroutine once we have it. + override val isAirplaneMode: StateFlow<Boolean> = + conflatedCallbackFlow { + val observer = + object : + SettingObserver( + globalSettings, + bgHandler, + Global.AIRPLANE_MODE_ON, + UserHandle.USER_ALL + ) { + override fun handleValueChanged(value: Int, observedChange: Boolean) { + trySend(value == 1) + } + } + + observer.isListening = true + trySend(observer.value == 1) + awaitClose { observer.isListening = false } + } + .distinctUntilChanged() + .logInputChange(logger, "isAirplaneMode") + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + // When the observer starts listening, the flow will emit the current value so the + // initialValue here is irrelevant. + initialValue = false, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt new file mode 100644 index 000000000000..3e9b2c2ae809 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractor.kt @@ -0,0 +1,46 @@ +/* + * 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.airplane.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * The business logic layer for airplane mode. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [AirplaneModeRepository] for more details. + */ +@SysUISingleton +class AirplaneModeInteractor +@Inject +constructor( + airplaneModeRepository: AirplaneModeRepository, + connectivityRepository: ConnectivityRepository, +) { + /** True if the device is currently in airplane mode. */ + val isAirplaneMode: Flow<Boolean> = airplaneModeRepository.isAirplaneMode + + /** True if we're configured to force-hide the airplane mode icon and false otherwise. */ + val isForceHidden: Flow<Boolean> = + connectivityRepository.forceHiddenSlots.map { it.contains(ConnectivitySlot.AIRPLANE) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt new file mode 100644 index 000000000000..fe30c0169021 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModel.kt @@ -0,0 +1,57 @@ +/* + * 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.airplane.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.stateIn + +/** + * Models the UI state for the status bar airplane mode icon. + * + * IMPORTANT: This is currently *not* used to render any airplane mode information anywhere. See + * [com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository] for + * more details. + */ +@SysUISingleton +class AirplaneModeViewModel +@Inject +constructor( + interactor: AirplaneModeInteractor, + logger: ConnectivityPipelineLogger, + @Application private val scope: CoroutineScope, +) { + /** True if the airplane mode icon is currently visible in the status bar. */ + val isAirplaneModeIconVisible: StateFlow<Boolean> = + combine(interactor.isAirplaneMode, interactor.isForceHidden) { + isAirplaneMode, + isAirplaneIconForceHidden -> + isAirplaneMode && !isAirplaneIconForceHidden + } + .distinctUntilChanged() + .logOutputChange(logger, "isAirplaneModeIconVisible") + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) +} 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 06d554232565..2aaa085645e4 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 @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository @@ -30,6 +32,9 @@ import dagger.Module @Module abstract class StatusBarPipelineModule { @Binds + abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository + + @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds 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 273be63eb8a2..25537b948517 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 @@ -91,6 +91,7 @@ object WifiViewBinder { val activityInView = view.requireViewById<ImageView>(R.id.wifi_in) 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) view.isVisible = true iconView.isVisible = true @@ -142,6 +143,12 @@ object WifiViewBinder { activityContainerView.isVisible = visible } } + + launch { + viewModel.isAirplaneSpacerVisible.distinctUntilChanged().collect { visible -> + airplaneSpacer.isVisible = visible + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt index 40f948f9ee6c..95ab251422b2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt @@ -32,6 +32,7 @@ class HomeWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -40,4 +41,5 @@ class HomeWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt index 9642ac42972e..86535d63f84f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt @@ -29,6 +29,7 @@ class KeyguardWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class KeyguardWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index cc6a375c40f1..7cbdf5dbdf2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -44,6 +44,9 @@ abstract class LocationBasedWifiViewModel( /** True if the activity container view should be visible. */ val isActivityContainerVisible: Flow<Boolean>, + + /** True if the airplane spacer view should be visible. */ + val isAirplaneSpacerVisible: Flow<Boolean>, ) { /** The color that should be used to tint the icon. */ val tint: Flow<Int> = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt index 0ddf90e21872..fd54c5f5062e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt @@ -29,6 +29,7 @@ class QsWifiViewModel( isActivityInViewVisible: Flow<Boolean>, isActivityOutViewVisible: Flow<Boolean>, isActivityContainerVisible: Flow<Boolean>, + isAirplaneSpacerVisible: Flow<Boolean>, ) : LocationBasedWifiViewModel( statusBarPipelineFlags, @@ -37,4 +38,5 @@ class QsWifiViewModel( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) 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 160c577042a4..89b96b7bc75d 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 @@ -31,6 +31,7 @@ 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.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange @@ -66,6 +67,7 @@ import kotlinx.coroutines.flow.stateIn class WifiViewModel @Inject constructor( + airplaneModeViewModel: AirplaneModeViewModel, connectivityConstants: ConnectivityConstants, private val context: Context, logger: ConnectivityPipelineLogger, @@ -177,6 +179,12 @@ constructor( } .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + // TODO(b/238425913): It isn't ideal for the wifi icon to need to know about whether the + // airplane icon is visible. Instead, we should have a parent StatusBarSystemIconsViewModel + // that appropriately knows about both icons and sets the padding appropriately. + private val isAirplaneSpacerVisible: Flow<Boolean> = + airplaneModeViewModel.isAirplaneModeIconVisible + /** A view model for the status bar on the home screen. */ val home: HomeWifiViewModel = HomeWifiViewModel( @@ -185,6 +193,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar on keyguard. */ @@ -195,6 +204,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) /** A view model for the status bar in quick settings. */ @@ -205,6 +215,7 @@ constructor( isActivityInViewVisible, isActivityOutViewVisible, isActivityContainerVisible, + isAirplaneSpacerVisible, ) companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt new file mode 100644 index 000000000000..b7a6c0125cfa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/AirplaneModeRepositoryImplTest.kt @@ -0,0 +1,116 @@ +/* + * 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.airplane.data.repository + +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings.Global +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.settings.FakeSettings +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.runBlocking +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class AirplaneModeRepositoryImplTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeRepositoryImpl + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var bgHandler: Handler + private lateinit var scope: CoroutineScope + private lateinit var settings: FakeSettings + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + bgHandler = Handler(Looper.getMainLooper()) + scope = CoroutineScope(IMMEDIATE) + settings = FakeSettings() + settings.userId = UserHandle.USER_ALL + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun isAirplaneMode_initiallyGetsSettingsValue() = + runBlocking(IMMEDIATE) { + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + + underTest = + AirplaneModeRepositoryImpl( + bgHandler, + settings, + logger, + scope, + ) + + val job = underTest.isAirplaneMode.launchIn(this) + + assertThat(underTest.isAirplaneMode.value).isTrue() + + job.cancel() + } + + @Test + fun isAirplaneMode_settingUpdated_valueUpdated() = + runBlocking(IMMEDIATE) { + val job = underTest.isAirplaneMode.launchIn(this) + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + settings.putInt(Global.AIRPLANE_MODE_ON, 1) + yield() + assertThat(underTest.isAirplaneMode.value).isTrue() + + settings.putInt(Global.AIRPLANE_MODE_ON, 0) + yield() + assertThat(underTest.isAirplaneMode.value).isFalse() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt new file mode 100644 index 000000000000..63bbdfca0071 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt @@ -0,0 +1,29 @@ +/* + * 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.airplane.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAirplaneModeRepository : AirplaneModeRepository { + private val _isAirplaneMode = MutableStateFlow(false) + override val isAirplaneMode: StateFlow<Boolean> = _isAirplaneMode + + fun setIsAirplaneMode(isAirplaneMode: Boolean) { + _isAirplaneMode.value = isAirplaneMode + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt new file mode 100644 index 000000000000..33a80e1a3dd6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorTest.kt @@ -0,0 +1,99 @@ +/* + * 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.airplane.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield +import org.junit.Before +import org.junit.Test + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeInteractorTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeInteractor + + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + + @Before + fun setUp() { + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + underTest = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + } + + @Test + fun isAirplaneMode_matchesRepo() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isAirplaneMode.onEach { latest = it }.launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + assertThat(latest).isFalse() + + airplaneModeRepository.setIsAirplaneMode(true) + yield() + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isForceHidden_repoHasWifiHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + + 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() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt new file mode 100644 index 000000000000..76016a121e68 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelTest.kt @@ -0,0 +1,110 @@ +/* + * 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.airplane.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +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.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot +import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +class AirplaneModeViewModelTest : SysuiTestCase() { + + private lateinit var underTest: AirplaneModeViewModel + + @Mock private lateinit var logger: ConnectivityPipelineLogger + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository + private lateinit var connectivityRepository: FakeConnectivityRepository + private lateinit var interactor: AirplaneModeInteractor + private lateinit var scope: CoroutineScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() + connectivityRepository = FakeConnectivityRepository() + interactor = AirplaneModeInteractor(airplaneModeRepository, connectivityRepository) + scope = CoroutineScope(IMMEDIATE) + + underTest = + AirplaneModeViewModel( + interactor, + logger, + scope, + ) + } + + @Test + fun isAirplaneModeIconVisible_notAirplaneMode_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(false) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_forceHidden_outputsFalse() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isAirplaneModeIconVisible_isAirplaneModeAndNotForceHidden_outputsTrue() = + runBlocking(IMMEDIATE) { + connectivityRepository.setForceHiddenIcons(setOf()) + airplaneModeRepository.setIsAirplaneMode(true) + + var latest: Boolean? = null + val job = underTest.isAirplaneModeIconVisible.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } +} + +private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 4efb13520ebf..c5841098010a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -30,6 +30,9 @@ import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.phone.StatusBarLocation 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 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository @@ -63,11 +66,13 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor private lateinit var viewModel: WifiViewModel private lateinit var scope: CoroutineScope + private lateinit var airplaneModeViewModel: AirplaneModeViewModel @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() @@ -77,12 +82,22 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(Dispatchers.Unconfined) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) viewModel = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 7686071206f9..a1afcd71e3c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -27,6 +27,9 @@ 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 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot @@ -64,19 +67,31 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = + AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) } @After @@ -102,6 +117,7 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase .thenReturn(testCase.hasDataCapabilities) underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, 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 79633d467b7d..7d2c56098584 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 @@ -20,8 +20,12 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon 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 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants 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.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository @@ -55,19 +59,31 @@ class WifiViewModelTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock private lateinit var wifiConstants: WifiConstants + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var wifiRepository: FakeWifiRepository private lateinit var interactor: WifiInteractor + private lateinit var airplaneModeViewModel: AirplaneModeViewModel private lateinit var scope: CoroutineScope @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() connectivityRepository = FakeConnectivityRepository() wifiRepository = FakeWifiRepository() wifiRepository.setIsWifiEnabled(true) interactor = WifiInteractor(connectivityRepository, wifiRepository) scope = CoroutineScope(IMMEDIATE) + airplaneModeViewModel = AirplaneModeViewModel( + AirplaneModeInteractor( + airplaneModeRepository, + connectivityRepository, + ), + logger, + scope, + ) + createAndSetViewModel() } @@ -462,11 +478,64 @@ class WifiViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun airplaneSpacer_notAirplaneMode_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneForceHidden_outputsFalse() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(true) + connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.AIRPLANE)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun airplaneSpacer_airplaneIconVisible_outputsTrue() = runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest + .qs + .isAirplaneSpacerVisible + .onEach { latest = it } + .launchIn(this) + + airplaneModeRepository.setIsAirplaneMode(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 // allows tests to create the view model only after those configs are correctly set up. underTest = WifiViewModel( + airplaneModeViewModel, connectivityConstants, context, logger, |