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

Commit 5ab3abb4 authored by Evan Laird's avatar Evan Laird
Browse files

[QS] Add flag-protected InternetTile backed by new data pipeline

This CL adds `InternetTileNewImpl`, which is meant to be a 1:1
replacement for the existing `InternetTile`, built using the new mobile
data pipeline.

The tile can be enabled by setting the appropriate system flag (see
below)

Test: adb shell cmd statusbar flag signal_callback_deprecation true
Test: InternetTileNewImplTest
Bug: 291321279
Change-Id: I64acd97d1af3dd468a2eaa41a2edc73eadedb362
parent fef3c333
Loading
Loading
Loading
Loading
+114 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.View
import com.android.internal.logging.MetricsLogger
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.qs.QSIconView
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.AlphaControlledSignalTileView
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory
import com.android.systemui.statusbar.connectivity.AccessPointController
import com.android.systemui.statusbar.pipeline.shared.ui.binder.InternetTileBinder
import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.InternetTileViewModel
import javax.inject.Inject

class InternetTileNewImpl
@Inject
constructor(
    host: QSHost,
    uiEventLogger: QsEventLogger,
    @Background backgroundLooper: Looper,
    @Main private val mainHandler: Handler,
    falsingManager: FalsingManager,
    metricsLogger: MetricsLogger,
    statusBarStateController: StatusBarStateController,
    activityStarter: ActivityStarter,
    qsLogger: QSLogger,
    viewModel: InternetTileViewModel,
    private val internetDialogFactory: InternetDialogFactory,
    private val accessPointController: AccessPointController,
) :
    QSTileImpl<QSTile.SignalState>(
        host,
        uiEventLogger,
        backgroundLooper,
        mainHandler,
        falsingManager,
        metricsLogger,
        statusBarStateController,
        activityStarter,
        qsLogger
    ) {
    private var model: InternetTileModel = viewModel.tileModel.value

    init {
        InternetTileBinder.bind(lifecycle, viewModel.tileModel) { newModel ->
            model = newModel
            refreshState()
        }
    }

    override fun createTileView(context: Context): QSIconView =
        AlphaControlledSignalTileView(context)

    override fun getTileLabel(): CharSequence =
        mContext.getString(R.string.quick_settings_internet_label)

    override fun newTileState(): QSTile.SignalState {
        return QSTile.SignalState().also { it.forceExpandIcon = true }
    }

    override fun handleClick(view: View?) {
        mainHandler.post {
            internetDialogFactory.create(
                aboveStatusBar = true,
                accessPointController.canConfigMobileData(),
                accessPointController.canConfigWifi(),
                view,
            )
        }
    }

    override fun handleUpdateState(state: QSTile.SignalState, arg: Any?) {
        state.label = mContext.resources.getString(R.string.quick_settings_internet_label)

        model.applyTo(state, mContext)
    }

    override fun getLongClickIntent(): Intent = WIFI_SETTINGS

    companion object {
        private val WIFI_SETTINGS = Intent(Settings.ACTION_WIFI_SETTINGS)
    }
}
+21 −6
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar.connectivity

import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.AirplaneModeTile
import com.android.systemui.qs.tiles.BluetoothTile
@@ -23,21 +25,17 @@ import com.android.systemui.qs.tiles.CastTile
import com.android.systemui.qs.tiles.DataSaverTile
import com.android.systemui.qs.tiles.HotspotTile
import com.android.systemui.qs.tiles.InternetTile
import com.android.systemui.qs.tiles.InternetTileNewImpl
import com.android.systemui.qs.tiles.NfcTile
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey

@Module
interface ConnectivityModule {

    /** Inject InternetTile into tileMap in QSModule */
    @Binds
    @IntoMap
    @StringKey(InternetTile.TILE_SPEC)
    fun bindInternetTile(internetTile: InternetTile): QSTileImpl<*>

    /** Inject BluetoothTile into tileMap in QSModule */
    @Binds
    @IntoMap
@@ -70,4 +68,21 @@ interface ConnectivityModule {

    /** Inject NfcTile into tileMap in QSModule */
    @Binds @IntoMap @StringKey(NfcTile.TILE_SPEC) fun bindNfcTile(nfcTile: NfcTile): QSTileImpl<*>

    companion object {
        /** Inject InternetTile or InternetTileNewImpl into tileMap in QSModule */
        @Provides
        @IntoMap
        @StringKey(InternetTile.TILE_SPEC)
        fun bindInternetTile(
            internetTile: InternetTile,
            newInternetTile: InternetTileNewImpl,
            featureFlags: FeatureFlags,
        ): QSTileImpl<*> =
            if (featureFlags.isEnabled(SIGNAL_CALLBACK_DEPRECATION)) {
                newInternetTile
            } else {
                internetTile
            }
    }
}
+241 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.service.quicksettings.Tile
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.MetricsLogger
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
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.dialog.InternetDialogFactory
import com.android.systemui.statusbar.connectivity.AccessPointController
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.domain.interactor.FakeMobileIconsInteractor
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.model.DefaultConnectionModel.Wifi
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.InternetTileViewModel
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.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
class InternetTileNewImplTest : SysuiTestCase() {
    lateinit var underTest: InternetTileNewImpl

    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)

    private var airplaneModeRepository = FakeAirplaneModeRepository()
    private var connectivityRepository = FakeConnectivityRepository()
    private var ethernetInteractor = EthernetInteractor(connectivityRepository)
    private var mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
    private var wifiRepository = FakeWifiRepository()
    private var wifiInteractor =
        WifiInteractorImpl(connectivityRepository, wifiRepository, testScope.backgroundScope)
    private lateinit var viewModel: InternetTileViewModel

    private lateinit var looper: TestableLooper

    @Mock private lateinit var host: QSHost
    @Mock private lateinit var eventLogger: QsEventLogger
    @Mock private lateinit var metricsLogger: MetricsLogger
    @Mock private lateinit var sbStateController: StatusBarStateController
    @Mock private lateinit var activityStarter: ActivityStarter
    @Mock private lateinit var logger: QSLogger
    @Mock private lateinit var dialogFactory: InternetDialogFactory
    @Mock private lateinit var accessPointController: AccessPointController

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

        // Allow the tile to load resources
        whenever(host.context).thenReturn(context)
        whenever(host.userContext).thenReturn(context)

        viewModel =
            InternetTileViewModel(
                airplaneModeRepository,
                connectivityRepository,
                ethernetInteractor,
                mobileIconsInteractor,
                wifiInteractor,
                context,
                testScope.backgroundScope,
            )

        underTest =
            InternetTileNewImpl(
                host,
                eventLogger,
                looper.looper,
                Handler(looper.looper),
                FalsingManagerFake(),
                metricsLogger,
                sbStateController,
                activityStarter,
                logger,
                viewModel,
                dialogFactory,
                accessPointController
            )

        underTest.initialize()
        underTest.setListening(Object(), true)

        looper.processAllMessages()
    }

    @Test
    fun noDefaultConnection_noNetworkAvailable() =
        testScope.runTest {
            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
            wifiRepository.wifiScanResults.value = listOf()

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.secondaryLabel.toString())
                .isEqualTo(context.getString(R.string.quick_settings_networks_unavailable))
            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)
        }

    @Test
    fun noDefaultConnection_networksAvailable() =
        testScope.runTest {
            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
            wifiRepository.wifiScanResults.value =
                listOf(
                    WifiScanEntry(ssid = "ssid 1"),
                    WifiScanEntry(ssid = "ssid 2"),
                )

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.secondaryLabel.toString())
                .isEqualTo(context.getString(R.string.quick_settings_networks_available))
            assertThat(underTest.state.state).isEqualTo(1)
        }

    @Test
    fun airplaneMode_enabled_wifiDisabled() =
        testScope.runTest {
            airplaneModeRepository.setIsAirplaneMode(true)
            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
            wifiRepository.setIsWifiEnabled(false)

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)
            assertThat(underTest.state.secondaryLabel)
                .isEqualTo(context.getString(R.string.status_bar_airplane))
        }

    @Test
    fun airplaneMode_enabled_wifiEnabledButNotConnected() =
        testScope.runTest {
            airplaneModeRepository.setIsAirplaneMode(true)
            connectivityRepository.defaultConnections.value = DefaultConnectionModel()
            wifiRepository.setIsWifiEnabled(true)

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)
            assertThat(underTest.state.secondaryLabel)
                .isEqualTo(context.getString(R.string.status_bar_airplane))
        }

    @Test
    fun airplaneMode_enabled_wifiEnabledAndConnected() =
        testScope.runTest {
            airplaneModeRepository.setIsAirplaneMode(true)
            connectivityRepository.defaultConnections.value =
                DefaultConnectionModel(
                    wifi = Wifi(true),
                    isValidated = true,
                )
            wifiRepository.setIsWifiEnabled(true)
            wifiRepository.setWifiNetwork(ACTIVE_WIFI)

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE)
            assertThat(underTest.state.secondaryLabel).isEqualTo(WIFI_SSID)
        }

    @Test
    fun wifiConnected() =
        testScope.runTest {
            connectivityRepository.defaultConnections.value =
                DefaultConnectionModel(
                    wifi = Wifi(true),
                    isValidated = true,
                )

            wifiRepository.setIsWifiEnabled(true)
            wifiRepository.setWifiNetwork(ACTIVE_WIFI)

            runCurrent()
            looper.processAllMessages()

            assertThat(underTest.state.state).isEqualTo(Tile.STATE_ACTIVE)
            assertThat(underTest.state.secondaryLabel).isEqualTo(WIFI_SSID)
        }

    companion object {
        const val WIFI_SSID = "test ssid"
        val ACTIVE_WIFI =
            WifiNetworkModel.Active(
                networkId = 1,
                isValidated = true,
                level = 4,
                ssid = WIFI_SSID,
            )
    }
}