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

Commit 3109ea2f authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Implement expansion to QS in keyguard

When in keyguard, swiping from the top directly expands QS instead of
expanding QQS. Wire up the corresponding signals to achieve this (as is
done in QSImpl).

Test: manual
Test: atest QSFragmentComposeViewModelForceQSTest
Fixes: 370046812
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Change-Id: Icd71bcf3fd6bad1b786bf13e99b7814073a8f1c3
parent f6bcf6a0
Loading
Loading
Loading
Loading
+69 −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.composefragment.viewmodel

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.testing.TestLifecycleOwner
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.testKosmos
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before

@OptIn(ExperimentalCoroutinesApi::class)
abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() {
    protected val kosmos = testKosmos()

    protected val lifecycleOwner =
        TestLifecycleOwner(
            initialState = Lifecycle.State.CREATED,
            coroutineDispatcher = kosmos.testDispatcher,
        )

    protected val underTest by lazy {
        kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope)
    }

    @Before
    fun setUp() {
        Dispatchers.setMain(kosmos.testDispatcher)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }

    protected inline fun TestScope.testWithinLifecycle(
        crossinline block: suspend TestScope.() -> TestResult
    ): TestResult {
        return runTest {
            lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
            lifecycleOwner.lifecycleScope.launch { underTest.activate() }
            block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
        }
    }
}
+98 −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.composefragment.viewmodel

import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@SmallTest
@RunWith(Parameterized::class)
@RunWithLooper
class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) :
    AbstractQSFragmentComposeViewModelTest() {

    @Test
    fun forceQs_orRealExpansion() =
        with(kosmos) {
            testScope.testWithinLifecycle {
                val expansionState by collectLastValue(underTest.expansionState)

                with(testData) {
                    sysuiStatusBarStateController.setState(statusBarState)
                    underTest.isQSExpanded = expanded
                    underTest.isStackScrollerOverscrolling = stackScrollerOverScrolling
                    fakeDeviceEntryRepository.setBypassEnabled(bypassEnabled)
                    underTest.isTransitioningToFullShade = transitioningToFullShade
                    underTest.isInSplitShade = inSplitShade

                    underTest.qsExpansionValue = EXPANSION
                    assertThat(expansionState!!.progress)
                        .isEqualTo(if (expectedForceQS) 1f else EXPANSION)
                }
            }
        }

    data class TestData(
        val statusBarState: Int,
        val expanded: Boolean,
        val stackScrollerOverScrolling: Boolean,
        val bypassEnabled: Boolean,
        val transitioningToFullShade: Boolean,
        val inSplitShade: Boolean,
    ) {
        private val inKeyguard = statusBarState == StatusBarState.KEYGUARD

        private val showCollapsedOnKeyguard =
            bypassEnabled || (transitioningToFullShade && !inSplitShade)

        val expectedForceQS =
            (expanded || stackScrollerOverScrolling) && (inKeyguard && !showCollapsedOnKeyguard)
    }

    companion object {
        private const val EXPANSION = 0.3f

        @Parameterized.Parameters(name = "{0}")
        @JvmStatic
        fun createTestData(): List<TestData> {
            return statusBarStates.flatMap { statusBarState ->
                (0u..31u).map { bitfield ->
                    TestData(
                        statusBarState,
                        expanded = (bitfield or 1u) == 1u,
                        stackScrollerOverScrolling = (bitfield or 2u) == 1u,
                        bypassEnabled = (bitfield or 4u) == 1u,
                        transitioningToFullShade = (bitfield or 8u) == 1u,
                        inSplitShade = (bitfield or 16u) == 1u,
                    )
                }
            }
        }

        private val statusBarStates =
            setOf(StatusBarState.SHADE, StatusBarState.KEYGUARD, StatusBarState.SHADE_LOCKED)
    }
}
+1 −49
Original line number Diff line number Diff line
@@ -19,15 +19,10 @@ package com.android.systemui.qs.composefragment.viewmodel
import android.app.StatusBarManager
import android.content.testableContext
import android.testing.TestableLooper.RunWithLooper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.fgsManagerController
import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
@@ -37,48 +32,15 @@ import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestResult
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@OptIn(ExperimentalCoroutinesApi::class)
class QSFragmentComposeViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos()

    private val lifecycleOwner =
        TestLifecycleOwner(
            initialState = Lifecycle.State.CREATED,
            coroutineDispatcher = kosmos.testDispatcher,
        )

    private val underTest by lazy {
        kosmos.qsFragmentComposeViewModelFactory.create(lifecycleOwner.lifecycleScope)
    }

    @Before
    fun setUp() {
        Dispatchers.setMain(kosmos.testDispatcher)
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }
class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() {

    @Test
    fun qsExpansionValueChanges_correctExpansionState() =
@@ -224,16 +186,6 @@ class QSFragmentComposeViewModelTest : SysuiTestCase() {
            }
        }

    private inline fun TestScope.testWithinLifecycle(
        crossinline block: suspend TestScope.() -> TestResult
    ): TestResult {
        return runTest {
            lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED)
            lifecycleOwner.lifecycleScope.launch { underTest.activate() }
            block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) }
        }
    }

    companion object {
        private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS

+2 −2
Original line number Diff line number Diff line
@@ -332,7 +332,7 @@ constructor(
    }

    override fun setOverscrolling(overscrolling: Boolean) {
        viewModel.stackScrollerOverscrollingValue = overscrolling
        viewModel.isStackScrollerOverscrolling = overscrolling
    }

    override fun setExpanded(qsExpanded: Boolean) {
@@ -411,7 +411,7 @@ constructor(
        qsTransitionFraction: Float,
        qsSquishinessFraction: Float,
    ) {
        viewModel.transitioningToFullShadeValue = isTransitioningToFullShade
        viewModel.isTransitioningToFullShade = isTransitioningToFullShade
        viewModel.lockscreenToShadeProgressValue = qsTransitionFraction
        if (isTransitioningToFullShade) {
            viewModel.squishinessFractionValue = qsSquishinessFraction
+99 −13
Original line number Diff line number Diff line
@@ -24,9 +24,11 @@ import androidx.lifecycle.LifecycleCoroutineScope
import com.android.systemui.Dumpable
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
@@ -35,7 +37,6 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.util.LargeScreenUtils
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printSection
@@ -44,6 +45,7 @@ import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.io.PrintWriter
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -51,9 +53,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.io.PrintWriter

class QSFragmentComposeViewModel
@AssistedInject
@@ -63,7 +65,7 @@ constructor(
    private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
    private val footerActionsController: FooterActionsController,
    private val sysuiStatusBarStateController: SysuiStatusBarStateController,
    private val keyguardBypassController: KeyguardBypassController,
    private val deviceEntryInteractor: DeviceEntryInteractor,
    private val disableFlagsRepository: DisableFlagsRepository,
    private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
    private val configurationInteractor: ConfigurationInteractor,
@@ -134,7 +136,7 @@ constructor(
    private val _headerAnimating = MutableStateFlow(false)

    private val _stackScrollerOverscrolling = MutableStateFlow(false)
    var stackScrollerOverscrollingValue: Boolean
    var isStackScrollerOverscrolling: Boolean
        get() = _stackScrollerOverscrolling.value
        set(value) {
            _stackScrollerOverscrolling.value = value
@@ -153,8 +155,6 @@ constructor(
                disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
            )

    private val _showCollapsedOnKeyguard = MutableStateFlow(false)

    private val _keyguardAndExpanded = MutableStateFlow(false)

    /**
@@ -180,25 +180,59 @@ constructor(

                awaitClose { sysuiStatusBarStateController.removeCallback(callback) }
            }
            .onStart { emit(sysuiStatusBarStateController.state) }
            .stateIn(
                lifecycleScope,
                SharingStarted.WhileSubscribed(),
                sysuiStatusBarStateController.state,
            )

    private val isKeyguardState =
        statusBarState
            .map { it == StatusBarState.KEYGUARD }
            .stateIn(
                lifecycleScope,
                SharingStarted.WhileSubscribed(),
                statusBarState.value == StatusBarState.KEYGUARD,
            )

    private val _viewHeight = MutableStateFlow(0)

    private val _headerTranslation = MutableStateFlow(0f)

    private val _inSplitShade = MutableStateFlow(false)
    var isInSplitShade: Boolean
        get() = _inSplitShade.value
        set(value) {
            _inSplitShade.value = value
        }

    private val _transitioningToFullShade = MutableStateFlow(false)
    var transitioningToFullShadeValue: Boolean
    var isTransitioningToFullShade: Boolean
        get() = _transitioningToFullShade.value
        set(value) {
            _transitioningToFullShade.value = value
        }

    private val isBypassEnabled = deviceEntryInteractor.isBypassEnabled

    private val showCollapsedOnKeyguard =
        combine(
                isBypassEnabled,
                _transitioningToFullShade,
                _inSplitShade,
                ::calculateShowCollapsedOnKeyguard,
            )
            .stateIn(
                lifecycleScope,
                SharingStarted.WhileSubscribed(),
                calculateShowCollapsedOnKeyguard(
                    isBypassEnabled.value,
                    isTransitioningToFullShade,
                    isInSplitShade,
                ),
            )

    private val _lockscreenToShadeProgress = MutableStateFlow(0.0f)
    var lockscreenToShadeProgressValue: Float
        get() = _lockscreenToShadeProgress.value
@@ -225,12 +259,32 @@ constructor(
            _heightOverride.value = value
        }

    private val forceQS =
        combine(
                _qsExpanded,
                _stackScrollerOverscrolling,
                isKeyguardState,
                showCollapsedOnKeyguard,
                ::calculateForceQs,
            )
            .stateIn(
                lifecycleScope,
                SharingStarted.WhileSubscribed(),
                calculateForceQs(
                    isQSExpanded,
                    isStackScrollerOverscrolling,
                    isKeyguardState.value,
                    showCollapsedOnKeyguard.value,
                ),
            )

    val expansionState: StateFlow<QSExpansionState> =
        combine(_stackScrollerOverscrolling, _qsExpanded, _qsExpansion) { args: Array<Any> ->
                val expansion = args[2] as Float
                QSExpansionState(expansion.coerceIn(0f, 1f))
            }
            .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), QSExpansionState(0f))
        combine(_qsExpansion, forceQS, ::calculateExpansionState)
            .stateIn(
                lifecycleScope,
                SharingStarted.WhileSubscribed(),
                calculateExpansionState(_qsExpansion.value, forceQS.value),
            )

    /**
     * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for
@@ -261,13 +315,17 @@ constructor(
                println("panelExpansionFraction", panelExpansionFractionValue)
                println("squishinessFraction", squishinessFractionValue)
                println("expansionState", expansionState.value)
                println("forceQS", forceQS.value)
            }
            printSection("Shade state") {
                println("stackOverscrolling", stackScrollerOverscrollingValue)
                println("stackOverscrolling", isStackScrollerOverscrolling)
                println("statusBarState", StatusBarState.toString(statusBarState.value))
                println("isKeyguardState", isKeyguardState.value)
                println("isSmallScreen", isSmallScreenValue)
                println("heightOverride", "${heightOverrideValue}px")
                println("qqsHeaderHeight", "${qqsHeaderHeight.value}px")
                println("isSplitShade", isInSplitShade)
                println("showCollapsedOnKeyguard", showCollapsedOnKeyguard.value)
            }
        }
    }
@@ -284,3 +342,31 @@ constructor(
private fun Float.constrainSquishiness(): Float {
    return (0.1f + this * 0.9f).coerceIn(0f, 1f)
}

// Helper methods for combining flows.

private fun calculateExpansionState(expansion: Float, forceQs: Boolean): QSExpansionState {
    return if (forceQs) {
        QSExpansionState(1f)
    } else {
        QSExpansionState(expansion.coerceIn(0f, 1f))
    }
}

private fun calculateForceQs(
    isQSExpanded: Boolean,
    isStackOverScrolling: Boolean,
    isKeyguardShowing: Boolean,
    shouldShowCollapsedOnKeyguard: Boolean,
): Boolean {
    return (isQSExpanded || isStackOverScrolling) &&
        (isKeyguardShowing && !shouldShowCollapsedOnKeyguard)
}

private fun calculateShowCollapsedOnKeyguard(
    isBypassEnabled: Boolean,
    isTransitioningToFullShade: Boolean,
    isInSplitShade: Boolean,
): Boolean {
    return isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade)
}
Loading