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

Commit 24024c72 authored by András Kurucz's avatar András Kurucz Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] Scroll the shade with TalkBack" into main

parents 1d0fc968 52ce865f
Loading
Loading
Loading
Loading
+59 −4
Original line number Diff line number Diff line
@@ -50,9 +50,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
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
@@ -60,6 +62,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
@@ -95,13 +98,17 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.ui.composable.ShadeHeader
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.max
import kotlin.math.roundToInt
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

@@ -315,6 +322,12 @@ fun SceneScope.NotificationScrollingStack(
     */
    val stackHeight = remember { mutableIntStateOf(0) }

    /**
     * Space available for the notification stack on the screen. These bounds don't scroll off the
     * screen, and respect the scrim paddings, scrim clipping.
     */
    val stackBoundsOnScreen = remember { mutableStateOf(Rect.Zero) }

    val scrimRounding =
        viewModel.shadeScrimRounding.collectAsStateWithLifecycle(ShadeScrimRounding())

@@ -348,12 +361,19 @@ fun SceneScope.NotificationScrollingStack(
    // The top y bound of the IME.
    val imeTop = remember { mutableFloatStateOf(0f) }

    val shadeScrollState by remember {
        derivedStateOf {
            ShadeScrollState(
                // we are not scrolled to the top unless the scrim is at its maximum offset.
    LaunchedEffect(viewModel, scrimOffset) {
        snapshotFlow { scrimOffset.value >= 0f }
            .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) }
                isScrolledToTop = scrimOffset.value >= 0f,
                scrollPosition = scrollState.value,
                maxScrollPosition = scrollState.maxValue,
            )
        }
    }

    LaunchedEffect(shadeScrollState) { viewModel.setScrollState(shadeScrollState) }

    // if contentHeight drops below minimum visible scrim height while scrim is
    // expanded, reset scrim offset.
    LaunchedEffect(stackHeight, scrimOffset) {
@@ -395,6 +415,38 @@ fun SceneScope.NotificationScrollingStack(
            }
    }

    // TalkBack sends a scroll event, when it wants to navigate to an item that is not displayed in
    // the current viewport.
    LaunchedEffect(viewModel) {
        viewModel.setAccessibilityScrollEventConsumer { event ->
            // scroll up, or down by the height of the visible portion of the notification stack
            val direction =
                when (event) {
                    AccessibilityScrollEvent.SCROLL_UP -> -1
                    AccessibilityScrollEvent.SCROLL_DOWN -> 1
                }
            val viewPortHeight = stackBoundsOnScreen.value.height
            val scrollStep = max(0f, viewPortHeight - stackScrollView.stackBottomInset)
            val scrollPosition = scrollState.value.toFloat()
            val scrollRange = scrollState.maxValue.toFloat()
            val targetScroll = (scrollPosition + direction * scrollStep).coerceIn(0f, scrollRange)
            coroutineScope.launch {
                scrollNotificationStack(
                    delta = targetScroll - scrollPosition,
                    animate = false,
                    scrimOffset = scrimOffset,
                    minScrimOffset = minScrimOffset,
                    scrollState = scrollState,
                )
            }
        }
        try {
            awaitCancellation()
        } finally {
            viewModel.setAccessibilityScrollEventConsumer(null)
        }
    }

    val scrimNestedScrollConnection =
        shadeSession.rememberSession(
            scrimOffset,
@@ -520,6 +572,9 @@ fun SceneScope.NotificationScrollingStack(
                        .verticalScroll(scrollState)
                        .padding(top = topPadding)
                        .fillMaxWidth()
                        .onGloballyPositioned { coordinates ->
                            stackBoundsOnScreen.value = coordinates.boundsInWindow()
                        }
            ) {
                NotificationPlaceholder(
                    stackScrollView = stackScrollView,
+58 −21
Original line number Diff line number Diff line
@@ -25,6 +25,8 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_A
import static com.android.systemui.Flags.notificationOverExpansionClippingFix;
import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN;
import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_UP;
import static com.android.systemui.util.DumpUtilsKt.println;
import static com.android.systemui.util.DumpUtilsKt.visibilityString;

@@ -118,8 +120,10 @@ import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCyc
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation;
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent;
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds;
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape;
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState;
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -609,7 +613,7 @@ public class NotificationStackScrollLayout
        @Override
        public boolean isScrolledToTop() {
            if (SceneContainerFlag.isEnabled()) {
                return mScrollViewFields.isScrolledToTop();
                return mScrollViewFields.getScrollState().isScrolledToTop();
            } else {
                return mOwnScrollY == 0;
            }
@@ -1247,9 +1251,25 @@ public class NotificationStackScrollLayout
    }

    @Override
    public void setScrolledToTop(boolean scrolledToTop) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
        mScrollViewFields.setScrolledToTop(scrolledToTop);
    public void setScrollState(@NonNull ShadeScrollState scrollState) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
            return;
        }

        boolean forwardScrollable =
                scrollState.getScrollPosition() < scrollState.getMaxScrollPosition();
        boolean backwardScrollable = scrollState.getScrollPosition() > 0;
        mScrollable = forwardScrollable || backwardScrollable;
        mForwardScrollable = forwardScrollable;
        mBackwardScrollable = backwardScrollable;

        boolean scrollPositionChanged = mScrollViewFields.getScrollState().getScrollPosition()
                != scrollState.getScrollPosition();
        mScrollViewFields.setScrollState(scrollState);

        if (scrollPositionChanged) {
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
        }
    }

    @Override
@@ -1294,6 +1314,12 @@ public class NotificationStackScrollLayout
        mScrollViewFields.setSyntheticScrollConsumer(consumer);
    }

    @Override
    public void setAccessibilityScrollEventConsumer(
            @Nullable Consumer<AccessibilityScrollEvent> consumer) {
        mScrollViewFields.setAccessibilityScrollEventConsumer(consumer);
    }

    @Override
    public void setCurrentGestureOverscrollConsumer(@Nullable Consumer<Boolean> consumer) {
        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
@@ -2645,6 +2671,11 @@ public class NotificationStackScrollLayout
        return mHeadsUpInset;
    }

    @Override
    public int getStackBottomInset() {
        return mPaddingBetweenElements + mShelf.getIntrinsicHeight();
    }

    /**
     * Calculate the gap height between two different views
     *
@@ -4243,17 +4274,27 @@ public class NotificationStackScrollLayout
     */
    @Override
    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
        // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled.
        if (SceneContainerFlag.isEnabled()) {
            return super.performAccessibilityActionInternal(action, arguments);
        }

        if (super.performAccessibilityActionInternal(action, arguments)) {
            return true;
        }
        if (!isEnabled()) {
            return false;
        }

        if (SceneContainerFlag.isEnabled()) {
            switch (action) {
                case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
                case android.R.id.accessibilityActionScrollDown:
                    mScrollViewFields.sendAccessibilityScrollEvent(SCROLL_DOWN);
                    return true;
                case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
                case android.R.id.accessibilityActionScrollUp:
                    mScrollViewFields.sendAccessibilityScrollEvent(SCROLL_UP);
                    return true;
            }
            return false;
        }

        int direction = -1;
        switch (action) {
            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
@@ -5029,25 +5070,21 @@ public class NotificationStackScrollLayout
    @Override
    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
        super.onInitializeAccessibilityEventInternal(event);
        // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled.
        if (SceneContainerFlag.isEnabled()) {
            return;
        }

        event.setScrollable(mScrollable);
        event.setMaxScrollX(mScrollX);

        if (SceneContainerFlag.isEnabled()) {
            event.setScrollY(mScrollViewFields.getScrollState().getScrollPosition());
            event.setMaxScrollY(mScrollViewFields.getScrollState().getMaxScrollPosition());
        } else {
            event.setScrollY(mOwnScrollY);
            event.setMaxScrollY(getScrollRange());
        }
    }

    @Override
    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfoInternal(info);
        // Don't handle scroll accessibility events from the NSSL, when SceneContainer enabled.
        if (SceneContainerFlag.isEnabled()) {
            return;
        }

        if (mScrollable) {
            info.setScrollable(true);
            if (mBackwardScrollable) {
+18 −3
Original line number Diff line number Diff line
@@ -17,7 +17,9 @@
package com.android.systemui.statusbar.notification.stack

import android.util.IndentingPrintWriter
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
import com.android.systemui.util.printSection
import com.android.systemui.util.println
import java.util.function.Consumer
@@ -32,8 +34,9 @@ import java.util.function.Consumer
class ScrollViewFields {
    /** Used to produce the clipping path */
    var scrimClippingShape: ShadeScrimShape? = null
    /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */
    var isScrolledToTop: Boolean = true

    /** Scroll state of the notification shade. */
    var scrollState: ShadeScrollState = ShadeScrollState()

    /**
     * Height in view pixels at which the Notification Stack would like to be laid out, including
@@ -47,6 +50,13 @@ class ScrollViewFields {
     * placeholder
     */
    var syntheticScrollConsumer: Consumer<Float>? = null

    /**
     * When the NSSL navigates through the notifications with TalkBack, it can send scroll events
     * here, to be able to browse through the whole list of notifications in the shade.
     */
    var accessibilityScrollEventConsumer: Consumer<AccessibilityScrollEvent>? = null

    /**
     * When a gesture is consumed internally by NSSL but needs to be handled by other elements (such
     * as the notif scrim) as overscroll, we can notify the placeholder through here.
@@ -86,10 +96,15 @@ class ScrollViewFields {
    fun sendRemoteInputRowBottomBound(bottomY: Float?) =
        remoteInputRowBottomBoundConsumer?.accept(bottomY)

    /** send an [AccessibilityScrollEvent] to the [accessibilityScrollEventConsumer] if present */
    fun sendAccessibilityScrollEvent(event: AccessibilityScrollEvent) {
        accessibilityScrollEventConsumer?.accept(event)
    }

    fun dump(pw: IndentingPrintWriter) {
        pw.printSection("StackViewStates") {
            pw.println("scrimClippingShape", scrimClippingShape)
            pw.println("isScrolledToTop", isScrolledToTop)
            pw.println("scrollState", scrollState)
        }
    }
}
+8 −5
Original line number Diff line number Diff line
@@ -17,7 +17,10 @@
package com.android.systemui.statusbar.notification.stack.data.repository

import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow

@@ -44,9 +47,9 @@ class NotificationPlaceholderRepository @Inject constructor() {
    /** height made available to the notifications in the size-constrained mode of lock screen. */
    val constrainedAvailableSpace = MutableStateFlow(0)

    /**
     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
     * further.
     */
    val scrolledToTop = MutableStateFlow(true)
    /** Scroll state of the notification shade. */
    val shadeScrollState = MutableStateFlow(ShadeScrollState())

    /** A consumer of [AccessibilityScrollEvent]s. */
    var accessibilityScrollEventConsumer: Consumer<AccessibilityScrollEvent>? = null
}
+19 −8
Original line number Diff line number Diff line
@@ -23,8 +23,11 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationPlaceholderRepository
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationViewHeightRepository
import com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimRounding
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrollState
import java.util.function.Consumer
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -78,11 +81,9 @@ constructor(
    val constrainedAvailableSpace: StateFlow<Int> =
        placeholderRepository.constrainedAvailableSpace.asStateFlow()

    /**
     * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any
     * further.
     */
    val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow()
    /** Scroll state of the notification shade. */
    val shadeScrollState: StateFlow<ShadeScrollState> =
        placeholderRepository.shadeScrollState.asStateFlow()

    /**
     * The amount in px that the notification stack should scroll due to internal expansion. This
@@ -123,9 +124,9 @@ constructor(
        placeholderRepository.shadeScrimBounds.value = bounds
    }

    /** Sets whether the notification stack is scrolled to the top. */
    fun setScrolledToTop(scrolledToTop: Boolean) {
        placeholderRepository.scrolledToTop.value = scrolledToTop
    /** Updates the current scroll state of the notification shade. */
    fun setScrollState(shadeScrollState: ShadeScrollState) {
        placeholderRepository.shadeScrollState.value = shadeScrollState
    }

    /** Sets the amount (px) that the notification stack should scroll due to internal expansion. */
@@ -133,6 +134,16 @@ constructor(
        viewHeightRepository.syntheticScroll.value = delta
    }

    /** Sends an [AccessibilityScrollEvent] to scroll the stack up or down. */
    fun sendAccessibilityScrollEvent(accessibilityScrollEvent: AccessibilityScrollEvent) {
        placeholderRepository.accessibilityScrollEventConsumer?.accept(accessibilityScrollEvent)
    }

    /** Set a consumer for the [AccessibilityScrollEvent]s to be handled by the placeholder. */
    fun setAccessibilityScrollEventConsumer(consumer: Consumer<AccessibilityScrollEvent>?) {
        placeholderRepository.accessibilityScrollEventConsumer = consumer
    }

    /** Sets whether the current touch gesture is overscroll. */
    fun setCurrentGestureOverscroll(isOverscroll: Boolean) {
        viewHeightRepository.isCurrentGestureOverscroll.value = isOverscroll
Loading