Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +108 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.qs.composefragment.viewmodel import android.app.StatusBarManager import android.content.testableContext import android.graphics.Rect import android.testing.TestableLooper.RunWithLooper import androidx.compose.runtime.snapshots.Snapshot import androidx.test.ext.junit.runners.AndroidJUnit4 Loading @@ -43,6 +44,7 @@ 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.util.animation.DisappearParameters import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope Loading Loading @@ -375,6 +377,92 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } } @Test fun applyQsScrollPositionForClipping() = with(kosmos) { testScope.testWithinLifecycle { val left = 1f val top = 3f val right = 5f val bottom = 7f underTest.applyNewQsScrollerBounds(left, top, right, bottom) assertThat(qsMediaHost.currentClipping) .isEqualTo(Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())) } } @Test fun shouldUpdateMediaSquishiness_inSplitShadeFalse_mediaSquishinessSet() = with(kosmos) { testScope.testWithinLifecycle { underTest.isInSplitShade = false underTest.squishinessFraction = 0.3f underTest.shouldUpdateSquishinessOnMedia = true Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(0.01f).of(0.3f) underTest.shouldUpdateSquishinessOnMedia = false Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(0.01f).of(1f) } } @Test fun inSplitShade_differentStatusBarState_mediaSquishinessSet() = with(kosmos) { testScope.testWithinLifecycle { underTest.isInSplitShade = true underTest.squishinessFraction = 0.3f sysuiStatusBarStateController.setState(StatusBarState.SHADE) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(0.3f) sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD) runCurrent() Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(1f) sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED) runCurrent() Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(1f) } } @Test fun disappearParams() = with(kosmos) { testScope.testWithinLifecycle { setMediaState(ACTIVE_MEDIA) setConfigurationForMediaInRow(false) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qqsMediaHost.disappearParameters) .isEqualTo(disappearParamsColumn) assertThat(underTest.qsMediaHost.disappearParameters) .isEqualTo(disappearParamsColumn) setConfigurationForMediaInRow(true) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qqsMediaHost.disappearParameters).isEqualTo(disappearParamsRow) assertThat(underTest.qsMediaHost.disappearParameters).isEqualTo(disappearParamsRow) } } private fun TestScope.setMediaState(state: MediaState) { with(kosmos) { val activeMedia = state == ACTIVE_MEDIA Loading Loading @@ -404,6 +492,26 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } private const val epsilon = 0.001f private val disappearParamsColumn = DisappearParameters().apply { fadeStartPosition = 0.95f disappearStart = 0f disappearEnd = 0.95f disappearSize.set(1f, 0f) gonePivot.set(0f, 0f) contentTranslationFraction.set(0f, 1f) } private val disappearParamsRow = DisappearParameters().apply { fadeStartPosition = 0.95f disappearStart = 0f disappearEnd = 0.6f disappearSize.set(0f, 0.4f) gonePivot.set(1f, 0f) contentTranslationFraction.set(0.25f, 1f) } } } Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +27 −5 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource Loading Loading @@ -124,6 +125,7 @@ import com.android.systemui.qs.ui.composable.QuickSettingsShade import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.res.R import com.android.systemui.util.LifecycleFragment import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection import com.android.systemui.util.println Loading Loading @@ -447,8 +449,7 @@ constructor( } override fun setShouldUpdateSquishinessOnMedia(shouldUpdate: Boolean) { super.setShouldUpdateSquishinessOnMedia(shouldUpdate) // TODO (b/353253280) viewModel.shouldUpdateSquishinessOnMedia = shouldUpdate } override fun setInSplitShade(isInSplitShade: Boolean) { Loading Loading @@ -660,7 +661,20 @@ constructor( Column( modifier = Modifier.offset { Modifier.onPlaced { coordinates -> val positionOnScreen = coordinates.positionOnScreen() val left = positionOnScreen.x val right = left + coordinates.size.width val top = positionOnScreen.y val bottom = top + coordinates.size.height viewModel.applyNewQsScrollerBounds( left = left, top = top, right = right, bottom = bottom, ) } .offset { IntOffset( x = 0, y = viewModel.qsScrollTranslationY.fastRoundToInt(), Loading Loading @@ -704,7 +718,10 @@ constructor( val Media = @Composable { if (viewModel.qsMediaVisible) { MediaObject(mediaHost = viewModel.qsMediaHost) MediaObject( mediaHost = viewModel.qsMediaHost, update = { translationY = viewModel.qsMediaTranslationY }, ) } } Box( Loading Loading @@ -987,7 +1004,11 @@ private fun Modifier.gesturesDisabled(disabled: Boolean) = } @Composable private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) { private fun MediaObject( mediaHost: MediaHost, modifier: Modifier = Modifier, update: UniqueObjectHostView.() -> Unit = {}, ) { Box { AndroidView( modifier = modifier, Loading @@ -1000,6 +1021,7 @@ private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) { ) } }, update = { view -> view.update() }, onReset = {}, ) } Loading packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +83 −3 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.LifecycleCoroutineScope import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.BouncerPanelExpansionCalculator import com.android.systemui.Dumpable Loading Loading @@ -270,6 +271,23 @@ constructor( val qsMediaInRow: Boolean get() = qsMediaInRowViewModel.shouldMediaShowInRow var shouldUpdateSquishinessOnMedia by mutableStateOf(false) val qsMediaTranslationY by derivedStateOf { if ( qsExpansion > 0f && !isKeyguardState && !qqsMediaVisible && !qsMediaInRow && !isInSplitShade ) { val interpolation = Interpolators.ACCELERATE.getInterpolation(1f - qsExpansion) -qsMediaHost.hostView.height * 1.3f * interpolation } else { 0f } } val animateTilesExpansion: Boolean get() = inFirstPage && !mediaSuddenlyAppearingInLandscape Loading Loading @@ -297,6 +315,18 @@ constructor( MediaHostState.EXPANDED } private val shouldApplySquishinessToMedia by derivedStateOf { shouldUpdateSquishinessOnMedia || (isInSplitShade && statusBarState == StatusBarState.SHADE) } private val mediaSquishiness by derivedStateOf { if (shouldApplySquishinessToMedia) { squishinessFraction } else { 1f } } private var qsBounds by mutableStateOf(Rect()) private val constrainedSquishinessFraction: Float Loading Loading @@ -355,8 +385,6 @@ constructor( private val isOverscrolling: Boolean get() = overScrollAmount != 0 private var shouldUpdateMediaSquishiness by mutableStateOf(false) private val forceQs by derivedStateOf { (isQsExpanded || isStackScrollerOverscrolling) && (isKeyguardState && !showCollapsedOnKeyguard) Loading Loading @@ -394,11 +422,26 @@ constructor( ), ) fun applyNewQsScrollerBounds(left: Float, top: Float, right: Float, bottom: Float) { if (usingMedia) { qsMediaHost.currentClipping.set( left.toInt(), top.toInt(), right.toInt(), bottom.toInt(), ) } } override suspend fun onActivated(): Nothing { initMediaHosts() // init regardless of using media (same as current QS). coroutineScope { launch { hydrateSquishinessInteractor() } if (usingMedia) { launch { hydrateQqsMediaExpansion() } launch { hydrateMediaSquishiness() } launch { hydrateMediaDisappearParameters() } } launch { hydrator.activate() } launch { containerViewModel.activate() } launch { qqsMediaInRowViewModel.activate() } Loading Loading @@ -429,6 +472,21 @@ constructor( snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it } } private suspend fun hydrateMediaSquishiness() { snapshotFlow { mediaSquishiness }.collect { qsMediaHost.squishFraction = it } } private suspend fun hydrateMediaDisappearParameters() { coroutineScope { launch { snapshotFlow { qqsMediaInRow }.collect { qqsMediaHost.applyDisappearParameters(it) } } launch { snapshotFlow { qsMediaInRow }.collect { qsMediaHost.applyDisappearParameters(it) } } } } override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { Loading Loading @@ -474,6 +532,9 @@ constructor( println("qsMediaInRow", qsMediaInRow) println("collapsedLandscapeMedia", collapsedLandscapeMedia) println("qqsMediaExpansion", qqsMediaExpansion) println("shouldUpdateSquishinessOnMedia", shouldUpdateSquishinessOnMedia) println("mediaSquishiness", mediaSquishiness) println("qsMediaTranslationY", qsMediaTranslationY) } } } Loading Loading @@ -510,3 +571,22 @@ private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> { // lazily. .onStart { emit(mediaHost.visible) } } // Taken from QSPanelControllerBase private fun MediaHost.applyDisappearParameters(inRow: Boolean) { disappearParameters.apply { fadeStartPosition = 0.95f disappearStart = 0f if (inRow) { disappearSize.set(0f, 0.4f) gonePivot.set(1f, 0f) contentTranslationFraction.set(0.25f, 1f) disappearEnd = 0.6f } else { disappearSize.set(1f, 0f) gonePivot.set(0f, 0f) contentTranslationFraction.set(0f, 1f) disappearEnd = 0.95f } } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +108 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package com.android.systemui.qs.composefragment.viewmodel import android.app.StatusBarManager import android.content.testableContext import android.graphics.Rect import android.testing.TestableLooper.RunWithLooper import androidx.compose.runtime.snapshots.Snapshot import androidx.test.ext.junit.runners.AndroidJUnit4 Loading @@ -43,6 +44,7 @@ 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.util.animation.DisappearParameters import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope Loading Loading @@ -375,6 +377,92 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } } @Test fun applyQsScrollPositionForClipping() = with(kosmos) { testScope.testWithinLifecycle { val left = 1f val top = 3f val right = 5f val bottom = 7f underTest.applyNewQsScrollerBounds(left, top, right, bottom) assertThat(qsMediaHost.currentClipping) .isEqualTo(Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())) } } @Test fun shouldUpdateMediaSquishiness_inSplitShadeFalse_mediaSquishinessSet() = with(kosmos) { testScope.testWithinLifecycle { underTest.isInSplitShade = false underTest.squishinessFraction = 0.3f underTest.shouldUpdateSquishinessOnMedia = true Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(0.01f).of(0.3f) underTest.shouldUpdateSquishinessOnMedia = false Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(0.01f).of(1f) } } @Test fun inSplitShade_differentStatusBarState_mediaSquishinessSet() = with(kosmos) { testScope.testWithinLifecycle { underTest.isInSplitShade = true underTest.squishinessFraction = 0.3f sysuiStatusBarStateController.setState(StatusBarState.SHADE) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(0.3f) sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD) runCurrent() Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(1f) sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED) runCurrent() Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qsMediaHost.squishFraction).isWithin(epsilon).of(1f) } } @Test fun disappearParams() = with(kosmos) { testScope.testWithinLifecycle { setMediaState(ACTIVE_MEDIA) setConfigurationForMediaInRow(false) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qqsMediaHost.disappearParameters) .isEqualTo(disappearParamsColumn) assertThat(underTest.qsMediaHost.disappearParameters) .isEqualTo(disappearParamsColumn) setConfigurationForMediaInRow(true) Snapshot.sendApplyNotifications() runCurrent() assertThat(underTest.qqsMediaHost.disappearParameters).isEqualTo(disappearParamsRow) assertThat(underTest.qsMediaHost.disappearParameters).isEqualTo(disappearParamsRow) } } private fun TestScope.setMediaState(state: MediaState) { with(kosmos) { val activeMedia = state == ACTIVE_MEDIA Loading Loading @@ -404,6 +492,26 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } private const val epsilon = 0.001f private val disappearParamsColumn = DisappearParameters().apply { fadeStartPosition = 0.95f disappearStart = 0f disappearEnd = 0.95f disappearSize.set(1f, 0f) gonePivot.set(0f, 0f) contentTranslationFraction.set(0f, 1f) } private val disappearParamsRow = DisappearParameters().apply { fadeStartPosition = 0.95f disappearStart = 0f disappearEnd = 0.6f disappearSize.set(0f, 0.4f) gonePivot.set(1f, 0f) contentTranslationFraction.set(0.25f, 1f) } } } Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +27 −5 Original line number Diff line number Diff line Loading @@ -70,6 +70,7 @@ import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource Loading Loading @@ -124,6 +125,7 @@ import com.android.systemui.qs.ui.composable.QuickSettingsShade import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.res.R import com.android.systemui.util.LifecycleFragment import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.asIndenting import com.android.systemui.util.printSection import com.android.systemui.util.println Loading Loading @@ -447,8 +449,7 @@ constructor( } override fun setShouldUpdateSquishinessOnMedia(shouldUpdate: Boolean) { super.setShouldUpdateSquishinessOnMedia(shouldUpdate) // TODO (b/353253280) viewModel.shouldUpdateSquishinessOnMedia = shouldUpdate } override fun setInSplitShade(isInSplitShade: Boolean) { Loading Loading @@ -660,7 +661,20 @@ constructor( Column( modifier = Modifier.offset { Modifier.onPlaced { coordinates -> val positionOnScreen = coordinates.positionOnScreen() val left = positionOnScreen.x val right = left + coordinates.size.width val top = positionOnScreen.y val bottom = top + coordinates.size.height viewModel.applyNewQsScrollerBounds( left = left, top = top, right = right, bottom = bottom, ) } .offset { IntOffset( x = 0, y = viewModel.qsScrollTranslationY.fastRoundToInt(), Loading Loading @@ -704,7 +718,10 @@ constructor( val Media = @Composable { if (viewModel.qsMediaVisible) { MediaObject(mediaHost = viewModel.qsMediaHost) MediaObject( mediaHost = viewModel.qsMediaHost, update = { translationY = viewModel.qsMediaTranslationY }, ) } } Box( Loading Loading @@ -987,7 +1004,11 @@ private fun Modifier.gesturesDisabled(disabled: Boolean) = } @Composable private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) { private fun MediaObject( mediaHost: MediaHost, modifier: Modifier = Modifier, update: UniqueObjectHostView.() -> Unit = {}, ) { Box { AndroidView( modifier = modifier, Loading @@ -1000,6 +1021,7 @@ private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) { ) } }, update = { view -> view.update() }, onReset = {}, ) } Loading
packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +83 −3 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.LifecycleCoroutineScope import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.BouncerPanelExpansionCalculator import com.android.systemui.Dumpable Loading Loading @@ -270,6 +271,23 @@ constructor( val qsMediaInRow: Boolean get() = qsMediaInRowViewModel.shouldMediaShowInRow var shouldUpdateSquishinessOnMedia by mutableStateOf(false) val qsMediaTranslationY by derivedStateOf { if ( qsExpansion > 0f && !isKeyguardState && !qqsMediaVisible && !qsMediaInRow && !isInSplitShade ) { val interpolation = Interpolators.ACCELERATE.getInterpolation(1f - qsExpansion) -qsMediaHost.hostView.height * 1.3f * interpolation } else { 0f } } val animateTilesExpansion: Boolean get() = inFirstPage && !mediaSuddenlyAppearingInLandscape Loading Loading @@ -297,6 +315,18 @@ constructor( MediaHostState.EXPANDED } private val shouldApplySquishinessToMedia by derivedStateOf { shouldUpdateSquishinessOnMedia || (isInSplitShade && statusBarState == StatusBarState.SHADE) } private val mediaSquishiness by derivedStateOf { if (shouldApplySquishinessToMedia) { squishinessFraction } else { 1f } } private var qsBounds by mutableStateOf(Rect()) private val constrainedSquishinessFraction: Float Loading Loading @@ -355,8 +385,6 @@ constructor( private val isOverscrolling: Boolean get() = overScrollAmount != 0 private var shouldUpdateMediaSquishiness by mutableStateOf(false) private val forceQs by derivedStateOf { (isQsExpanded || isStackScrollerOverscrolling) && (isKeyguardState && !showCollapsedOnKeyguard) Loading Loading @@ -394,11 +422,26 @@ constructor( ), ) fun applyNewQsScrollerBounds(left: Float, top: Float, right: Float, bottom: Float) { if (usingMedia) { qsMediaHost.currentClipping.set( left.toInt(), top.toInt(), right.toInt(), bottom.toInt(), ) } } override suspend fun onActivated(): Nothing { initMediaHosts() // init regardless of using media (same as current QS). coroutineScope { launch { hydrateSquishinessInteractor() } if (usingMedia) { launch { hydrateQqsMediaExpansion() } launch { hydrateMediaSquishiness() } launch { hydrateMediaDisappearParameters() } } launch { hydrator.activate() } launch { containerViewModel.activate() } launch { qqsMediaInRowViewModel.activate() } Loading Loading @@ -429,6 +472,21 @@ constructor( snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it } } private suspend fun hydrateMediaSquishiness() { snapshotFlow { mediaSquishiness }.collect { qsMediaHost.squishFraction = it } } private suspend fun hydrateMediaDisappearParameters() { coroutineScope { launch { snapshotFlow { qqsMediaInRow }.collect { qqsMediaHost.applyDisappearParameters(it) } } launch { snapshotFlow { qsMediaInRow }.collect { qsMediaHost.applyDisappearParameters(it) } } } } override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { Loading Loading @@ -474,6 +532,9 @@ constructor( println("qsMediaInRow", qsMediaInRow) println("collapsedLandscapeMedia", collapsedLandscapeMedia) println("qqsMediaExpansion", qqsMediaExpansion) println("shouldUpdateSquishinessOnMedia", shouldUpdateSquishinessOnMedia) println("mediaSquishiness", mediaSquishiness) println("qsMediaTranslationY", qsMediaTranslationY) } } } Loading Loading @@ -510,3 +571,22 @@ private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> { // lazily. .onStart { emit(mediaHost.visible) } } // Taken from QSPanelControllerBase private fun MediaHost.applyDisappearParameters(inRow: Boolean) { disappearParameters.apply { fadeStartPosition = 0.95f disappearStart = 0f if (inRow) { disappearSize.set(0f, 0.4f) gonePivot.set(1f, 0f) contentTranslationFraction.set(0.25f, 1f) disappearEnd = 0.6f } else { disappearSize.set(1f, 0f) gonePivot.set(0f, 0f) contentTranslationFraction.set(0f, 1f) disappearEnd = 0.95f } } }