Merge "[SB Refactor] Create an airplane mode repo/interactor/view model and use it to determine when to show the wifi<->airplane spacer." into tm-qpr-dev
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 0000000..7aa5ee1
--- /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 0000000..3e9b2c2
--- /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 0000000..fe30c01
--- /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 06d5542..2aaa085 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 @@
 @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 273be63..25537b9 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 @@
         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 @@
                         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 40f948f..95ab251 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 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -40,4 +41,5 @@
         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 9642ac4..86535d6 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 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         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 cc6a375..7cbdf5d 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 @@
 
     /** 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 0ddf90e..fd54c5f 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 @@
     isActivityInViewVisible: Flow<Boolean>,
     isActivityOutViewVisible: Flow<Boolean>,
     isActivityContainerVisible: Flow<Boolean>,
+    isAirplaneSpacerVisible: Flow<Boolean>,
 ) :
     LocationBasedWifiViewModel(
         statusBarPipelineFlags,
@@ -37,4 +38,5 @@
         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 160c577..89b96b7 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_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 @@
 class WifiViewModel
 @Inject
 constructor(
+    airplaneModeViewModel: AirplaneModeViewModel,
     connectivityConstants: ConnectivityConstants,
     private val context: Context,
     logger: ConnectivityPipelineLogger,
@@ -177,6 +179,12 @@
                 }
              .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 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar on keyguard. */
@@ -195,6 +204,7 @@
             isActivityInViewVisible,
             isActivityOutViewVisible,
             isActivityContainerVisible,
+            isAirplaneSpacerVisible,
         )
 
     /** A view model for the status bar in quick settings. */
@@ -205,6 +215,7 @@
             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 0000000..b7a6c01
--- /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 0000000..63bbdfc
--- /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 0000000..33a80e1
--- /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 0000000..76016a1
--- /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 4efb135..c584109 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_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 @@
     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 @@
         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 7686071..a1afcd7 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_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 @@
     @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 @@
                 .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 79633d4..7d2c560 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 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 @@
     @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 @@
         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,