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

Commit a39f5980 authored by Olivier St-Onge's avatar Olivier St-Onge
Browse files

Use main click on long press for small dual target tiles

Test: manually
Test: TileTest.kt
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Fixes: 410023978
Change-Id: I1b7297e709c251e3db6ba77375c70495c7feeb1a
parent 465c4874
Loading
Loading
Loading
Loading
+11 −4
Original line number Diff line number Diff line
@@ -197,7 +197,14 @@ fun Tile(
                        hapticsViewModel?.setTileInteractionState(
                            TileHapticsViewModel.TileInteractionState.LONG_CLICKED
                        )
                        tile.onLongClick(expandable)

                        // User main click on long press for small dual target tiles
                        if (iconOnly && isDualTarget) {
                            tile.mainClick(expandable)
                        } else {
                            // Settings click otherwise
                            tile.settingsClick(expandable)
                        }
                    }
                    .takeIf { uiState.handlesLongClick }

@@ -213,9 +220,9 @@ fun Tile(
                        // For those tile's who doesn't have a detailed view, process with
                        // their `onClick` behavior.
                        if (iconOnly && isDualTarget) {
                            tile.onSecondaryClick()
                            tile.toggleClick()
                        } else {
                            tile.onClick(expandable)
                            tile.mainClick(expandable)
                        }

                        // Side effects of the click
@@ -252,7 +259,7 @@ fun Tile(
                                hapticsViewModel?.setTileInteractionState(
                                    TileHapticsViewModel.TileInteractionState.CLICKED
                                )
                                tile.onSecondaryClick()
                                tile.toggleClick()
                            }
                            .takeIf { isDualTarget }
                    LargeTileContent(
+18 −3
Original line number Diff line number Diff line
@@ -44,15 +44,30 @@ data class TileViewModel(private val tile: QSTile, val spec: TileSpec) {
    val currentState: QSTile.State
        get() = tile.state

    fun onClick(expandable: Expandable?) {
    /**
     * Callback for the tile's main click, i.e. the primary function of the tile.
     *
     * @param expandable the [Expandable] to use if expanding to a dialog/activity
     */
    fun mainClick(expandable: Expandable?) {
        tile.click(expandable)
    }

    fun onLongClick(expandable: Expandable?) {
    /**
     * Callback to open the tile's settings page
     *
     * @param expandable the [Expandable] to use if expanding to the settings page
     */
    fun settingsClick(expandable: Expandable?) {
        tile.longClick(expandable)
    }

    fun onSecondaryClick() {
    /**
     * Callback to the tile's toggle function.
     *
     * This is used for one-tap operations.
     */
    fun toggleClick() {
        tile.secondaryClick(null)
    }

+225 −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.panels.ui.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.theme.PlatformTheme
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.msdl.tileHapticsViewModelFactoryProvider
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile
import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class TileTest : SysuiTestCase() {
    @get:Rule val composeRule = createComposeRule()
    private val kosmos = testKosmos()
    private val tileHapticsViewModelFactoryProvider = kosmos.tileHapticsViewModelFactoryProvider

    @Composable
    private fun TestTile(tile: TileViewModel, iconOnly: Boolean) {
        PlatformTheme {
            Tile(
                tile = tile,
                iconOnly = iconOnly,
                squishiness = { 1f },
                coroutineScope = rememberCoroutineScope(),
                bounceableInfo =
                    BounceableInfo(
                        BounceableTileViewModel(),
                        previousTile = null,
                        nextTile = null,
                        bounceEnd = true,
                    ),
                tileHapticsViewModelFactoryProvider = tileHapticsViewModelFactoryProvider,
                detailsViewModel = null,
            )
        }
    }

    @Test
    fun click_largeTile_shouldReceiveClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(QSTile.State().apply { label = "largeTile" })
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = false) }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("largeTile").performClick()

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.CLICK)
    }

    @Test
    fun click_largeDualTargetTile_shouldReceiveClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(
            QSTile.State().apply {
                label = "largeDualTargetTile"
                handlesSecondaryClick = true
            }
        )
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = false) }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("largeDualTargetTile").performClick()

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.CLICK)
    }

    @Test
    fun click_smallTile_shouldReceiveClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(QSTile.State().apply { contentDescription = "smallTile" })
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = true) }
        composeRule.waitForIdle()

        composeRule.onNodeWithContentDescription("smallTile").performClick()

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.CLICK)
    }

    @Test
    fun click_smallDualTargetTile_shouldReceiveSecondaryClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(
            QSTile.State().apply {
                contentDescription = "smallDualTargetTile"
                handlesSecondaryClick = true
            }
        )
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = true) }
        composeRule.waitForIdle()

        composeRule.onNodeWithContentDescription("smallDualTargetTile").performClick()

        assertThat(tile.interactions)
            .containsExactly(InteractableFakeQSTile.Interaction.SECONDARY_CLICK)
    }

    @Test
    fun longClick_largeTile_shouldReceiveLongClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(QSTile.State().apply { label = "largeTile" })
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = false) }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("largeTile").performTouchInput { longClick() }

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.LONG_CLICK)
    }

    @Test
    fun longClick_largeDualTargetTile_shouldReceiveLongClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(
            QSTile.State().apply {
                label = "largeDualTargetTile"
                handlesSecondaryClick = true
            }
        )
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = false) }
        composeRule.waitForIdle()

        composeRule.onNodeWithText("largeDualTargetTile").performTouchInput { longClick() }

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.LONG_CLICK)
    }

    @Test
    fun longClick_smallTile_shouldReceiveLongClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(QSTile.State().apply { contentDescription = "smallTile" })
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = true) }
        composeRule.waitForIdle()

        composeRule.onNodeWithContentDescription("smallTile").performTouchInput { longClick() }

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.LONG_CLICK)
    }

    @Test
    fun longClick_smallDualTargetTile_shouldReceiveClick() {
        val tile = InteractableFakeQSTile()
        tile.fakeTile.changeState(
            QSTile.State().apply {
                contentDescription = "smallDualTargetTile"
                handlesSecondaryClick = true
            }
        )
        val viewModel = TileViewModel(tile.fakeTile, TileSpec.Companion.create("test"))

        composeRule.setContent { TestTile(viewModel, iconOnly = true) }
        composeRule.waitForIdle()

        composeRule.onNodeWithContentDescription("smallDualTargetTile").performTouchInput {
            longClick()
        }

        assertThat(tile.interactions).containsExactly(InteractableFakeQSTile.Interaction.CLICK)
    }

    private class InteractableFakeQSTile {
        val interactions = mutableListOf<Interaction>()

        val fakeTile =
            FakeQSTile(
                user = 0,
                available = true,
                onClick = { interactions.add(Interaction.CLICK) },
                onLongClick = { interactions.add(Interaction.LONG_CLICK) },
                onSecondaryClick = { interactions.add(Interaction.SECONDARY_CLICK) },
            )

        enum class Interaction {
            CLICK,
            SECONDARY_CLICK,
            LONG_CLICK,
        }
    }
}
+16 −4
Original line number Diff line number Diff line
@@ -20,7 +20,13 @@ import com.android.internal.logging.InstanceId
import com.android.systemui.animation.Expandable
import com.android.systemui.plugins.qs.QSTile

class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
class FakeQSTile(
    var user: Int,
    var available: Boolean = true,
    val onClick: () -> Unit = {},
    val onLongClick: () -> Unit = {},
    val onSecondaryClick: () -> Unit = {},
) : QSTile {
    private var tileSpec: String? = null
    private var destroyed = false
    var hasDetailsViewModel: Boolean = true
@@ -54,11 +60,17 @@ class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
        callbacks.clear()
    }

    override fun click(expandable: Expandable?) {}
    override fun click(expandable: Expandable?) {
        onClick()
    }

    override fun secondaryClick(expandable: Expandable?) {}
    override fun secondaryClick(expandable: Expandable?) {
        onSecondaryClick()
    }

    override fun longClick(expandable: Expandable?) {}
    override fun longClick(expandable: Expandable?) {
        onLongClick()
    }

    override fun userSwitch(currentUser: Int) {
        user = currentUser