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

Commit 66915cc6 authored by Gauri Shankar's avatar Gauri Shankar
Browse files

Fixed Dismissal of Dialog when tapped over it during animation || Allowed QS...

Fixed Dismissal of Dialog when tapped over it during animation || Allowed QS Tile tap when dialog is collapsing.

Bug: 358291064
Bug: 385829348
Flag: com.android.systemui.qs_tile_transition_interaction_refinement
Test: DialogTransitionAnimatorTest
Change-Id: Ic0724b30d7be25a14d651e359fcb73fad00287c1
parent d6c0959f
Loading
Loading
Loading
Loading
+51 −1
Original line number Diff line number Diff line
@@ -571,6 +571,7 @@ private class AnimatedDialog(
     * configuration change) to ensure that the dialog stays full width.
     */
    private var decorViewLayoutListener: View.OnLayoutChangeListener? = null
    private var dialogTouchInterceptorView: ViewGroup? = null

    private var hasInstrumentedJank = false

@@ -622,9 +623,13 @@ private class AnimatedDialog(

                viewGroupWithBackground
            } else {
                val (dialogContentWithBackground, decorViewLayoutListener) =
                val (
                    dialogContentWithBackground,
                    dialogTouchInterceptorView,
                    decorViewLayoutListener) =
                    dialog.maybeForceFullscreen()!!
                this.decorViewLayoutListener = decorViewLayoutListener
                this.dialogTouchInterceptorView = dialogTouchInterceptorView
                dialogContentWithBackground
            }

@@ -804,6 +809,9 @@ private class AnimatedDialog(
                if (hasInstrumentedJank) {
                    interactionJankMonitor.end(controller.cuj!!.cujType)
                }
                if (Flags.qsTileTransitionInteractionRefinement()) {
                    dialogTouchInterceptorView?.visibility = View.GONE
                }
            },
        )
    }
@@ -861,6 +869,12 @@ private class AnimatedDialog(
            onLaunchAnimationStart = {
                // Remove the dim background as soon as we start the animation.
                dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)

                if (Flags.qsTileTransitionInteractionRefinement()) {
                    // While collapsing the dialog with animation, allow other quick tiles to be
                    // clickable.
                    dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
                }
            },
            onLaunchAnimationEnd = {
                val dialogContentWithBackground = this.dialogContentWithBackground!!
@@ -969,6 +983,11 @@ private class AnimatedDialog(
                    state.visible = !state.visible
                    endController.onTransitionAnimationProgress(state, progress, linearProgress)

                    if (Flags.qsTileTransitionInteractionRefinement()) {
                        // animate touch Interceptor view
                        updateTouchInterceptorViewConstraints(state)
                    }

                    // If the dialog content is complex, its dimension might change during the
                    // launch animation. The animation end position might also change during the
                    // exit animation, for instance when locking the phone when the dialog is open.
@@ -984,6 +1003,37 @@ private class AnimatedDialog(
        transitionAnimator.startAnimation(controller, endState, originalDialogBackgroundColor)
    }

    private fun updateTouchInterceptorViewConstraints(state: TransitionAnimator.State) {
        dialogTouchInterceptorView?.let { view ->
            val currentWidth = state.right - state.left
            val currentHeight = state.bottom - state.top
            var currentLayoutParams = view.layoutParams

            if (currentLayoutParams == null) {
                // If the view has no LayoutParams (e.g., created programmatically but not yet added
                // to a parent,
                // or added to a parent that didn't assign default params), create new ones.
                // It's crucial to use the correct LayoutParams type for the view's parent.
                currentLayoutParams = ViewGroup.MarginLayoutParams(currentWidth, currentHeight)
            } else {
                // Modify the existing LayoutParams
                currentLayoutParams.width = currentWidth
                currentLayoutParams.height = currentHeight
            }

            if (currentLayoutParams is ViewGroup.MarginLayoutParams) {
                /**
                 * update the left Margin and top Margin of [touchInterceptorView] to match that of
                 * drawable during animation
                 */
                currentLayoutParams.leftMargin = state.left
                currentLayoutParams.topMargin =
                    state.top - ((view.parent as? ViewGroup)?.paddingTop ?: 0)
            }
            view.layoutParams = currentLayoutParams
        }
    }

    private fun shouldAnimateDialogIntoSource(): Boolean {
        // Don't animate if the dialog was previously hidden using hide() or if we disabled the exit
        // animation.
+23 −11
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import android.window.OnBackInvokedDispatcher
import com.android.systemui.Flags
import com.android.systemui.animation.back.BackAnimationSpec
import com.android.systemui.animation.back.BackTransformation
import com.android.systemui.animation.back.applyTo
@@ -39,7 +40,7 @@ fun Dialog.registerAnimationOnBackInvoked(
    targetView: View,
    backAnimationSpec: BackAnimationSpec =
        BackAnimationSpec.floatingSystemSurfacesForSysUi(
            displayMetricsProvider = { targetView.resources.displayMetrics },
            displayMetricsProvider = { targetView.resources.displayMetrics }
        ),
) {
    targetView.registerOnBackInvokedCallbackOnViewAttached(
@@ -58,13 +59,15 @@ fun Dialog.registerAnimationOnBackInvoked(
 * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate
 * outside its bounds. No-op if the dialog is already fullscreen.
 *
 * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view
 * and a layout listener. The new view matches the original dialog DecorView in size, position, and
 * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The
 * layout listener is listening to changes to the modified DecorView. It is the responsibility of
 * the caller to deregister the listener when the dialog is dismissed.
 * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a triple containing a
 * dialogBackgroundView, a touchInterceptorView to stop its dismissal during animation and a layout
 * listener. The new view matches the original dialog DecorView in size, position, and background.
 * This new view will be a child of the modified, transparent, fullscreen DecorView. The layout
 * listener is listening to changes to the modified DecorView. It is the responsibility of the
 * caller to deregister the listener when the dialog is dismissed.
 */
fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? {
fun Dialog.maybeForceFullscreen():
    Triple<LaunchableFrameLayout, LaunchableFrameLayout, View.OnLayoutChangeListener>? {
    // Create the dialog so that its onCreate() method is called, which usually sets the dialog
    // content.
    create()
@@ -94,10 +97,11 @@ fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChan
    decorView.addView(
        fullscreenTransparentBackground,
        0 /* index */,
        FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT),
    )

    val dialogContentWithBackground = LaunchableFrameLayout(context)
    val touchInterceptorView = LaunchableFrameLayout(context)
    dialogContentWithBackground.background = decorView.background

    // Make the window background transparent. Note that setting the window (or DecorView)
@@ -109,19 +113,27 @@ fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChan
    // Close the dialog when clicking outside of it.
    fullscreenTransparentBackground.setOnClickListener { dismiss() }
    dialogContentWithBackground.isClickable = true
    touchInterceptorView.isClickable = true

    // Make sure the transparent and dialog backgrounds are not focusable by accessibility
    // features.
    fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
    dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
    touchInterceptorView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO

    if (Flags.qsTileTransitionInteractionRefinement()) {
        fullscreenTransparentBackground.addView(
            touchInterceptorView,
            ViewGroup.MarginLayoutParams(window.attributes.width, window.attributes.width),
        )
    }
    fullscreenTransparentBackground.addView(
        dialogContentWithBackground,
        FrameLayout.LayoutParams(
            window.attributes.width,
            window.attributes.height,
            window.attributes.gravity
        )
            window.attributes.gravity,
        ),
    )

    // Move all original children of the DecorView to the new View we just added.
@@ -158,5 +170,5 @@ fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChan
        }
    decorView.addOnLayoutChangeListener(decorViewLayoutListener)

    return dialogContentWithBackground to decorViewLayoutListener
    return Triple(dialogContentWithBackground, touchInterceptorView, decorViewLayoutListener)
}
+1 −1
Original line number Diff line number Diff line
@@ -73,7 +73,7 @@ class PrivacyDialogV2(
    private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
    private val dismissed = AtomicBoolean(false)
    // Note: this will call the dialog create method during init
    private val decorViewLayoutListener = maybeForceFullscreen()?.component2()
    private val decorViewLayoutListener = maybeForceFullscreen()?.component3()

    /**
     * Add a listener that will be called when the dialog is dismissed.
+82 −54
Original line number Diff line number Diff line
@@ -5,6 +5,8 @@ import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.View
@@ -17,6 +19,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
import com.android.internal.policy.DecorView
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.testKosmos
@@ -45,8 +48,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
    private val attachedViews = mutableSetOf<View>()
    @get:Rule
    val rule: MockitoRule = MockitoJUnit.rule()
    @get:Rule val rule: MockitoRule = MockitoJUnit.rule()

    @Before
    fun setUp() {
@@ -55,15 +57,12 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {

    @After
    fun tearDown() {
        runOnMainThreadAndWaitForIdleSync {
            attachedViews.forEach {
                ViewUtils.detachView(it)
            }
        }
        runOnMainThreadAndWaitForIdleSync { attachedViews.forEach { ViewUtils.detachView(it) } }
    }

    @EnableFlags(Flags.FLAG_QS_TILE_TRANSITION_INTERACTION_REFINEMENT)
    @Test
    fun testShowDialogFromView() {
    fun testShowDialogFromView_withInterceptorViewFlagEnabled() {
        // Show the dialog. showFromView() must be called on the main thread with a dialog created
        // on the main thread too.
        val dialog = createAndShowDialog()
@@ -78,30 +77,66 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
        assertEquals(MATCH_PARENT, decorView.layoutParams.width)
        assertEquals(MATCH_PARENT, decorView.layoutParams.height)

        // The single DecorView child is a transparent fullscreen view that will dismiss the dialog
        // when clicked.
        assertEquals(1, decorView.childCount)
        // The single transparent background child is a fake window with the same size and
        // background as the dialog initially had and a touchInterceptor view behind background
        // for consuming click to stop its dismissal during animation.\

        val transparentBackground = decorView.getChildAt(0) as ViewGroup
        assertEquals(MATCH_PARENT, transparentBackground.layoutParams.width)
        assertEquals(MATCH_PARENT, transparentBackground.layoutParams.height)
        val dialogContentWithBackground = transparentBackground.getChildAt(1) as ViewGroup
        val touchInterceptorView = transparentBackground.getChildAt(0) as ViewGroup

        assertEquals(2, transparentBackground.childCount)
        touchInterceptorView.apply {
            assertEquals(View.GONE, visibility)
            assertEquals(TestDialog.DIALOG_WIDTH, layoutParams.width)
            assertEquals(TestDialog.DIALOG_HEIGHT, layoutParams.height)
        }

        assertEquals(TestDialog.DIALOG_WIDTH, dialogContentWithBackground.layoutParams.width)
        assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentWithBackground.layoutParams.height)
        assertEquals(dialog.windowBackground, dialogContentWithBackground.background)

        // The dialog content is inside this fake window view.
        assertNotNull(dialogContentWithBackground.findViewByPredicate { it === dialog.contentView })

        // Clicking the transparent background should dismiss the dialog.
        runOnMainThreadAndWaitForIdleSync { transparentBackground.performClick() }
        assertFalse(dialog.isShowing)
    }

    @DisableFlags(Flags.FLAG_QS_TILE_TRANSITION_INTERACTION_REFINEMENT)
    @Test
    fun testShowDialogFromView_withInterceptorViewFlagDisabled() {
        // Show the dialog. showFromView() must be called on the main thread with a dialog created
        // on the main thread too.
        val dialog = createAndShowDialog()

        assertTrue(dialog.isShowing)

        // The dialog is now fullscreen.
        val window = checkNotNull(dialog.window)
        val decorView = window.decorView as DecorView
        assertEquals(MATCH_PARENT, window.attributes.width)
        assertEquals(MATCH_PARENT, window.attributes.height)
        assertEquals(MATCH_PARENT, decorView.layoutParams.width)
        assertEquals(MATCH_PARENT, decorView.layoutParams.height)

        // The single transparent background child is a fake window with the same size and
        // background as the dialog initially had.
        // background as the dialog initially
        val dialogContentWithBackground: ViewGroup
        val transparentBackground = decorView.getChildAt(0) as ViewGroup
        assertEquals(1, transparentBackground.childCount)
        val dialogContentWithBackground = transparentBackground.getChildAt(0) as ViewGroup
        dialogContentWithBackground = transparentBackground.getChildAt(0) as ViewGroup

        assertEquals(TestDialog.DIALOG_WIDTH, dialogContentWithBackground.layoutParams.width)
        assertEquals(TestDialog.DIALOG_HEIGHT, dialogContentWithBackground.layoutParams.height)
        assertEquals(dialog.windowBackground, dialogContentWithBackground.background)

        // The dialog content is inside this fake window view.
        assertNotNull(
                dialogContentWithBackground.findViewByPredicate { it === dialog.contentView }
        )
        assertNotNull(dialogContentWithBackground.findViewByPredicate { it === dialog.contentView })

        // Clicking the transparent background should dismiss the dialog.
        runOnMainThreadAndWaitForIdleSync {
            transparentBackground.performClick()
        }
        runOnMainThreadAndWaitForIdleSync { transparentBackground.performClick() }
        assertFalse(dialog.isShowing)
    }

@@ -112,9 +147,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {

        assertTrue(firstDialog.isShowing)
        assertTrue(secondDialog.isShowing)
        runOnMainThreadAndWaitForIdleSync {
            mDialogTransitionAnimator.dismissStack(secondDialog)
        }
        runOnMainThreadAndWaitForIdleSync { mDialogTransitionAnimator.dismissStack(secondDialog) }

        assertFalse(firstDialog.isShowing)
        assertFalse(secondDialog.isShowing)
@@ -125,8 +158,8 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
        val firstDialog = createAndShowDialog()
        val secondDialog = createDialogAndShowFromDialog(firstDialog)

        val controller = mDialogTransitionAnimator
                .createActivityTransitionController(secondDialog.contentView)!!
        val controller =
            mDialogTransitionAnimator.createActivityTransitionController(secondDialog.contentView)!!

        // The dialog shouldn't be dismissable during the animation.
        runOnMainThreadAndWaitForIdleSync {
@@ -146,9 +179,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
    @Test
    fun testActivityLaunchFromHiddenDialog() {
        val dialog = createAndShowDialog()
        runOnMainThreadAndWaitForIdleSync {
            dialog.hide()
        }
        runOnMainThreadAndWaitForIdleSync { dialog.hide() }
        assertNull(mDialogTransitionAnimator.createActivityTransitionController(dialog.contentView))
    }

@@ -159,18 +190,20 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
                mainExecutor = mContext.mainExecutor,
                isUnlocked = false,
                isShowingAlternateAuthOnUnlock = false,
                        interactionJankMonitor = kosmos.interactionJankMonitor)
                interactionJankMonitor = kosmos.interactionJankMonitor,
            )
        val dialog = createAndShowDialog(dialogTransitionAnimator)
        assertNull(dialogTransitionAnimator.createActivityTransitionController(dialog.contentView))
    }

    @Test
    fun testActivityLaunchWhenLockedWithAlternateAuth() {
        val dialogTransitionAnimator = fakeDialogTransitionAnimator(
        val dialogTransitionAnimator =
            fakeDialogTransitionAnimator(
                mainExecutor = mContext.mainExecutor,
                isUnlocked = false,
                isShowingAlternateAuthOnUnlock = true,
                interactionJankMonitor = kosmos.interactionJankMonitor
                interactionJankMonitor = kosmos.interactionJankMonitor,
            )
        val dialog = createAndShowDialog(dialogTransitionAnimator)
        assertNotNull(
@@ -202,7 +235,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
            mDialogTransitionAnimator.showFromView(
                dialog,
                touchSurface,
                    cuj = DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN)
                cuj = DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN),
            )
        }

@@ -218,7 +251,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
            mDialogTransitionAnimator.showFromDialog(
                dialog,
                firstDialog,
                    cuj = DialogCuj(Cuj.CUJ_USER_DIALOG_OPEN)
                cuj = DialogCuj(Cuj.CUJ_USER_DIALOG_OPEN),
            )
            dialog
        }
@@ -293,7 +326,7 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {
    }

    private fun createAndShowDialog(
            animator: DialogTransitionAnimator = mDialogTransitionAnimator,
        animator: DialogTransitionAnimator = mDialogTransitionAnimator
    ): TestDialog {
        val touchSurface = createTouchSurface()
        return showDialogFromView(touchSurface, animator)
@@ -335,19 +368,14 @@ class DialogTransitionAnimatorTest : SysuiTestCase() {

    private fun <T : Any> runOnMainThreadAndWaitForIdleSync(f: () -> T): T {
        lateinit var result: T
        context.mainExecutor.execute {
            result = f()
        }
        context.mainExecutor.execute { result = f() }
        waitForIdleSync()
        return result
    }

    private class TouchSurfaceView(context: Context) : FrameLayout(context), LaunchableView {
        private val delegate =
                LaunchableViewDelegate(
                        this,
                        superSetVisibility = { super.setVisibility(it) },
                )
            LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })

        override fun setShouldBlockVisibilityChanges(block: Boolean) {
            delegate.setShouldBlockVisibilityChanges(block)