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

Commit dcbfe903 authored by Gauri Shankar's avatar Gauri Shankar Committed by Android (Google) Code Review
Browse files

Merge "Fixed Dismissal of Dialog when tapped over it during animation ||...

Merge "Fixed Dismissal of Dialog when tapped over it during animation || Allowed QS Tile tap when dialog is collapsing." into main
parents 45e91524 66915cc6
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)