summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt128
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt548
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt113
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt76
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt275
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt36
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt24
9 files changed, 1313 insertions, 0 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
new file mode 100644
index 000000000000..39755bf8d764
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain
+
+import android.graphics.drawable.TestStubDrawable
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val internetTileConfig = kosmos.qsInternetTileConfig
+ private val mapper by lazy {
+ InternetTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.ic_qs_no_internet_unavailable, TestStubDrawable())
+ addOverride(wifiRes, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ context
+ )
+ }
+
+ @Test
+ fun withActiveModel_mappedStateMatchesDataModel() {
+ val inputModel =
+ InternetTileModel.Active(
+ secondaryLabel = Text.Resource(R.string.quick_settings_networks_available),
+ iconId = wifiRes,
+ stateDescription = null,
+ contentDescription =
+ ContentDescription.Resource(R.string.quick_settings_internet_label),
+ )
+
+ val outputState = mapper.map(internetTileConfig, inputModel)
+
+ val expectedState =
+ createInternetTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(R.string.quick_settings_networks_available),
+ Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
+ context.getString(R.string.quick_settings_internet_label)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun withInactiveModel_mappedStateMatchesDataModel() {
+ val inputModel =
+ InternetTileModel.Inactive(
+ secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+ iconId = R.drawable.ic_qs_no_internet_unavailable,
+ stateDescription = null,
+ contentDescription =
+ ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+ )
+
+ val outputState = mapper.map(internetTileConfig, inputModel)
+
+ val expectedState =
+ createInternetTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(R.string.quick_settings_networks_unavailable),
+ Icon.Loaded(
+ context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
+ contentDescription = null
+ ),
+ context.getString(R.string.quick_settings_networks_unavailable)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createInternetTileState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String,
+ icon: Icon,
+ contentDescription: String,
+ ): QSTileState {
+ val label = context.getString(R.string.quick_settings_internet_label)
+ return QSTileState(
+ { icon },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ contentDescription,
+ null,
+ QSTileState.SideViewIcon.Chevron,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+
+ private companion object {
+ val wifiRes = WIFI_FULL_ICONS[4]
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
new file mode 100644
index 000000000000..37ef6ad17a64
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -0,0 +1,548 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain.interactor
+
+import android.graphics.drawable.TestStubDrawable
+import android.os.UserHandle
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.AccessibilityContentDescriptions
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.Flags
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
+import com.android.systemui.util.CarrierConfigTracker
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@TestableLooper.RunWithLooper
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InternetTileDataInteractorTest : SysuiTestCase() {
+ private val testUser = UserHandle.of(1)
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var underTest: InternetTileDataInteractor
+ private lateinit var mobileIconsInteractor: MobileIconsInteractor
+
+ private val airplaneModeRepository = FakeAirplaneModeRepository()
+ private val connectivityRepository = FakeConnectivityRepository()
+ private val ethernetInteractor = EthernetInteractor(connectivityRepository)
+ private val wifiRepository = FakeWifiRepository()
+ private val userSetupRepo = FakeUserSetupRepository()
+ private val wifiInteractor =
+ WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
+
+ private val tableLogBuffer: TableLogBuffer = mock()
+ private val carrierConfigTracker: CarrierConfigTracker = mock()
+
+ private val mobileConnectionsRepository =
+ FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
+ private val mobileConnectionRepository =
+ FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer)
+
+ private val flags =
+ FakeFeatureFlagsClassic().also {
+ it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
+ }
+
+ private val internet = context.getString(R.string.quick_settings_internet_label)
+
+ @Before
+ fun setUp() {
+ mobileConnectionRepository.apply {
+ setNetworkTypeKey(mobileConnectionsRepository.GSM_KEY)
+ isInService.value = true
+ dataConnectionState.value = DataConnectionState.Connected
+ dataEnabled.value = true
+ }
+
+ mobileConnectionsRepository.apply {
+ activeMobileDataRepository.value = mobileConnectionRepository
+ activeMobileDataSubscriptionId.value = SUB_1_ID
+ setMobileConnectionRepositoryMap(mapOf(SUB_1_ID to mobileConnectionRepository))
+ }
+
+ mobileIconsInteractor =
+ MobileIconsInteractorImpl(
+ mobileConnectionsRepository,
+ carrierConfigTracker,
+ tableLogBuffer,
+ connectivityRepository,
+ userSetupRepo,
+ testScope.backgroundScope,
+ context,
+ flags,
+ )
+
+ context.orCreateTestableResources.apply {
+ addOverride(com.android.internal.R.drawable.ic_signal_cellular, TestStubDrawable())
+ addOverride(
+ com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_0,
+ TestStubDrawable()
+ )
+ addOverride(
+ com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_1,
+ TestStubDrawable()
+ )
+ addOverride(
+ com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_2,
+ TestStubDrawable()
+ )
+ addOverride(
+ com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_3,
+ TestStubDrawable()
+ )
+ addOverride(
+ com.android.settingslib.R.drawable.ic_no_internet_wifi_signal_4,
+ TestStubDrawable()
+ )
+ addOverride(com.android.settingslib.R.drawable.ic_hotspot_phone, TestStubDrawable())
+ addOverride(com.android.settingslib.R.drawable.ic_hotspot_laptop, TestStubDrawable())
+ addOverride(com.android.settingslib.R.drawable.ic_hotspot_tablet, TestStubDrawable())
+ addOverride(com.android.settingslib.R.drawable.ic_hotspot_watch, TestStubDrawable())
+ addOverride(com.android.settingslib.R.drawable.ic_hotspot_auto, TestStubDrawable())
+ }
+
+ underTest =
+ InternetTileDataInteractor(
+ context,
+ testScope.backgroundScope,
+ airplaneModeRepository,
+ connectivityRepository,
+ ethernetInteractor,
+ mobileIconsInteractor,
+ wifiInteractor,
+ )
+ }
+
+ @Test
+ fun noDefault_noNetworksAvailable() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ connectivityRepository.defaultConnections.value = DefaultConnectionModel()
+
+ assertThat(latest?.secondaryLabel)
+ .isEqualTo(Text.Resource(R.string.quick_settings_networks_unavailable))
+ assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_unavailable)
+ }
+
+ @Test
+ fun noDefault_networksAvailable() =
+ testScope.runTest {
+ // TODO(b/328419203): support [WifiInteractor.areNetworksAvailable]
+ }
+
+ @Test
+ fun wifiDefaultAndActive() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ val networkModel =
+ WifiNetworkModel.Active(
+ networkId = 1,
+ level = 4,
+ ssid = "test ssid",
+ )
+ val wifiIcon =
+ WifiIcon.fromModel(model = networkModel, context = context, showHotspotInfo = true)
+ as WifiIcon.Visible
+
+ connectivityRepository.setWifiConnected()
+ wifiRepository.setIsWifiDefault(true)
+ wifiRepository.setWifiNetwork(networkModel)
+
+ assertThat(latest?.secondaryTitle).isEqualTo("test ssid")
+ assertThat(latest?.secondaryLabel).isNull()
+ val expectedIcon =
+ Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+
+ val actualIcon = latest?.icon
+ assertThat(actualIcon).isEqualTo(expectedIcon)
+ assertThat(latest?.iconId).isNull()
+ assertThat(latest?.contentDescription.loadContentDescription(context))
+ .isEqualTo("$internet,test ssid")
+ val expectedSd = wifiIcon.contentDescription
+ assertThat(latest?.stateDescription).isEqualTo(expectedSd)
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotNone() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ val networkModel =
+ WifiNetworkModel.Active(
+ networkId = 1,
+ level = 4,
+ ssid = "test ssid",
+ hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
+ )
+
+ connectivityRepository.setWifiConnected()
+ wifiRepository.setIsWifiDefault(true)
+ wifiRepository.setWifiNetwork(networkModel)
+
+ val expectedIcon =
+ Icon.Loaded(context.getDrawable(WifiIcons.WIFI_NO_INTERNET_ICONS[4])!!, null)
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .doesNotContain(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotTablet() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.TABLET)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_tablet)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotLaptop() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.LAPTOP)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_laptop)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotWatch() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.WATCH)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_watch)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotAuto() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.AUTO)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_auto)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotPhone() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.PHONE)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotUnknown() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.UNKNOWN)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndActive_hotspotInvalid() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ setWifiNetworkWithHotspot(WifiNetworkModel.HotspotDeviceType.INVALID)
+
+ val expectedIcon =
+ Icon.Loaded(
+ context.getDrawable(com.android.settingslib.R.drawable.ic_hotspot_phone)!!,
+ null
+ )
+ assertThat(latest?.icon).isEqualTo(expectedIcon)
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(
+ context.getString(AccessibilityContentDescriptions.WIFI_OTHER_DEVICE_CONNECTION)
+ )
+ }
+
+ @Test
+ fun wifiDefaultAndNotActive_noNetworksAvailable() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ val networkModel = WifiNetworkModel.Inactive
+
+ connectivityRepository.setWifiConnected(validated = false)
+ wifiRepository.setIsWifiDefault(true)
+ wifiRepository.setWifiNetwork(networkModel)
+ wifiRepository.wifiScanResults.value = emptyList()
+
+ assertThat(latest).isEqualTo(NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+ }
+
+ @Test
+ fun wifiDefaultAndNotActive_networksAvailable() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ val networkModel = WifiNetworkModel.Inactive
+
+ connectivityRepository.setWifiConnected(validated = false)
+ wifiRepository.setIsWifiDefault(true)
+ wifiRepository.setWifiNetwork(networkModel)
+ wifiRepository.wifiScanResults.value = listOf(WifiScanEntry("test 1"))
+
+ assertThat(latest?.secondaryLabel).isNull()
+ assertThat(latest?.secondaryTitle)
+ .isEqualTo(context.getString(R.string.quick_settings_networks_available))
+ assertThat(latest?.icon).isNull()
+ assertThat(latest?.iconId).isEqualTo(R.drawable.ic_qs_no_internet_available)
+ assertThat(latest?.stateDescription).isNull()
+ val expectedCd =
+ "$internet,${context.getString(R.string.quick_settings_networks_available)}"
+ assertThat(latest?.contentDescription.loadContentDescription(context))
+ .isEqualTo(expectedCd)
+ }
+
+ @Test
+ fun mobileDefault_usesNetworkNameAndIcon() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+
+ connectivityRepository.setMobileConnected()
+ mobileConnectionsRepository.mobileIsDefault.value = true
+ mobileConnectionRepository.apply {
+ setAllLevels(3)
+ setAllRoaming(false)
+ networkName.value = NetworkNameModel.Default("test network")
+ }
+
+ assertThat(latest).isNotNull()
+ assertThat(latest?.secondaryTitle).isNotNull()
+ assertThat(latest?.secondaryTitle.toString()).contains("test network")
+ assertThat(latest?.secondaryLabel).isNull()
+ assertThat(latest?.icon).isInstanceOf(Icon.Loaded::class.java)
+ assertThat(latest?.iconId).isNull()
+ assertThat(latest?.stateDescription.loadContentDescription(context))
+ .isEqualTo(latest?.secondaryTitle.toString())
+ assertThat(latest?.contentDescription.loadContentDescription(context))
+ .isEqualTo(internet)
+ }
+
+ @Test
+ fun ethernetDefault_validated_matchesInteractor() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+ connectivityRepository.setEthernetConnected(default = true, validated = true)
+
+ assertThat(latest?.secondaryLabel.loadText(context))
+ .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+ assertThat(latest?.secondaryTitle).isNull()
+ assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet_fully)
+ assertThat(latest?.icon).isNull()
+ assertThat(latest?.stateDescription).isNull()
+ assertThat(latest?.contentDescription.loadContentDescription(context))
+ .isEqualTo(latest?.secondaryLabel.loadText(context))
+ }
+
+ @Test
+ fun ethernetDefault_notValidated_matchesInteractor() =
+ testScope.runTest {
+ val latest by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ val ethernetIcon by collectLastValue(ethernetInteractor.icon)
+
+ connectivityRepository.setEthernetConnected(default = true, validated = false)
+
+ assertThat(latest?.secondaryLabel.loadText(context))
+ .isEqualTo(ethernetIcon!!.contentDescription.loadContentDescription(context))
+ assertThat(latest?.secondaryTitle).isNull()
+ assertThat(latest?.iconId).isEqualTo(R.drawable.stat_sys_ethernet)
+ assertThat(latest?.icon).isNull()
+ assertThat(latest?.stateDescription).isNull()
+ assertThat(latest?.contentDescription.loadContentDescription(context))
+ .isEqualTo(latest?.secondaryLabel.loadText(context))
+ }
+
+ private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
+ val networkModel =
+ WifiNetworkModel.Active(
+ networkId = 1,
+ level = 4,
+ ssid = "test ssid",
+ hotspotDeviceType = hotspot,
+ )
+
+ connectivityRepository.setWifiConnected()
+ wifiRepository.setIsWifiDefault(true)
+ wifiRepository.setWifiNetwork(networkModel)
+ }
+
+ private companion object {
+ const val SUB_1_ID = 1
+
+ val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+ InternetTileModel.Inactive(
+ secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+ iconId = R.drawable.ic_qs_no_internet_unavailable,
+ stateDescription = null,
+ contentDescription =
+ ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
new file mode 100644
index 000000000000..e1f3d97eb35c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain.interactor
+
+import android.platform.test.annotations.EnabledOnRavenwood
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+
+@SmallTest
+@EnabledOnRavenwood
+@RunWith(AndroidJUnit4::class)
+class InternetTileUserActionInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val inputHandler = FakeQSTileIntentUserInputHandler()
+
+ private lateinit var underTest: InternetTileUserActionInteractor
+
+ @Mock private lateinit var internetDialogManager: InternetDialogManager
+ @Mock private lateinit var controller: AccessPointController
+
+ @Before
+ fun setup() {
+ internetDialogManager = mock<InternetDialogManager>()
+ controller = mock<AccessPointController>()
+
+ underTest =
+ InternetTileUserActionInteractor(
+ kosmos.testScope.coroutineContext,
+ internetDialogManager,
+ controller,
+ inputHandler,
+ )
+ }
+
+ @Test
+ fun handleClickWhenActive() =
+ kosmos.testScope.runTest {
+ val input = InternetTileModel.Active()
+
+ underTest.handleInput(QSTileInputTestKtx.click(input))
+
+ verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+ }
+
+ @Test
+ fun handleClickWhenInactive() =
+ kosmos.testScope.runTest {
+ val input = InternetTileModel.Inactive()
+
+ underTest.handleInput(QSTileInputTestKtx.click(input))
+
+ verify(internetDialogManager).create(eq(true), anyBoolean(), anyBoolean(), nullable())
+ }
+
+ @Test
+ fun handleLongClickWhenActive() =
+ kosmos.testScope.runTest {
+ val input = InternetTileModel.Active()
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+ }
+ }
+
+ @Test
+ fun handleLongClickWhenInactive() =
+ kosmos.testScope.runTest {
+ val input = InternetTileModel.Inactive()
+
+ underTest.handleInput(QSTileInputTestKtx.longClick(input))
+
+ QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
+ Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
new file mode 100644
index 000000000000..caae4d26634d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain
+
+import android.content.Context
+import android.content.res.Resources
+import android.widget.Switch
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text.Companion.loadText
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [InternetTileModel] to [QSTileState]. */
+class InternetTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+ private val context: Context,
+) : QSTileDataToStateMapper<InternetTileModel> {
+
+ override fun map(config: QSTileConfig, data: InternetTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ label = resources.getString(R.string.quick_settings_internet_label)
+ expandedAccessibilityClass = Switch::class
+
+ if (data.secondaryLabel != null) {
+ secondaryLabel = data.secondaryLabel.loadText(context)
+ } else {
+ secondaryLabel = data.secondaryTitle
+ }
+
+ stateDescription = data.stateDescription.loadContentDescription(context)
+ contentDescription = data.contentDescription.loadContentDescription(context)
+
+ if (data.icon != null) {
+ this.icon = { data.icon }
+ } else if (data.iconId != null) {
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(data.iconId!!, theme),
+ contentDescription = null
+ )
+ this.icon = { loadedIcon }
+ }
+
+ sideViewIcon = QSTileState.SideViewIcon.Chevron
+
+ activationState =
+ if (data is InternetTileModel.Active) QSTileState.ActivationState.ACTIVE
+ else QSTileState.ActivationState.INACTIVE
+
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
new file mode 100644
index 000000000000..fdc596b3030c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain.interactor
+
+import android.annotation.StringRes
+import android.content.Context
+import android.os.UserHandle
+import android.text.Html
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+/** Observes internet state changes providing the [InternetTileModel]. */
+class InternetTileDataInteractor
+@Inject
+constructor(
+ private val context: Context,
+ @Application private val scope: CoroutineScope,
+ airplaneModeRepository: AirplaneModeRepository,
+ private val connectivityRepository: ConnectivityRepository,
+ ethernetInteractor: EthernetInteractor,
+ mobileIconsInteractor: MobileIconsInteractor,
+ wifiInteractor: WifiInteractor,
+) : QSTileDataInteractor<InternetTileModel> {
+ private val internetLabel: String = context.getString(R.string.quick_settings_internet_label)
+
+ // Three symmetrical Flows that can be switched upon based on the value of
+ // [DefaultConnectionModel]
+ private val wifiIconFlow: Flow<InternetTileModel> =
+ wifiInteractor.wifiNetwork.flatMapLatest {
+ val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true)
+ if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) {
+ val secondary = removeDoubleQuotes(it.ssid)
+ flowOf(
+ InternetTileModel.Active(
+ secondaryTitle = secondary,
+ icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
+ stateDescription = wifiIcon.contentDescription,
+ contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
+ )
+ )
+ } else {
+ notConnectedFlow
+ }
+ }
+
+ private val mobileDataContentName: Flow<CharSequence?> =
+ mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+ if (it == null) {
+ flowOf(null)
+ } else {
+ combine(it.isRoaming, it.networkTypeIconGroup) { isRoaming, networkTypeIconGroup ->
+ val cd = loadString(networkTypeIconGroup.contentDescription)
+ if (isRoaming) {
+ val roaming = context.getString(R.string.data_connection_roaming)
+ if (cd != null) {
+ context.getString(R.string.mobile_data_text_format, roaming, cd)
+ } else {
+ roaming
+ }
+ } else {
+ cd
+ }
+ }
+ }
+ }
+
+ private val mobileIconFlow: Flow<InternetTileModel> =
+ mobileIconsInteractor.activeDataIconInteractor.flatMapLatest {
+ if (it == null) {
+ notConnectedFlow
+ } else {
+ combine(
+ it.networkName,
+ it.signalLevelIcon,
+ mobileDataContentName,
+ ) { networkNameModel, signalIcon, dataContentDescription ->
+ when (signalIcon) {
+ is SignalIconModel.Cellular -> {
+ val secondary =
+ mobileDataContentConcat(
+ networkNameModel.name,
+ dataContentDescription
+ )
+
+ val stateLevel = signalIcon.level
+ val drawable = SignalDrawable(context)
+ drawable.setLevel(stateLevel)
+ val loadedIcon = Icon.Loaded(drawable, null)
+
+ InternetTileModel.Active(
+ secondaryTitle = secondary,
+ icon = loadedIcon,
+ stateDescription = ContentDescription.Loaded(secondary.toString()),
+ contentDescription = ContentDescription.Loaded(internetLabel),
+ )
+ }
+ is SignalIconModel.Satellite -> {
+ val secondary =
+ signalIcon.icon.contentDescription.loadContentDescription(context)
+ InternetTileModel.Active(
+ secondaryTitle = secondary,
+ iconId = signalIcon.icon.res,
+ stateDescription = ContentDescription.Loaded(secondary),
+ contentDescription = ContentDescription.Loaded(internetLabel),
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun mobileDataContentConcat(
+ networkName: String?,
+ dataContentDescription: CharSequence?
+ ): CharSequence {
+ if (dataContentDescription == null) {
+ return networkName ?: ""
+ }
+ if (networkName == null) {
+ return Html.fromHtml(dataContentDescription.toString(), 0)
+ }
+
+ return Html.fromHtml(
+ context.getString(
+ R.string.mobile_carrier_text_format,
+ networkName,
+ dataContentDescription
+ ),
+ 0
+ )
+ }
+
+ private fun loadString(@StringRes resId: Int): CharSequence? =
+ if (resId != 0) {
+ context.getString(resId)
+ } else {
+ null
+ }
+
+ private val ethernetIconFlow: Flow<InternetTileModel> =
+ ethernetInteractor.icon.flatMapLatest {
+ if (it == null) {
+ notConnectedFlow
+ } else {
+ val secondary = it.contentDescription
+ flowOf(
+ InternetTileModel.Active(
+ secondaryLabel = secondary?.toText(),
+ iconId = it.res,
+ stateDescription = null,
+ contentDescription = secondary,
+ )
+ )
+ }
+ }
+
+ private val notConnectedFlow: StateFlow<InternetTileModel> =
+ combine(
+ wifiInteractor.areNetworksAvailable,
+ airplaneModeRepository.isAirplaneMode,
+ ) { networksAvailable, isAirplaneMode ->
+ when {
+ isAirplaneMode -> {
+ val secondary = context.getString(R.string.status_bar_airplane)
+ InternetTileModel.Inactive(
+ secondaryTitle = secondary,
+ iconId = R.drawable.ic_qs_no_internet_unavailable,
+ stateDescription = null,
+ contentDescription = ContentDescription.Loaded(secondary),
+ )
+ }
+ networksAvailable -> {
+ val secondary =
+ context.getString(R.string.quick_settings_networks_available)
+ InternetTileModel.Inactive(
+ secondaryTitle = secondary,
+ iconId = R.drawable.ic_qs_no_internet_available,
+ stateDescription = null,
+ contentDescription =
+ ContentDescription.Loaded("$internetLabel,$secondary")
+ )
+ }
+ else -> {
+ NOT_CONNECTED_NETWORKS_UNAVAILABLE
+ }
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), NOT_CONNECTED_NETWORKS_UNAVAILABLE)
+
+ /**
+ * Consumable flow describing the correct state for the InternetTile.
+ *
+ * Strict ordering of which repo is sending its data to the internet tile. Swaps between each of
+ * the interim providers (wifi, mobile, ethernet, or not-connected).
+ */
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<InternetTileModel> =
+ connectivityRepository.defaultConnections.flatMapLatest {
+ when {
+ it.ethernet.isDefault -> ethernetIconFlow
+ it.mobile.isDefault || it.carrierMerged.isDefault -> mobileIconFlow
+ it.wifi.isDefault -> wifiIconFlow
+ else -> notConnectedFlow
+ }
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
+
+ private companion object {
+ val NOT_CONNECTED_NETWORKS_UNAVAILABLE =
+ InternetTileModel.Inactive(
+ secondaryLabel = Text.Resource(R.string.quick_settings_networks_unavailable),
+ iconId = R.drawable.ic_qs_no_internet_unavailable,
+ stateDescription = null,
+ contentDescription =
+ ContentDescription.Resource(R.string.quick_settings_networks_unavailable),
+ )
+
+ fun removeDoubleQuotes(string: String?): String? {
+ if (string == null) return null
+ return if (string.firstOrNull() == '"' && string.lastOrNull() == '"') {
+ string.substring(1, string.length - 1)
+ } else string
+ }
+
+ fun ContentDescription.toText(): Text =
+ when (this) {
+ is ContentDescription.Loaded -> Text.Loaded(this.description)
+ is ContentDescription.Resource -> Text.Resource(this.res)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
new file mode 100644
index 000000000000..2620cd555d8e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain.interactor
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.dialog.InternetDialogManager
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.connectivity.AccessPointController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+/** Handles internet tile clicks. */
+class InternetTileUserActionInteractor
+@Inject
+constructor(
+ @Main private val mainContext: CoroutineContext,
+ private val internetDialogManager: InternetDialogManager,
+ private val accessPointController: AccessPointController,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+) : QSTileUserActionInteractor<InternetTileModel> {
+
+ override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ withContext(mainContext) {
+ internetDialogManager.create(
+ aboveStatusBar = true,
+ accessPointController.canConfigMobileData(),
+ accessPointController.canConfigWifi(),
+ action.view,
+ )
+ }
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.view,
+ Intent(Settings.ACTION_WIFI_SETTINGS)
+ )
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
new file mode 100644
index 000000000000..ece904611782
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/model/InternetTileModel.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet.domain.model
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+
+/** Model describing the state that the QS Internet tile should be in. */
+sealed interface InternetTileModel {
+ val secondaryTitle: CharSequence?
+ val secondaryLabel: Text?
+ val iconId: Int?
+ val icon: Icon?
+ val stateDescription: ContentDescription?
+ val contentDescription: ContentDescription?
+
+ data class Active(
+ override val secondaryTitle: CharSequence? = null,
+ override val secondaryLabel: Text? = null,
+ override val iconId: Int? = null,
+ override val icon: Icon? = null,
+ override val stateDescription: ContentDescription? = null,
+ override val contentDescription: ContentDescription? = null,
+ ) : InternetTileModel
+
+ data class Inactive(
+ override val secondaryTitle: CharSequence? = null,
+ override val secondaryLabel: Text? = null,
+ override val iconId: Int? = null,
+ override val icon: Icon? = null,
+ override val stateDescription: ContentDescription? = null,
+ override val contentDescription: ContentDescription? = null,
+ ) : InternetTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 642eaccc3c99..c4d9cbfcd55c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -35,6 +35,10 @@ import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
+import com.android.systemui.qs.tiles.impl.internet.domain.InternetTileMapper
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileDataInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.interactor.InternetTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverTileMapper
import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileDataInteractor
import com.android.systemui.qs.tiles.impl.saver.domain.interactor.DataSaverTileUserActionInteractor
@@ -90,6 +94,7 @@ interface ConnectivityModule {
const val AIRPLANE_MODE_TILE_SPEC = "airplane"
const val DATA_SAVER_TILE_SPEC = "saver"
+ const val INTERNET_TILE_SPEC = "internet"
/** Inject InternetTile or InternetTileNewImpl into tileMap in QSModule */
@Provides
@@ -168,5 +173,36 @@ interface ConnectivityModule {
stateInteractor,
mapper,
)
+
+ @Provides
+ @IntoMap
+ @StringKey(INTERNET_TILE_SPEC)
+ fun provideInternetTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(INTERNET_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.ic_qs_no_internet_available,
+ labelRes = R.string.quick_settings_internet_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /** Inject InternetTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(INTERNET_TILE_SPEC)
+ fun provideInternetTileViewModel(
+ factory: QSTileViewModelFactory.Static<InternetTileModel>,
+ mapper: InternetTileMapper,
+ stateInteractor: InternetTileDataInteractor,
+ userActionInteractor: InternetTileUserActionInteractor
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(INTERNET_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
new file mode 100644
index 000000000000..6772ba317da4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/internet/InternetTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.qs.tiles.impl.internet
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
+
+val Kosmos.qsInternetTileConfig by
+ Kosmos.Fixture { ConnectivityModule.provideInternetTileConfig(qsEventLogger) }