Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c7fe2ce authored by Quang Luong's avatar Quang Luong
Browse files

Add wifi tile with toggle

Add a new wifi tile with toggle to pause and scan for wifi.
To ease the transition away from the Provider Model, the tile will still
be titled Internet and show the current default non-wifi internet
provider when Wifi is not default.

Flag: com.android.systemui.qs_split_internet_tile

Bug: 201143076
Test: atest WifiTileTest WifiTileDataInteractorTest
WifiTileUserActionInteractorTest WifiTileMapperTest

Change-Id: Icf4b64d5ed393bbeb6c7eba50c2f86b303b077b8
parent 4edfa6ed
Loading
Loading
Loading
Loading
+173 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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

import android.os.Handler
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigProvider
import com.android.systemui.qs.tiles.base.shared.model.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.base.shared.model.QSTileState
import com.android.systemui.qs.tiles.base.shared.model.QSTileUIConfig
import com.android.systemui.qs.tiles.impl.wifi.domain.interactor.WifiTileDataInteractor
import com.android.systemui.qs.tiles.impl.wifi.domain.interactor.WifiTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.wifi.domain.model.WifiTileModel
import com.android.systemui.qs.tiles.impl.wifi.ui.mapper.WifiTileMapper
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
class WifiTileTest : SysuiTestCase() {

    @Mock private lateinit var host: QSHost
    @Mock private lateinit var uiEventLogger: QsEventLogger
    @Mock private lateinit var metricsLogger: MetricsLogger
    @Mock private lateinit var statusBarStateController: StatusBarStateController
    @Mock private lateinit var activityStarter: ActivityStarter
    @Mock private lateinit var qsLogger: QSLogger
    @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider

    // Mocks for the new-arch components
    @Mock private lateinit var dataInteractor: WifiTileDataInteractor
    @Mock private lateinit var tileMapper: WifiTileMapper
    @Mock private lateinit var userActionInteractor: WifiTileUserActionInteractor

    private lateinit var testableLooper: TestableLooper
    private lateinit var underTest: WifiTile
    private val tileDataFlow =
        MutableStateFlow<WifiTileModel>(mock(WifiTileModel.Inactive::class.java))

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)

        whenever(host.context).thenReturn(context)
        whenever(host.userContext).thenReturn(context)
        whenever(host.userId).thenReturn(0)

        whenever(dataInteractor.tileData()).thenReturn(tileDataFlow)

        whenever(qsTileConfigProvider.getConfig(WifiTile.TILE_SPEC))
            .thenReturn(
                QSTileConfigTestBuilder.build {
                    uiConfig =
                        QSTileUIConfig.Resource(
                            iconRes = R.drawable.ic_signal_wifi_off,
                            labelRes = R.string.quick_settings_internet_label,
                        )
                }
            )

        underTest =
            WifiTile(
                host,
                uiEventLogger,
                testableLooper.looper,
                Handler(testableLooper.looper),
                FalsingManagerFake(),
                metricsLogger,
                statusBarStateController,
                activityStarter,
                qsLogger,
                qsTileConfigProvider,
                dataInteractor,
                tileMapper,
                userActionInteractor,
            )

        underTest.initialize()
        underTest.setListening(this, true)
        testableLooper.processAllMessages()
    }

    @After
    fun tearDown() {
        underTest.setListening(this, false)
        underTest.destroy()
        testableLooper.processAllMessages()
    }

    @Test
    fun tileDataChanges_triggersMapperWithNewModel() {
        val model = mock(WifiTileModel.Active::class.java)
        whenever(tileMapper.map(any(), any())).thenReturn(mock(QSTileState::class.java))

        tileDataFlow.value = model
        testableLooper.processAllMessages()

        verify(tileMapper).map(any(), eq(model))
    }

    @Test
    fun click_callsUserActionInteractor() = runTest {
        val expandable = mock(Expandable::class.java)
        underTest.click(expandable)
        testableLooper.processAllMessages()

        verify(userActionInteractor).handleClick(expandable)
    }

    @Test
    fun secondaryClick_callsUserActionInteractor() {
        val expandable = mock(Expandable::class.java)
        underTest.secondaryClick(expandable)
        testableLooper.processAllMessages()

        verify(userActionInteractor).handleSecondaryClick(expandable)
    }

    @Test
    fun isAvailable_fromDataInteractor_isTrue() {
        whenever(dataInteractor.isAvailable()).thenReturn(true)

        assertThat(underTest.isAvailable).isTrue()
    }

    @Test
    fun isAvailable_fromDataInteractor_isFalse() {
        whenever(dataInteractor.isAvailable()).thenReturn(false)

        assertThat(underTest.isAvailable).isFalse()
    }
}
+240 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wifi.domain.interactor

import android.os.UserHandle
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.Flags as AconfigFlags
import com.android.systemui.Flags.FLAG_QS_SPLIT_INTERNET_TILE
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.flags.andSceneContainer
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.qs.tiles.impl.wifi.domain.model.WifiTileModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.connectivity.WifiIcons
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
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.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.ui.model.WifiTileIconModel
import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.CarrierConfigTracker
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters

@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
class WifiTileDataInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {

    @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()

    init {
        mSetFlagsRule.setFlagsParameterization(flags)
    }

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var underTest: WifiTileDataInteractor
    private lateinit var mobileIconsInteractor: MobileIconsInteractor
    private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
    private lateinit var mobileConnectionRepository: FakeMobileConnectionRepository
    private lateinit var connectivityRepository: FakeConnectivityRepository
    private lateinit var wifiRepository: FakeWifiRepository

    @Before
    fun setUp() {
        val mobileContextProvider = mock<MobileContextProvider>()
        whenever(mobileContextProvider.getMobileContextForSub(any(), any())).thenReturn(context)

        val tableLogBuffer = logcatTableLogBuffer(kosmos, "WifiTileDataInteractorTest")
        mobileConnectionsRepository =
            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer)
        mobileConnectionRepository = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
        connectivityRepository = FakeConnectivityRepository()
        wifiRepository = FakeWifiRepository()

        val wifiInteractor =
            WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)

        mobileIconsInteractor =
            MobileIconsInteractorImpl(
                mobileConnectionsRepository,
                mock<CarrierConfigTracker>(),
                tableLogBuffer,
                connectivityRepository,
                FakeUserSetupRepository(),
                testScope.backgroundScope,
                context,
                FakeFeatureFlagsClassic().also {
                    it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
                },
            )

        underTest =
            WifiTileDataInteractor(
                context,
                testScope.backgroundScope,
                FakeAirplaneModeRepository(),
                connectivityRepository,
                mobileIconsInteractor,
                mobileContextProvider,
                wifiInteractor,
            )
    }

    @Test
    fun tileData_wifiActiveAndDefault_showsSsid() =
        kosmos.runTest {
            val tileData by collectLastValue(underTest.tileData())

            connectivityRepository.setWifiConnected()
            wifiRepository.setIsWifiDefault(true)
            wifiRepository.setWifiNetwork(
                WifiNetworkModel.Active.of(isValidated = true, level = 4, ssid = "Test SSID")
            )

            val expectedModel =
                WifiTileModel.Active(
                    icon = WifiTileIconModel(WifiIcons.WIFI_FULL_ICONS[4]),
                    secondaryLabel = "Test SSID",
                )
            assertThat(tileData).isEqualTo(expectedModel)
        }

    @Test
    fun tileData_wifiDisabled_showsOffState() =
        kosmos.runTest {
            val tileData by collectLastValue(underTest.tileData())

            wifiRepository.setIsWifiEnabled(false)

            val expectedModel =
                WifiTileModel.Inactive(
                    icon = WifiTileIconModel(R.drawable.ic_signal_wifi_off),
                    secondaryLabel = context.getString(R.string.quick_settings_networks_unavailable),
                )
            assertThat(tileData?.icon).isEqualTo(expectedModel.icon)
            assertThat(tileData?.secondaryLabel).isEqualTo(expectedModel.secondaryLabel)
        }

    @Test
    fun tileData_ethernetIsDefault_showsEthernet() =
        kosmos.runTest {
            val tileData by collectLastValue(underTest.tileData())
            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
            connectivityRepository.setEthernetConnected()

            val expectedModel =
                WifiTileModel.Active(
                    icon = WifiTileIconModel(R.drawable.stat_sys_ethernet_fully),
                    secondaryLabel =
                        context.getString(
                            com.android.settingslib.R.string.accessibility_ethernet_connected
                        ),
                )
            assertThat(tileData).isEqualTo(expectedModel)
        }

    @Test
    fun tileData_mobileIsDefault_showsMobileNetworkName() =
        kosmos.runTest {
            val tileData by collectLastValue(underTest.tileData())

            wifiRepository.setIsWifiEnabled(true)
            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())

            connectivityRepository.setMobileConnected()
            mobileConnectionsRepository.apply {
                activeMobileDataRepository.value = mobileConnectionRepository
                activeMobileDataSubscriptionId.value = SUB_ID
                setMobileConnectionRepositoryMap(mapOf(SUB_ID to mobileConnectionRepository))
            }
            mobileConnectionRepository.apply {
                isInService.value = true
                dataConnectionState.value = DataConnectionState.Connected
                dataEnabled.value = true
                networkName.value = NetworkNameModel.Default("Test Mobile")
            }

            val expectedModel =
                WifiTileModel.Inactive(
                    icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL),
                    secondaryLabel = "Test Mobile",
                )

            assertThat(tileData?.icon).isEqualTo(expectedModel.icon)
            assertThat(tileData?.secondaryLabel.toString()).contains("Test Mobile")
        }

    @Test
    @RequiresFlagsEnabled(FLAG_QS_SPLIT_INTERNET_TILE)
    fun availability_flagEnabled_isTrue() =
        kosmos.runTest {
            assertThat(AconfigFlags.qsSplitInternetTile()).isTrue()
            val availability by collectLastValue(underTest.availability(USER))
            assertThat(availability).isTrue()
        }

    @Test
    @RequiresFlagsDisabled(FLAG_QS_SPLIT_INTERNET_TILE)
    fun availability_flagDisabled_isFalse() =
        kosmos.runTest {
            assertThat(AconfigFlags.qsSplitInternetTile()).isFalse()
            val availability by collectLastValue(underTest.availability(USER))
            assertThat(availability).isFalse()
        }

    companion object {
        private const val SUB_ID = 456
        private val USER = UserHandle.of(0)

        @JvmStatic
        @Parameters(name = "{0}")
        fun getParams(): List<FlagsParameterization> {
            return FlagsParameterization.allCombinationsOf().andSceneContainer()
        }
    }
}
+164 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.wifi.domain.interactor

import android.os.UserHandle
import android.provider.Settings
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.domain.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.domain.actions.qsTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.domain.model.QSTileInputTestKtx
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.wifi.domain.model.WifiTileModel
import com.android.systemui.statusbar.connectivity.AccessPointController
import com.android.systemui.statusbar.pipeline.shared.ui.model.WifiToggleState
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiTileIconModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(JUnit4::class)
class WifiTileUserActionInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val intentHandler = kosmos.qsTileIntentUserInputHandler

    @Mock private lateinit var internetDialogManager: InternetDialogManager
    @Mock private lateinit var accessPointController: AccessPointController
    @Mock private lateinit var wifiRepository: WifiRepository
    @Mock private lateinit var expandable: Expandable

    private lateinit var underTest: WifiTileUserActionInteractor

    private val isWifiEnabledState = MutableStateFlow(true)
    private val wifiToggleState = MutableStateFlow(WifiToggleState.Normal)

    private val testModel = WifiTileModel.Active(WifiTileIconModel(0), "ssid")

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        whenever(wifiRepository.isWifiEnabled).thenReturn(isWifiEnabledState)
        whenever(wifiRepository.wifiToggleState).thenReturn(wifiToggleState)

        underTest =
            WifiTileUserActionInteractor(
                testScope.coroutineContext,
                internetDialogManager,
                accessPointController,
                wifiRepository,
                intentHandler,
            )
    }

    @Test
    fun handleLongClick_opensWifiSettings() =
        testScope.runTest {
            underTest.handleInput(QSTileInputTestKtx.longClick(testModel))

            QSTileIntentUserInputHandlerSubject.assertThat(intentHandler).handledOneIntentInput {
                assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
            }
        }

    @Test
    fun handleClick_showsInternetDialog() =
        testScope.runTest {
            whenever(accessPointController.canConfigMobileData()).thenReturn(true)
            whenever(accessPointController.canConfigWifi()).thenReturn(true)

            underTest.handleInput(QSTileInputTestKtx.click(testModel, UserHandle.of(0), expandable))

            verify(internetDialogManager)
                .create(
                    aboveStatusBar = true,
                    canConfigMobileData = true,
                    canConfigWifi = true,
                    expandable = expandable,
                )
        }

    @Test
    fun handleSecondaryClick_whenDisabled_enablesWifi() =
        testScope.runTest {
            isWifiEnabledState.value = false
            wifiToggleState.value = WifiToggleState.Normal

            underTest.handleInput(QSTileInputTestKtx.toggleClick(testModel))

            verify(wifiRepository).enableWifi()
        }

    @Test
    fun handleSecondaryClick_whenEnabledAndNotConnected_scansForWifi() =
        testScope.runTest {
            isWifiEnabledState.value = true
            wifiToggleState.value = WifiToggleState.Normal
            whenever(wifiRepository.isWifiConnectedWithValidSsid()).thenReturn(false)

            underTest.handleInput(QSTileInputTestKtx.toggleClick(testModel))

            verify(wifiRepository).scanForWifi()
        }

    @Test
    fun handleSecondaryClick_whenEnabledAndConnected_pausesWifi() =
        testScope.runTest {
            isWifiEnabledState.value = true
            wifiToggleState.value = WifiToggleState.Normal
            whenever(wifiRepository.isWifiConnectedWithValidSsid()).thenReturn(true)

            underTest.handleInput(QSTileInputTestKtx.toggleClick(testModel))

            verify(wifiRepository).pauseWifi()
        }

    @Test
    fun handleSecondaryClick_whenPausing_scansForWifi() =
        testScope.runTest {
            wifiToggleState.value = WifiToggleState.Pausing

            underTest.handleInput(QSTileInputTestKtx.toggleClick(testModel))

            verify(wifiRepository).scanForWifi()
        }

    @Test
    fun handleSecondaryClick_whenScanning_pausesWifi() =
        testScope.runTest {
            wifiToggleState.value = WifiToggleState.Scanning

            underTest.handleInput(QSTileInputTestKtx.toggleClick(testModel))

            verify(wifiRepository).pauseWifi()
        }
}
+136 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -979,6 +979,7 @@
    <string name="quick_settings_connected">Connected</string>
    <!-- QuickSettings: Control panel: Label for connecting device. [CHAR LIMIT=NONE] -->
    <string name="quick_settings_connecting">Connecting...</string>
    <string name="quick_settings_scanning_for_wifi">Scanning for Wi\u2011Fi...</string>
    <!-- QuickSettings: Tethering. [CHAR LIMIT=NONE] -->
    <!-- QuickSettings: Hotspot. [CHAR LIMIT=NONE] -->
    <string name="quick_settings_hotspot_label">Hotspot</string>
Loading