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

Commit 8bf1f140 authored by Luca Zuccarini's avatar Luca Zuccarini
Browse files

Intercept Shade accessibility focus events during launch animations.

This ensures that we don't have stray TalkBack events when a launching
view loses focus after the launch transition is triggered.

Any solution to this problem requires us to rely on the knowledge of
whether or not the launch animation is running. This seems like the
most comprehensive approach for the whole Shade, while being as surgical
as possible and removing the performance implications of marking the
subtree unimportant for accessibility altogether.

Bug: 383688212
Bug: 379222226
Flag: com.android.systemui.shade_launch_accessibility
Test: atest NotificationShadeViewControllerTest NotificationShadeViewTest
Change-Id: I5a65e0107c245e7e8830769f2c515e41578c70bf
parent 10b78ff1
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -1957,3 +1957,13 @@ flag {
      purpose: PURPOSE_BUGFIX
    }
}

flag {
   name: "shade_launch_accessibility"
   namespace: "systemui"
   description: "Intercept accessibility focus events for the Shade during launch animations to avoid stray TalkBack events."
   bug: "379222226"
   metadata {
        purpose: PURPOSE_BUGFIX
   }
}
+22 −1
Original line number Diff line number Diff line
@@ -20,7 +20,7 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper.RunWithLooper
import android.view.Choreographer
import android.view.MotionEvent
import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -45,7 +45,10 @@ import com.android.systemui.res.R
import com.android.systemui.settings.brightness.data.repository.BrightnessMirrorShowingRepository
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.shade.data.repository.ShadeAnimationRepository
import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorLegacyImpl
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.DragDownHelper
import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -185,6 +188,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
                notificationShadeDepthController,
                underTest,
                shadeViewController,
                ShadeAnimationInteractorLegacyImpl(
                    ShadeAnimationRepository(),
                    ShadeRepositoryImpl(testScope),
                ),
                panelExpansionInteractor,
                ShadeExpansionStateManager(),
                notificationStackScrollLayoutController,
@@ -259,6 +266,20 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
        verify(configurationForwarder).dispatchOnMovedToDisplay(eq(1), eq(config))
    }

    @Test
    @EnableFlags(AConfigFlags.FLAG_SHADE_LAUNCH_ACCESSIBILITY)
    fun requestSendAccessibilityEvent_duringLaunchAnimation_blocksFocusEvent() {
        underTest.setAnimatingContentLaunch(true)

        assertThat(
                underTest.requestSendAccessibilityEvent(
                    underTest.getChildAt(0),
                    AccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED),
                )
            )
            .isFalse()
    }

    private fun captureInteractionEventHandler() {
        verify(underTest).setInteractionEventHandler(interactionEventHandlerCaptor.capture())
        interactionEventHandler = interactionEventHandlerCaptor.value
+21 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.shade;

import static android.os.Trace.TRACE_TAG_APP;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;

import static com.android.systemui.Flags.enableViewCaptureTracing;
import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG;
@@ -49,10 +50,12 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.accessibility.AccessibilityEvent;

import com.android.app.viewcapture.ViewCaptureFactory;
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.Flags;
import com.android.systemui.scene.ui.view.WindowRootView;
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.statusbar.phone.ConfigurationForwarder;
@@ -77,6 +80,8 @@ public class NotificationShadeWindowView extends WindowRootView {

    private SafeCloseable mViewCaptureCloseable;

    private boolean mAnimatingContentLaunch = false;

    public NotificationShadeWindowView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setMotionEventSplittingEnabled(false);
@@ -188,6 +193,22 @@ public class NotificationShadeWindowView extends WindowRootView {
        }
    }

    @Override
    public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
        if (Flags.shadeLaunchAccessibility() && mAnimatingContentLaunch
                && event.getEventType() == TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
            // Block accessibility focus events during launch animations to avoid stray TalkBack
            // announcements.
            return false;
        }

        return super.requestSendAccessibilityEvent(child, event);
    }

    public void setAnimatingContentLaunch(boolean animating) {
        mAnimatingContentLaunch = animating;
    }

    public void setConfigurationForwarder(ConfigurationForwarder configurationForwarder) {
        ShadeWindowGoesAround.isUnexpectedlyInLegacyMode();
        mConfigurationForwarder = configurationForwarder;
+22 −1
Original line number Diff line number Diff line
@@ -16,10 +16,12 @@

package com.android.systemui.shade;

import static com.android.systemui.Flags.shadeLaunchAccessibility;
import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING;
import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;

import android.app.StatusBarManager;
import android.util.Log;
@@ -59,6 +61,7 @@ import com.android.systemui.res.R;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor;
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor;
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.BlurUtils;
@@ -85,6 +88,7 @@ import com.android.systemui.window.ui.WindowRootViewBinder;
import com.android.systemui.window.ui.viewmodel.WindowRootViewModel;

import kotlinx.coroutines.ExperimentalCoroutinesApi;
import kotlinx.coroutines.flow.Flow;

import java.io.PrintWriter;
import java.util.Optional;
@@ -174,6 +178,7 @@ public class NotificationShadeWindowViewController implements Dumpable {
            NotificationShadeDepthController depthController,
            NotificationShadeWindowView notificationShadeWindowView,
            ShadeViewController shadeViewController,
            ShadeAnimationInteractor shadeAnimationInteractor,
            PanelExpansionInteractor panelExpansionInteractor,
            ShadeExpansionStateManager shadeExpansionStateManager,
            NotificationStackScrollLayoutController notificationStackScrollLayoutController,
@@ -238,9 +243,17 @@ public class NotificationShadeWindowViewController implements Dumpable {
        collectFlow(mView, keyguardTransitionInteractor.transition(
                Edge.create(LOCKSCREEN, DREAMING)),
                mLockscreenToDreamingTransition);
        Flow<Boolean> isLaunchAnimationRunning =
                shadeLaunchAccessibility()
                        ? combineFlows(
                                notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
                                shadeAnimationInteractor.isLaunchingActivity(),
                                (notificationLaunching, shadeLaunching) ->
                                        notificationLaunching || shadeLaunching)
                        : notificationLaunchAnimationInteractor.isLaunchAnimationRunning();
        collectFlow(
                mView,
                notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
                isLaunchAnimationRunning,
                this::setExpandAnimationRunning);
        if (QSComposeFragment.isEnabled()) {
            collectFlow(mView,
@@ -726,9 +739,17 @@ public class NotificationShadeWindowViewController implements Dumpable {
            if (ActivityTransitionAnimator.DEBUG_TRANSITION_ANIMATION) {
                Log.d(TAG, "Setting mExpandAnimationRunning=" + running);
            }

            if (running) {
                mLaunchAnimationTimeout = mClock.uptimeMillis() + 5000;
            }

            if (shadeLaunchAccessibility()) {
                // The view needs to know when an animation is ongoing so it can intercept
                // unnecessary accessibility events.
                mView.setAnimatingContentLaunch(running);
            }

            mExpandAnimationRunning = running;
            mNotificationShadeWindowController.setLaunchingActivity(mExpandAnimationRunning);
        }
+14 −5
Original line number Diff line number Diff line
package com.android.systemui.statusbar.phone

import android.view.View
import com.android.systemui.Flags
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.TransitionAnimator
import com.android.systemui.animation.TransitionAnimator.Companion.getProgress
@@ -22,7 +23,7 @@ class StatusBarTransitionAnimatorController(
    private val notificationShadeWindowController: NotificationShadeWindowController,
    private val commandQueue: CommandQueue,
    @DisplayId private val displayId: Int,
    private val isLaunchForActivity: Boolean = true
    private val isLaunchForActivity: Boolean = true,
) : ActivityTransitionAnimator.Controller by delegate {
    private var hideIconsDuringLaunchAnimation: Boolean = true

@@ -41,8 +42,16 @@ class StatusBarTransitionAnimatorController(
    }

    override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
        if (Flags.shadeLaunchAccessibility()) {
            // We set this before calling the delegate to make sure that accessibility is disabled
            // for the whole duration of the transition, so that we don't have stray TalkBack events
            // once the animating view becomes invisible.
            shadeAnimationInteractor.setIsLaunchingActivity(true)
            delegate.onTransitionAnimationStart(isExpandingFullyAbove)
        } else {
            delegate.onTransitionAnimationStart(isExpandingFullyAbove)
            shadeAnimationInteractor.setIsLaunchingActivity(true)
        }
        if (!isExpandingFullyAbove) {
            shadeController.collapseWithDuration(
                ActivityTransitionAnimator.TIMINGS.totalDuration.toInt()
@@ -59,7 +68,7 @@ class StatusBarTransitionAnimatorController(
    override fun onTransitionAnimationProgress(
        state: TransitionAnimator.State,
        progress: Float,
        linearProgress: Float
        linearProgress: Float,
    ) {
        delegate.onTransitionAnimationProgress(state, progress, linearProgress)
        val hideIcons =
@@ -67,7 +76,7 @@ class StatusBarTransitionAnimatorController(
                ActivityTransitionAnimator.TIMINGS,
                linearProgress,
                ANIMATION_DELAY_ICON_FADE_IN,
                100
                100,
            ) == 0.0f
        if (hideIcons != hideIconsDuringLaunchAnimation) {
            hideIconsDuringLaunchAnimation = hideIcons
Loading