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

Commit 436a937d authored by András Kurucz's avatar András Kurucz
Browse files

[Flexiglass] Wire in CUJ_NOTIFICATION_SHADE_SCROLL_FLING

CUJ_NOTIFICATION_SHADE_SCROLL_FLING tracks any scrolls on the
Notifications. It starts when a swipe starts to scroll up/down the
stack, and stops when the scrollable element has settled including any
flings or overscroll effects.
This CL starts tracking it from the Notification placeholder composable.

Bug: 360100111
Test: OffsetOverscrollEffectTest
Test: manually verify the CUJ marker calls on scrolls, flings and overscrolls
Flag: com.android.systemui.scene_container
Change-Id: Ib1a98438de987711023a28fc2a7cdf95675e8f5e
parent 341800f2
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -55,7 +55,12 @@ open class BaseContentOverscrollEffect(
        get() = animatable.value

    override val isInProgress: Boolean
        get() = overscrollDistance != 0f
        /**
         * We need both checks, because [overscrollDistance] can be
         * - zero while it is already being animated, if the animation starts from 0
         * - greater than zero without an animation, if the content is still being dragged
         */
        get() = overscrollDistance != 0f || animatable.isRunning

    override fun applyToScroll(
        delta: Offset,
+172 −9
Original line number Diff line number Diff line
@@ -16,12 +16,17 @@

package com.android.compose.gesture.effect

import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.overscroll
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalDensity
@@ -32,11 +37,14 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,7 +55,13 @@ class OffsetOverscrollEffectTest {

    private val BOX_TAG = "box"

    private data class LayoutInfo(val layoutSize: Dp, val touchSlop: Float, val density: Density) {
    private data class LayoutInfo(
        val layoutSize: Dp,
        val touchSlop: Float,
        val density: Density,
        val scrollableState: ScrollableState,
        val overscrollEffect: OverscrollEffect,
    ) {
        fun expectedOffset(currentOffset: Dp): Dp {
            return with(density) {
                OffsetOverscrollEffect.computeOffset(this, currentOffset.toPx()).toDp()
@@ -55,22 +69,29 @@ class OffsetOverscrollEffectTest {
        }
    }

    private fun setupOverscrollableBox(scrollableOrientation: Orientation): LayoutInfo {
    private fun setupOverscrollableBox(
        scrollableOrientation: Orientation,
        canScroll: () -> Boolean,
    ): LayoutInfo {
        val layoutSize: Dp = 200.dp
        var touchSlop: Float by Delegates.notNull()
        // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
        // detected as a drag event.
        lateinit var density: Density
        lateinit var scrollableState: ScrollableState
        lateinit var overscrollEffect: OverscrollEffect

        rule.setContent {
            density = LocalDensity.current
            touchSlop = LocalViewConfiguration.current.touchSlop
            val overscrollEffect = rememberOffsetOverscrollEffect()
            scrollableState = rememberScrollableState { if (canScroll()) it else 0f }
            overscrollEffect = rememberOffsetOverscrollEffect()

            Box(
                Modifier.overscroll(overscrollEffect)
                    // A scrollable that does not consume the scroll gesture.
                    .scrollable(
                        state = rememberScrollableState { 0f },
                        state = scrollableState,
                        orientation = scrollableOrientation,
                        overscrollEffect = overscrollEffect,
                    )
@@ -78,12 +99,16 @@ class OffsetOverscrollEffectTest {
                    .testTag(BOX_TAG)
            )
        }
        return LayoutInfo(layoutSize, touchSlop, density)
        return LayoutInfo(layoutSize, touchSlop, density, scrollableState, overscrollEffect)
    }

    @Test
    fun applyVerticalOffset_duringVerticalOverscroll() {
        val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { false },
            )

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

@@ -99,7 +124,11 @@ class OffsetOverscrollEffectTest {

    @Test
    fun applyNoOffset_duringHorizontalOverscroll() {
        val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { false },
            )

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

@@ -113,7 +142,11 @@ class OffsetOverscrollEffectTest {

    @Test
    fun backToZero_afterOverscroll() {
        val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { false },
            )

        rule.onRoot().performTouchInput {
            down(center)
@@ -131,7 +164,11 @@ class OffsetOverscrollEffectTest {

    @Test
    fun offsetOverscroll_followTheTouchPointer() {
        val info = setupOverscrollableBox(scrollableOrientation = Orientation.Vertical)
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { false },
            )

        // First gesture, drag down.
        rule.onRoot().performTouchInput {
@@ -165,4 +202,130 @@ class OffsetOverscrollEffectTest {
            .onNodeWithTag(BOX_TAG)
            .assertTopPositionInRootIsEqualTo(info.expectedOffset(-info.layoutSize))
    }

    @Test
    fun isScrollInProgress_overscroll() = runTest {
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { false },
            )

        // Start a swipe gesture, and swipe down to start an overscroll.
        rule.onRoot().performTouchInput {
            down(center)
            moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2))
        }

        assertThat(info.scrollableState.isScrollInProgress).isTrue()
        assertThat(info.overscrollEffect.isInProgress).isTrue()

        // Finish the swipe gesture.
        rule.onRoot().performTouchInput { up() }

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isTrue()

        // Wait until the overscroll returns to idle.
        rule.awaitIdle()

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isFalse()
    }

    @Test
    fun isScrollInProgress_scroll() = runTest {
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { true },
            )

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

        // Start a swipe gesture, and swipe down to scroll.
        rule.onRoot().performTouchInput {
            down(center)
            moveBy(Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2))
        }

        assertThat(info.scrollableState.isScrollInProgress).isTrue()
        assertThat(info.overscrollEffect.isInProgress).isFalse()

        // Finish the swipe gesture.
        rule.onRoot().performTouchInput { up() }

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isTrue()

        // Wait until the overscroll returns to idle.
        rule.awaitIdle()

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isFalse()
    }

    @Test
    fun isScrollInProgress_flingToScroll() = runTest {
        val info =
            setupOverscrollableBox(
                scrollableOrientation = Orientation.Vertical,
                canScroll = { true },
            )

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

        // Swipe down and leave some velocity to start a fling.
        rule.onRoot().performTouchInput {
            swipeWithVelocity(
                Offset.Zero,
                Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2),
                endVelocity = 100f,
            )
        }

        assertThat(info.scrollableState.isScrollInProgress).isTrue()
        assertThat(info.overscrollEffect.isInProgress).isFalse()

        // Wait until the fling is finished.
        rule.awaitIdle()

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isFalse()
    }

    @Test
    fun isScrollInProgress_flingToOverscroll() = runTest {
        // Start with a scrollable state.
        var canScroll by mutableStateOf(true)
        val info =
            setupOverscrollableBox(scrollableOrientation = Orientation.Vertical) { canScroll }

        rule.onNodeWithTag(BOX_TAG).assertTopPositionInRootIsEqualTo(0.dp)

        // Swipe down and leave some velocity to start a fling.
        rule.onRoot().performTouchInput {
            swipeWithVelocity(
                Offset.Zero,
                Offset(0f, info.touchSlop + info.layoutSize.toPx() / 2),
                endVelocity = 100f,
            )
        }

        assertThat(info.scrollableState.isScrollInProgress).isTrue()
        assertThat(info.overscrollEffect.isInProgress).isFalse()

        // The fling reaches the end of the scrollable region, and an overscroll starts.
        canScroll = false
        rule.mainClock.advanceTimeUntil { !info.scrollableState.isScrollInProgress }

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isTrue()

        // Wait until the overscroll returns to idle.
        rule.awaitIdle()

        assertThat(info.scrollableState.isScrollInProgress).isFalse()
        assertThat(info.overscrollEffect.isInProgress).isFalse()
    }
}
+24 −2
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -92,7 +93,11 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.gesture.NestedScrollableBound
import com.android.compose.gesture.effect.OffsetOverscrollEffect
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect
import com.android.compose.modifiers.thenIf
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
@@ -288,17 +293,19 @@ fun ContentScope.NotificationScrollingStack(
    shadeSession: SaveableSession,
    stackScrollView: NotificationScrollView,
    viewModel: NotificationsPlaceholderViewModel,
    jankMonitor: InteractionJankMonitor,
    maxScrimTop: () -> Float,
    shouldPunchHoleBehindScrim: Boolean,
    stackTopPadding: Dp,
    stackBottomPadding: Dp,
    modifier: Modifier = Modifier,
    shouldFillMaxSize: Boolean = true,
    shouldIncludeHeadsUpSpace: Boolean = true,
    shouldShowScrim: Boolean = true,
    supportNestedScrolling: Boolean,
    onEmptySpaceClick: (() -> Unit)? = null,
    modifier: Modifier = Modifier,
) {
    val composeViewRoot = LocalView.current
    val coroutineScope = shadeSession.sessionCoroutineScope()
    val density = LocalDensity.current
    val screenCornerRadius = LocalScreenCornerRadius.current
@@ -477,6 +484,21 @@ fun ContentScope.NotificationScrollingStack(
            )
        }

    val overScrollEffect: OffsetOverscrollEffect = rememberOffsetOverscrollEffect()
    // whether the stack is moving due to a swipe or fling
    val isScrollInProgress =
        scrollState.isScrollInProgress || overScrollEffect.isInProgress || scrimOffset.isRunning

    LaunchedEffect(isScrollInProgress) {
        if (isScrollInProgress) {
            jankMonitor.begin(composeViewRoot, CUJ_NOTIFICATION_SHADE_SCROLL_FLING)
            debugLog(viewModel) { "STACK scroll begins" }
        } else {
            debugLog(viewModel) { "STACK scroll ends" }
            jankMonitor.end(CUJ_NOTIFICATION_SHADE_SCROLL_FLING)
        }
    }

    Box(
        modifier =
            modifier
@@ -577,7 +599,7 @@ fun ContentScope.NotificationScrollingStack(
                        .thenIf(supportNestedScrolling) {
                            Modifier.nestedScroll(scrimNestedScrollConnection)
                        }
                        .verticalScroll(scrollState)
                        .verticalScroll(scrollState, overscrollEffect = overScrollEffect)
                        .padding(top = stackTopPadding, bottom = stackBottomPadding)
                        .fillMaxWidth()
                        .onGloballyPositioned { coordinates ->
+3 −0
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
@@ -68,6 +69,7 @@ constructor(
    private val keyguardClockViewModel: KeyguardClockViewModel,
    private val mediaCarouselController: MediaCarouselController,
    @Named(QUICK_QS_PANEL) private val mediaHost: Lazy<MediaHost>,
    private val jankMonitor: InteractionJankMonitor,
) : Overlay {
    override val key = Overlays.NotificationsShade

@@ -145,6 +147,7 @@ constructor(
                        shadeSession = shadeSession,
                        stackScrollView = stackScrollView.get(),
                        viewModel = placeholderViewModel,
                        jankMonitor = jankMonitor,
                        maxScrimTop = { 0f },
                        shouldPunchHoleBehindScrim = false,
                        stackTopPadding = notificationStackPadding,
+5 −0
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -126,6 +127,7 @@ constructor(
    private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory,
    private val mediaCarouselController: MediaCarouselController,
    @Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost,
    private val jankMonitor: InteractionJankMonitor,
) : ExclusiveActivatable(), Scene {
    override val key = Scenes.QuickSettings

@@ -165,6 +167,7 @@ constructor(
            mediaHost = mediaHost,
            modifier = modifier,
            shadeSession = shadeSession,
            jankMonitor = jankMonitor,
        )
    }

@@ -186,6 +189,7 @@ private fun ContentScope.QuickSettingsScene(
    mediaHost: MediaHost,
    modifier: Modifier = Modifier,
    shadeSession: SaveableSession,
    jankMonitor: InteractionJankMonitor,
) {
    val cutoutLocation = LocalDisplayCutout.current.location
    val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
@@ -432,6 +436,7 @@ private fun ContentScope.QuickSettingsScene(
            shadeSession = shadeSession,
            stackScrollView = notificationStackScrollView,
            viewModel = notificationsPlaceholderViewModel,
            jankMonitor = jankMonitor,
            maxScrimTop = { minNotificationStackTop.toFloat() },
            shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
            stackTopPadding = notificationStackPadding,
Loading