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

Commit ae04a4f4 authored by Shawn Lee's avatar Shawn Lee
Browse files

[flexiglass] Add Snoozeable HUN Placeholder and HUN touch handling

Creates a variant of HeadsUpNotificationSpace that can be swiped up off the screen. Currently, doing so triggers most of the side effects as in legacy, but not all (to be validated in a follow-up CL). In addition, when flexiglass is on, NSSLC creates an instance of HeadsUpTouchHelper with an empty impl for the NPVC interface, so that we can reuse the touch tracking by calling it in NSSLC's touch handling methods. The side effects that have been left unflagged in HeadsUpTouchHelper didn't seem to be broken, but it might be better to move them elsewhere later.

Bug: 340514839
Test: Verified HeadsUpTouchHelper internal state is updated correctly during swipes on HUNs via logging
Test: Verified NSSL dragging state is updated correctly via logging
Test: Verified dragging and flinging HUN placeholder over Gone scene upwards moves it off screen
Test: Verified dragging and flinging HUN placeholder downwards opens Shade scene
Test: Verified letting go of HUN placeholder before reaching thresholds animates it back to pinned position
Test: Verified changing directions while swiping HUN placeholder works as expected
Flag: com.android.systemui.scene_container
Change-Id: I62631f1f9dc8ff1e52cac2366ed310a6b3f0cc1f
parent 37230d65
Loading
Loading
Loading
Loading
+136 −0
Original line number Diff line number Diff line
@@ -19,13 +19,19 @@ package com.android.systemui.notifications.ui.composable

import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -41,9 +47,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -52,6 +60,7 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -59,10 +68,12 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -71,6 +82,7 @@ import com.android.compose.animation.scene.LowestZIndexScenePicker
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
import com.android.compose.modifiers.thenIf
import com.android.internal.policy.SystemBarUtils
import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
@@ -137,6 +149,87 @@ fun SceneScope.HeadsUpNotificationSpace(
    )
}

/**
 * A version of [HeadsUpNotificationSpace] that can be swiped up off the top edge of the screen by
 * the user. When swiped up, the heads up notification is snoozed.
 */
@Composable
fun SceneScope.SnoozeableHeadsUpNotificationSpace(
    stackScrollView: NotificationScrollView,
    viewModel: NotificationsPlaceholderViewModel,
) {
    val context = LocalContext.current
    val density = LocalDensity.current
    val statusBarHeight = SystemBarUtils.getStatusBarHeight(context)
    val headsUpPadding =
        with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() }

    val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)

    var scrollOffset by remember { mutableFloatStateOf(0f) }
    val minScrollOffset = -(statusBarHeight + headsUpPadding.toFloat())
    val maxScrollOffset = 0f

    val scrollableState = rememberScrollableState { delta ->
        consumeDeltaWithinRange(
            current = scrollOffset,
            setCurrent = { scrollOffset = it },
            min = minScrollOffset,
            max = maxScrollOffset,
            delta
        )
    }

    val nestedScrollConnection =
        object : NestedScrollConnection {
            override suspend fun onPreFling(available: Velocity): Velocity {
                if (
                    velocityOrPositionalThresholdReached(scrollOffset, minScrollOffset, available.y)
                ) {
                    scrollableState.animateScrollBy(minScrollOffset, tween())
                } else {
                    scrollableState.animateScrollBy(-minScrollOffset, tween())
                }
                return available
            }
        }

    LaunchedEffect(isHeadsUp) { scrollOffset = 0f }

    LaunchedEffect(scrollableState.isScrollInProgress) {
        if (!scrollableState.isScrollInProgress && scrollOffset <= minScrollOffset) {
            viewModel.snoozeHun()
        }
    }

    HeadsUpNotificationSpace(
        stackScrollView = stackScrollView,
        viewModel = viewModel,
        modifier =
            Modifier.absoluteOffset {
                    IntOffset(
                        x = 0,
                        y =
                            calculateHeadsUpPlaceholderYOffset(
                                scrollOffset.roundToInt(),
                                minScrollOffset.roundToInt(),
                                stackScrollView.topHeadsUpHeight
                            )
                    )
                }
                .thenIf(isHeadsUp) {
                    Modifier.verticalNestedScrollToScene(
                            bottomBehavior = NestedScrollBehavior.EdgeAlways
                        )
                        .nestedScroll(nestedScrollConnection)
                        .scrollable(
                            orientation = Orientation.Vertical,
                            state = scrollableState,
                        )
                }
    )
}

/** Adds the space where notification stack should appear in the scene. */
@Composable
fun SceneScope.ConstrainedNotificationStack(
@@ -480,6 +573,47 @@ private fun calculateCornerRadius(
    }
}

private fun calculateHeadsUpPlaceholderYOffset(
    scrollOffset: Int,
    minScrollOffset: Int,
    topHeadsUpHeight: Int,
): Int {
    return -minScrollOffset +
        (scrollOffset * (-minScrollOffset + topHeadsUpHeight) / -minScrollOffset)
}

private fun velocityOrPositionalThresholdReached(
    scrollOffset: Float,
    minScrollOffset: Float,
    availableVelocityY: Float,
): Boolean {
    return availableVelocityY < HUN_SNOOZE_VELOCITY_THRESHOLD ||
        (availableVelocityY <= 0f &&
            scrollOffset < minScrollOffset * HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION)
}

/**
 * Takes a range, current value, and delta, and updates the current value by the delta, coercing the
 * result within the given range. Returns how much of the delta was consumed.
 */
private fun consumeDeltaWithinRange(
    current: Float,
    setCurrent: (Float) -> Unit,
    min: Float,
    max: Float,
    delta: Float
): Float {
    return if (delta < 0 && current > min) {
        val remainder = (current + delta - min).coerceAtMost(0f)
        setCurrent((current + delta).coerceAtLeast(min))
        delta - remainder
    } else if (delta > 0 && current < max) {
        val remainder = (current + delta).coerceAtLeast(0f)
        setCurrent((current + delta).coerceAtMost(max))
        delta - remainder
    } else 0f
}

private inline fun debugLog(
    viewModel: NotificationsPlaceholderViewModel,
    msg: () -> Any,
@@ -514,3 +648,5 @@ private const val TAG = "FlexiNotifs"
private val DEBUG_STACK_COLOR = Color(1f, 0f, 0f, 0.2f)
private val DEBUG_HUN_COLOR = Color(0f, 0f, 1f, 0.2f)
private val DEBUG_BOX_COLOR = Color(0f, 1f, 0f, 0.2f)
private const val HUN_SNOOZE_POSITIONAL_THRESHOLD_FRACTION = 0.25f
private const val HUN_SNOOZE_VELOCITY_THRESHOLD = -70f
+2 −28
Original line number Diff line number Diff line
@@ -17,26 +17,19 @@
package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.IntOffset
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateSceneDpAsState
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.internal.policy.SystemBarUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -72,28 +65,9 @@ constructor(
        )
        animateSceneDpAsState(value = Default, key = MediaLandscapeTopOffset, canOverflow = false)
        Spacer(modifier.fillMaxSize())
        HeadsUpNotificationStack(
        SnoozeableHeadsUpNotificationSpace(
            stackScrollView = notificationStackScrolLView.get(),
            viewModel = notificationsPlaceholderViewModel
        )
    }
}

@Composable
private fun SceneScope.HeadsUpNotificationStack(
    stackScrollView: NotificationScrollView,
    viewModel: NotificationsPlaceholderViewModel,
) {
    val context = LocalContext.current
    val density = LocalDensity.current
    val statusBarHeight = SystemBarUtils.getStatusBarHeight(context)
    val headsUpPadding =
        with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() }

    HeadsUpNotificationSpace(
        stackScrollView = stackScrollView,
        viewModel = viewModel,
        modifier =
            Modifier.absoluteOffset { IntOffset(x = 0, y = statusBarHeight + headsUpPadding) }
    )
}
+3 −0
Original line number Diff line number Diff line
@@ -41,4 +41,7 @@ interface HeadsUpRepository {
    val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>>

    fun setHeadsUpAnimatingAway(animatingAway: Boolean)

    /** Snooze the currently pinned HUN. */
    fun snooze()
}
+7 −0
Original line number Diff line number Diff line
@@ -101,10 +101,17 @@ constructor(

    fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor =
        HeadsUpRowInteractor(key as HeadsUpRowRepository)

    fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey

    fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
        headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
    }

    /** Snooze the currently pinned HUN. */
    fun snooze() {
        headsUpRepository.snooze()
    }
}

class HeadsUpRowInteractor(repository: HeadsUpRowRepository)
+7 −0
Original line number Diff line number Diff line
@@ -3499,6 +3499,13 @@ public class NotificationStackScrollLayout
        setIsBeingDragged(true);
    }

    // Only when scene container is enabled, mark that we are being dragged so that we start
    // dispatching the rest of the gesture to scene container.
    void startDraggingOnHun() {
        SceneContainerFlag.isUnexpectedlyInLegacyMode();
        setIsBeingDragged(true);
    }

    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {
        if (!isScrollingEnabled()
Loading