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

Commit 8e03e1f7 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes I52fc320f,Ib5c80dd1,I846a4357 into main

* changes:
  [flexiglass] fix notif scroll not working after viewing QS
  [flexiglass] SessionDisposableEffect + sessionCoroutineScope
  [Flexiglass] Fix NotificationScrimNestedScrollConnection rememeberSession keys
parents 8718341a 5af076d5
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.notifications.ui.composable

import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.animateDecay
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.ui.MotionDurationScale
import com.android.systemui.scene.session.ui.composable.rememberSession
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext
import kotlin.math.abs

/**
 * Fork of [androidx.compose.foundation.gestures.DefaultFlingBehavior] to allow us to use it with
 * [rememberSession].
 */
internal class NotificationScrimFlingBehavior(
    private var flingDecay: DecayAnimationSpec<Float>,
    private val motionDurationScale: MotionDurationScale = NotificationScrimMotionDurationScale
) : FlingBehavior {
    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
        // come up with the better threshold, but we need it since spline curve gives us NaNs
        return withContext(motionDurationScale) {
            if (abs(initialVelocity) > 1f) {
                var velocityLeft = initialVelocity
                var lastValue = 0f
                val animationState =
                    AnimationState(
                        initialValue = 0f,
                        initialVelocity = initialVelocity,
                    )
                try {
                    animationState.animateDecay(flingDecay) {
                        val delta = value - lastValue
                        val consumed = scrollBy(delta)
                        lastValue = value
                        velocityLeft = this.velocity
                        // avoid rounding errors and stop if anything is unconsumed
                        if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
                    }
                } catch (exception: CancellationException) {
                    velocityLeft = animationState.velocity
                }
                velocityLeft
            } else {
                initialVelocity
            }
        }
    }
}

internal val NotificationScrimMotionDurationScale =
    object : MotionDurationScale {
        override val scaleFactor: Float
            get() = 1f
    }
 No newline at end of file
+9 −10
Original line number Diff line number Diff line
@@ -20,12 +20,13 @@ package com.android.systemui.notifications.ui.composable
import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.splineBasedDecay
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.ScrollableDefaults
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollBy
@@ -59,7 +60,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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
@@ -97,6 +97,7 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi
import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.session.ui.composable.rememberSession
import com.android.systemui.scene.session.ui.composable.sessionCoroutineScope
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
@@ -298,7 +299,7 @@ fun ContentScope.NotificationScrollingStack(
    onEmptySpaceClick: (() -> Unit)? = null,
    modifier: Modifier = Modifier,
) {
    val coroutineScope = rememberCoroutineScope()
    val coroutineScope = shadeSession.sessionCoroutineScope()
    val density = LocalDensity.current
    val screenCornerRadius = LocalScreenCornerRadius.current
    val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius)
@@ -308,8 +309,6 @@ fun ContentScope.NotificationScrollingStack(
            ScrollState(initial = 0)
        }
    val syntheticScroll = viewModel.syntheticScroll.collectAsStateWithLifecycle(0f)
    val isCurrentGestureOverscroll =
        viewModel.isCurrentGestureOverscroll.collectAsStateWithLifecycle(false)
    val expansionFraction by viewModel.expandFraction.collectAsStateWithLifecycle(0f)
    val shadeToQsFraction by viewModel.shadeToQsFraction.collectAsStateWithLifecycle(0f)

@@ -454,15 +453,15 @@ fun ContentScope.NotificationScrollingStack(
        }
    }

    val flingBehavior = ScrollableDefaults.flingBehavior()
    val scrimNestedScrollConnection =
        shadeSession.rememberSession(
            scrimOffset,
            maxScrimTop,
            minScrimTop,
            isCurrentGestureOverscroll,
            flingBehavior,
            viewModel.isCurrentGestureOverscroll,
            density,
        ) {
            val flingSpec: DecayAnimationSpec<Float> = splineBasedDecay(density)
            val flingBehavior = NotificationScrimFlingBehavior(flingSpec)
            NotificationScrimNestedScrollConnection(
                scrimOffset = { scrimOffset.value },
                snapScrimOffset = { value -> coroutineScope.launch { scrimOffset.snapTo(value) } },
@@ -473,7 +472,7 @@ fun ContentScope.NotificationScrollingStack(
                maxScrimOffset = 0f,
                contentHeight = { stackHeight.intValue.toFloat() },
                minVisibleScrimHeight = minVisibleScrimHeight,
                isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value },
                isCurrentGestureOverscroll = { viewModel.isCurrentGestureOverscroll },
                flingBehavior = flingBehavior,
            )
        }
+80 −12
Original line number Diff line number Diff line
@@ -17,10 +17,14 @@
package com.android.systemui.scene.session.ui.composable

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.mapSaver
@@ -28,6 +32,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.android.systemui.scene.session.shared.SessionStorage
import com.android.systemui.util.kotlin.mapValuesNotNullTo
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job

/**
 * An explicit storage for remembering composable state outside of the lifetime of a composition.
@@ -88,6 +96,55 @@ fun Session(storage: SessionStorage = SessionStorage()): Session = SessionImpl(s
fun <T> Session.rememberSession(vararg inputs: Any?, key: String? = null, init: () -> T): T =
    rememberSession(key, *inputs, init = init)

/**
 * A side effect of composition that must be reversed or cleaned up if the [Session] ends.
 *
 * @see androidx.compose.runtime.DisposableEffect
 */
@Composable
fun Session.SessionDisposableEffect(
    vararg inputs: Any?,
    key: String? = null,
    effect: DisposableEffectScope.() -> DisposableEffectResult,
) {
    rememberSession(inputs, key) {
        object : RememberObserver {

            var onDispose: DisposableEffectResult? = null

            override fun onAbandoned() {
                // no-op
            }

            override fun onForgotten() {
                onDispose?.dispose()
                onDispose = null
            }

            override fun onRemembered() {
                onDispose = DisposableEffectScope().effect()
            }
        }
    }
}

/**
 * Return a [CoroutineScope] bound to this [Session] using the optional [CoroutineContext] provided
 * by [getContext]. [getContext] will only be called once and the same [CoroutineScope] instance
 * will be returned for the duration of the [Session].
 *
 * @see androidx.compose.runtime.rememberCoroutineScope
 */
@Composable
fun Session.sessionCoroutineScope(
    getContext: () -> CoroutineContext = { EmptyCoroutineContext }
): CoroutineScope {
    val effectContext: CoroutineContext = rememberCompositionContext().effectCoroutineContext
    val job = rememberSession { Job() }
    SessionDisposableEffect { onDispose { job.cancel() } }
    return rememberSession { CoroutineScope(effectContext + job + getContext()) }
}

/**
 * An explicit storage for remembering composable state outside of the lifetime of a composition.
 *
@@ -147,15 +204,10 @@ interface SaveableSession : Session {
 *   location in the composition tree.
 */
@Composable
fun rememberSaveableSession(
    vararg inputs: Any?,
    key: String? = null,
): SaveableSession =
fun rememberSaveableSession(vararg inputs: Any?, key: String? = null): SaveableSession =
    rememberSaveable(*inputs, SaveableSessionImpl.SessionSaver, key) { SaveableSessionImpl() }

private class SessionImpl(
    private val storage: SessionStorage = SessionStorage(),
) : Session {
private class SessionImpl(private val storage: SessionStorage = SessionStorage()) : Session {
    @Composable
    override fun <T> rememberSession(key: String?, vararg inputs: Any?, init: () -> T): T {
        val storage = storage.storage
@@ -169,16 +221,31 @@ private class SessionImpl(
            }
        if (finalKey !in storage) {
            val value = init()
            SideEffect { storage[finalKey] = SessionStorage.StorageEntry(inputs, value) }
            SideEffect {
                storage[finalKey] = SessionStorage.StorageEntry(inputs, value)
                if (value is RememberObserver) {
                    value.onRemembered()
                }
            }
            return value
        }
        val entry = storage[finalKey]!!
        if (!inputs.contentEquals(entry.keys)) {
            val value = init()
            SideEffect { entry.stored = value }
            SideEffect {
                val oldValue = entry.stored
                if (oldValue is RememberObserver) {
                    oldValue.onForgotten()
                }
                entry.stored = value
                if (value is RememberObserver) {
                    value.onRemembered()
                }
            }
            return value
        }
        @Suppress("UNCHECKED_CAST") return entry.stored as T
        @Suppress("UNCHECKED_CAST")
        return entry.stored as T
    }
}

@@ -228,7 +295,8 @@ private class SaveableSessionImpl(
                    }
                    return value
                }
                @Suppress("UNCHECKED_CAST") return entry.stored as T
                @Suppress("UNCHECKED_CAST")
                return entry.stored as T
            }
        }
    }
@@ -263,7 +331,7 @@ private class SaveableSessionImpl(
                            v?.let { StorageEntry.Unrestored(v) }
                        }
                )
            }
            },
        )
}

+11 −7
Original line number Diff line number Diff line
@@ -89,6 +89,17 @@ constructor(
            source = shadeModeInteractor.shadeMode.map { getQuickSettingsShadeContentKey(it) },
        )

    /**
     * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
     * consumed part of the gesture.
     */
    val isCurrentGestureOverscroll: Boolean by
        hydrator.hydratedStateOf(
            traceName = "isCurrentGestureOverscroll",
            initialValue = false,
            source = interactor.isCurrentGestureOverscroll
        )

    /** DEBUG: whether the placeholder should be made slightly visible for positional debugging. */
    val isVisualDebuggingEnabled: Boolean = featureFlags.isEnabled(Flags.NSSL_DEBUG_LINES)

@@ -157,13 +168,6 @@ constructor(
    val syntheticScroll: Flow<Float> =
        interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll")

    /**
     * Whether the current touch gesture is overscroll. If true, it means the NSSL has already
     * consumed part of the gesture.
     */
    val isCurrentGestureOverscroll: Flow<Boolean> =
        interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll")

    /** Whether remote input is currently active for any notification. */
    val isRemoteInputActive = remoteInputInteractor.isRemoteInputActive