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

Commit f35a4c57 authored by Ioana Alexandru's avatar Ioana Alexandru
Browse files

Add legacy ModesTile

Even though the main focus is to get the new architecture tile working,
this tile shouldn't depend on the qs_new_tiles release, so we still need
a legacy style tile until the transition is complete.

Bug: 346519570
Test: checked that the tile works when qs_new_tiles is off
Flag: android.app.modes_ui

Change-Id: Ibedd556fdb38130eb71ca818aa32498b6b82f682
parent 58eb0b24
Loading
Loading
Loading
Loading
+120 −0
Original line number Diff line number Diff line
/*
 * 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

import android.app.Flags
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.logging.MetricsLogger
import com.android.systemui.animation.Expandable
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.QSTile.BooleanState
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.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.impl.modes.domain.interactor.ModesTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel
import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.launch

class ModesTile
@Inject
constructor(
    host: QSHost,
    uiEventLogger: QsEventLogger,
    @Background backgroundLooper: Looper,
    @Main mainHandler: Handler,
    falsingManager: FalsingManager,
    metricsLogger: MetricsLogger,
    statusBarStateController: StatusBarStateController,
    activityStarter: ActivityStarter,
    qsLogger: QSLogger,
    qsTileConfigProvider: QSTileConfigProvider,
    dataInteractor: ModesTileDataInteractor,
    private val tileMapper: ModesTileMapper,
) :
    QSTileImpl<BooleanState>(
        host,
        uiEventLogger,
        backgroundLooper,
        mainHandler,
        falsingManager,
        metricsLogger,
        statusBarStateController,
        activityStarter,
        qsLogger
    ) {

    private lateinit var tileState: QSTileState
    private val config = qsTileConfigProvider.getConfig(TILE_SPEC)

    init {
        lifecycle.coroutineScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
                dataInteractor.tileData().collect { refreshState(it) }
            }
        }
    }

    override fun isAvailable(): Boolean = Flags.modesUi()

    override fun getTileLabel(): CharSequence = tileState.label

    override fun newTileState() = BooleanState()

    override fun handleClick(expandable: Expandable?) {
        // TODO(b/346519570) open dialog
    }

    override fun getLongClickIntent(): Intent? {
        // TODO(b/346519570) open settings
        return null
    }

    override fun handleUpdateState(booleanState: BooleanState?, arg: Any?) {
        if (arg is ModesTileModel) {
            tileState = tileMapper.map(config, arg)

            booleanState?.apply {
                state = tileState.activationState.legacyState
                icon = ResourceIcon.get(tileState.iconRes ?: R.drawable.qs_dnd_icon_off)
                label = tileLabel
                secondaryLabel = tileState.secondaryLabel
                contentDescription = tileState.contentDescription
                // TODO(b/346519570) open settings
                handlesLongClick = false
            }
        }
    }

    companion object {
        const val TILE_SPEC = "modes"
    }
}
+8 −3
Original line number Diff line number Diff line
@@ -38,9 +38,14 @@ class ModesTileDataInteractor @Inject constructor(val zenModeRepository: ZenMode
    override fun tileData(
        user: UserHandle,
        triggers: Flow<DataUpdateTrigger>
    ): Flow<ModesTileModel> {
        return zenModeActive.map { ModesTileModel(isActivated = it) }
    }
    ): Flow<ModesTileModel> = tileData()

    /**
     * An adapted version of the base class' [tileData] method for use in an old-style tile.
     *
     * TODO(b/299909989): Remove after the transition.
     */
    fun tileData() = zenModeActive.map { ModesTileModel(isActivated = it) }

    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(Flags.modesUi())
}
+6 −2
Original line number Diff line number Diff line
@@ -34,13 +34,17 @@ constructor(
) : QSTileDataToStateMapper<ModesTileModel> {
    override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState =
        QSTileState.build(resources, theme, config.uiConfig) {
            val iconRes =
            iconRes =
                if (data.isActivated) {
                    R.drawable.qs_dnd_icon_on
                } else {
                    R.drawable.qs_dnd_icon_off
                }
            val icon = Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null)
            val icon =
                Icon.Loaded(
                    resources.getDrawable(iconRes!!, theme),
                    contentDescription = null,
                )
            this.icon = { icon }
            if (data.isActivated) {
                activationState = QSTileState.ActivationState.ACTIVE
+7 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.systemui.qs.tiles.DndTile
import com.android.systemui.qs.tiles.FlashlightTile
import com.android.systemui.qs.tiles.LocationTile
import com.android.systemui.qs.tiles.MicrophoneToggleTile
import com.android.systemui.qs.tiles.ModesTile
import com.android.systemui.qs.tiles.UiModeNightTile
import com.android.systemui.qs.tiles.WorkModeTile
import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
@@ -79,6 +80,12 @@ interface PolicyModule {
    /** Inject DndTile into tileMap in QSModule */
    @Binds @IntoMap @StringKey(DndTile.TILE_SPEC) fun bindDndTile(dndTile: DndTile): QSTileImpl<*>

    /** Inject ModesTile into tileMap in QSModule */
    @Binds
    @IntoMap
    @StringKey(ModesTile.TILE_SPEC)
    fun bindModesTile(modesTile: ModesTile): QSTileImpl<*>

    /** Inject WorkModeTile into tileMap in QSModule */
    @Binds
    @IntoMap
+160 −0
Original line number Diff line number Diff line
/*
 * 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

import android.graphics.drawable.TestStubDrawable
import android.os.Handler
import android.platform.test.annotations.EnableFlags
import android.service.quicksettings.Tile
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.settingslib.notification.data.repository.FakeZenModeRepository
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.impl.modes.domain.interactor.ModesTileDataInteractor
import com.android.systemui.qs.tiles.impl.modes.ui.ModesTileMapper
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
import com.android.systemui.res.R
import com.android.systemui.util.mockito.any
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
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.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(android.app.Flags.FLAG_MODES_UI)
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
class ModesTileTest : SysuiTestCase() {

    @Mock private lateinit var qsHost: QSHost

    @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 uiEventLogger: QsEventLogger

    @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider

    private val zenModeRepository = FakeZenModeRepository()
    private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository)
    private val mapper =
        ModesTileMapper(
            context.orCreateTestableResources
                .apply {
                    addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable())
                    addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable())
                }
                .resources,
            context.theme,
        )

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

    private lateinit var secureSettings: SecureSettings
    private lateinit var testableLooper: TestableLooper
    private lateinit var underTest: ModesTile

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

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

        whenever(qsTileConfigProvider.getConfig(any()))
            .thenReturn(
                QSTileConfigTestBuilder.build {
                    uiConfig =
                        QSTileUIConfig.Resource(
                            iconRes = R.drawable.qs_dnd_icon_off,
                            labelRes = R.string.quick_settings_modes_label,
                        )
                }
            )

        underTest =
            ModesTile(
                qsHost,
                uiEventLogger,
                testableLooper.looper,
                Handler(testableLooper.looper),
                FalsingManagerFake(),
                metricsLogger,
                statusBarStateController,
                activityStarter,
                qsLogger,
                qsTileConfigProvider,
                tileDataInteractor,
                mapper
            )

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

        testableLooper.processAllMessages()
    }

    @After
    fun tearDown() {
        underTest.destroy()
        testableLooper.processAllMessages()
    }

    @Test
    fun stateUpdatesOnChange() =
        testScope.runTest {
            assertThat(underTest.state.state).isEqualTo(Tile.STATE_INACTIVE)

            zenModeRepository.addMode(id = "Test", active = true)
            runCurrent()
            testableLooper.processAllMessages()

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