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

Commit 9d7bbd00 authored by Shawn Lee's avatar Shawn Lee Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Add Snoozeable HUN Placeholder and HUN touch handling" into main

parents d9a94996 ae04a4f4
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