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
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
new file mode 100644
index 0000000..930a443
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt
@@ -0,0 +1,120 @@
+/*
+ * 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"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 930109a..31e91aa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -38,9 +38,14 @@
     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())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
index 07b393e..26b9a4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt
@@ -34,13 +34,17 @@
 ) : 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
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 81e41d67..cf9a78f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -29,6 +29,7 @@
 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 @@
     /** 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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
new file mode 100644
index 0000000..aa25628
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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)
+        }
+}