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

Commit 099f148e authored by Liran Binyamin's avatar Liran Binyamin
Browse files

Fix flickers when switching bubbles with IME open

When switching bubbles while the IME is open, we now perform the
switch after the IME is fully hidden.

Flag: EXEMPT bug fix
Fixes: 384838831
Test: atest BubbleStackViewTest
Test: manual
       - have 2 floating bubbles
       - expand one and request IME
       - select the second bubble
       - observe switching does not flicker
       - repeat switching
Change-Id: If269ccf55ace7acec4b8b10b723c257315aec120
parent 9fa5cd45
Loading
Loading
Loading
Loading
+161 −1
Original line number Original line Diff line number Diff line
@@ -36,6 +36,8 @@ import com.android.internal.protolog.ProtoLog
import com.android.launcher3.icons.BubbleIconFactory
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.Flags
import com.android.wm.shell.Flags
import com.android.wm.shell.R
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubbleStackView.SurfaceSynchronizer
import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.FloatingContentCoordinator
@@ -75,6 +77,7 @@ class BubbleStackViewTest {
    private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
    private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
    private lateinit var bubbleData: BubbleData
    private lateinit var bubbleData: BubbleData
    private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
    private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
    private lateinit var surfaceSynchronizer: FakeSurfaceSynchronizer
    private var sysuiProxy = mock<SysuiProxy>()
    private var sysuiProxy = mock<SysuiProxy>()


    @Before
    @Before
@@ -108,13 +111,14 @@ class BubbleStackViewTest {
        bubbleStackViewManager = FakeBubbleStackViewManager()
        bubbleStackViewManager = FakeBubbleStackViewManager()
        expandedViewManager = FakeBubbleExpandedViewManager()
        expandedViewManager = FakeBubbleExpandedViewManager()
        bubbleTaskViewFactory = FakeBubbleTaskViewFactory(context, shellExecutor)
        bubbleTaskViewFactory = FakeBubbleTaskViewFactory(context, shellExecutor)
        surfaceSynchronizer = FakeSurfaceSynchronizer()
        bubbleStackView =
        bubbleStackView =
            BubbleStackView(
            BubbleStackView(
                context,
                context,
                bubbleStackViewManager,
                bubbleStackViewManager,
                positioner,
                positioner,
                bubbleData,
                bubbleData,
                null,
                surfaceSynchronizer,
                FloatingContentCoordinator(),
                FloatingContentCoordinator(),
                { sysuiProxy },
                { sysuiProxy },
                shellExecutor
                shellExecutor
@@ -309,6 +313,7 @@ class BubbleStackViewTest {


    @Test
    @Test
    fun tapDifferentBubble_shouldReorder() {
    fun tapDifferentBubble_shouldReorder() {
        surfaceSynchronizer.isActive = false
        val bubble1 = createAndInflateChatBubble(key = "bubble1")
        val bubble1 = createAndInflateChatBubble(key = "bubble1")
        val bubble2 = createAndInflateChatBubble(key = "bubble2")
        val bubble2 = createAndInflateChatBubble(key = "bubble2")
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -378,6 +383,147 @@ class BubbleStackViewTest {
            .inOrder()
            .inOrder()
    }
    }


    @Test
    fun tapDifferentBubble_imeVisible_shouldWaitForIme() {
        val bubble1 = createAndInflateChatBubble(key = "bubble1")
        val bubble2 = createAndInflateChatBubble(key = "bubble2")
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            bubbleStackView.addBubble(bubble1)
            bubbleStackView.addBubble(bubble2)
        }
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()

        assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
        assertThat(bubbleData.bubbles).hasSize(2)
        assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
        assertThat(bubble2.iconView).isNotNull()

        val expandListener = FakeBubbleExpandListener()
        bubbleStackView.setExpandListener(expandListener)

        var lastUpdate: BubbleData.Update? = null
        val semaphore = Semaphore(0)
        val listener =
            BubbleData.Listener { update ->
                lastUpdate = update
                semaphore.release()
            }
        bubbleData.setListener(listener)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            bubble2.iconView!!.performClick()
            assertThat(bubbleData.isExpanded).isTrue()

            bubbleStackView.setSelectedBubble(bubble2)
            bubbleStackView.isExpanded = true
            shellExecutor.flushAll()
        }

        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
        assertThat(lastUpdate!!.expanded).isTrue()
        assertThat(lastUpdate!!.bubbles.map { it.key })
            .containsExactly("bubble2", "bubble1")
            .inOrder()

        // wait for idle to allow the animation to start
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        // wait for the expansion animation to complete before interacting with the bubbles
        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
            AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)

        // make the IME visible and tap on bubble1 to select it
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            positioner.setImeVisible(true, 100)
            bubble1.iconView!!.performClick()
            // we have to set the selected bubble in the stack view manually because we don't have a
            // listener wired up.
            bubbleStackView.setSelectedBubble(bubble1)
            shellExecutor.flushAll()
        }

        val onImeHidden = bubbleStackViewManager.onImeHidden
        assertThat(onImeHidden).isNotNull()

        assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("bubble2" to true))

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            onImeHidden!!.run()
            shellExecutor.flushAll()
        }

        assertThat(expandListener.bubblesExpandedState)
            .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
        assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
    }

    @Test
    fun tapDifferentBubble_imeHidden_updatesImmediately() {
        val bubble1 = createAndInflateChatBubble(key = "bubble1")
        val bubble2 = createAndInflateChatBubble(key = "bubble2")
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            bubbleStackView.addBubble(bubble1)
            bubbleStackView.addBubble(bubble2)
        }
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()

        assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
        assertThat(bubbleData.bubbles).hasSize(2)
        assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
        assertThat(bubble2.iconView).isNotNull()

        val expandListener = FakeBubbleExpandListener()
        bubbleStackView.setExpandListener(expandListener)

        var lastUpdate: BubbleData.Update? = null
        val semaphore = Semaphore(0)
        val listener =
            BubbleData.Listener { update ->
                lastUpdate = update
                semaphore.release()
            }
        bubbleData.setListener(listener)

        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            bubble2.iconView!!.performClick()
            assertThat(bubbleData.isExpanded).isTrue()

            bubbleStackView.setSelectedBubble(bubble2)
            bubbleStackView.isExpanded = true
            shellExecutor.flushAll()
        }

        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
        assertThat(lastUpdate!!.expanded).isTrue()
        assertThat(lastUpdate!!.bubbles.map { it.key })
            .containsExactly("bubble2", "bubble1")
            .inOrder()

        // wait for idle to allow the animation to start
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        // wait for the expansion animation to complete before interacting with the bubbles
        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
            AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)

        // make the IME hidden and tap on bubble1 to select it
        InstrumentationRegistry.getInstrumentation().runOnMainSync {
            positioner.setImeVisible(false, 0)
            bubble1.iconView!!.performClick()
            // we have to set the selected bubble in the stack view manually because we don't have a
            // listener wired up.
            bubbleStackView.setSelectedBubble(bubble1)
            shellExecutor.flushAll()
        }

        val onImeHidden = bubbleStackViewManager.onImeHidden
        assertThat(onImeHidden).isNull()

        assertThat(expandListener.bubblesExpandedState)
            .isEqualTo(mapOf("bubble1" to true, "bubble2" to false))
        assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
        assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
    }

    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
    @Test
    @Test
    fun testCreateStackView_noOverflowContents_noOverflow() {
    fun testCreateStackView_noOverflowContents_noOverflow() {
@@ -563,4 +709,18 @@ class BubbleStackViewTest {
            this.onImeHidden = onImeHidden
            this.onImeHidden = onImeHidden
        }
        }
    }
    }

    private class FakeBubbleExpandListener : BubbleExpandListener {
        val bubblesExpandedState = mutableMapOf<String, Boolean>()
        override fun onBubbleExpandChanged(isExpanding: Boolean, key: String) {
            bubblesExpandedState[key] = isExpanding
        }
    }

    private class FakeSurfaceSynchronizer : SurfaceSynchronizer {
        var isActive = true
        override fun syncSurfaceAndRun(callback: Runnable) {
            if (isActive) callback.run()
        }
    }
}
}
+29 −24
Original line number Original line Diff line number Diff line
@@ -2183,8 +2183,7 @@ public class BubbleStackView extends FrameLayout
        ProtoLog.d(WM_SHELL_BUBBLES, "showNewlySelectedBubble b=%s, previouslySelected=%s,"
        ProtoLog.d(WM_SHELL_BUBBLES, "showNewlySelectedBubble b=%s, previouslySelected=%s,"
                        + " mIsExpanded=%b", newlySelectedKey, previouslySelectedKey, mIsExpanded);
                        + " mIsExpanded=%b", newlySelectedKey, previouslySelectedKey, mIsExpanded);
        if (mIsExpanded) {
        if (mIsExpanded) {
            hideCurrentInputMethod();
            Runnable onImeHidden = () -> {

                if (Flags.enableRetrievableBubbles()) {
                if (Flags.enableRetrievableBubbles()) {
                    if (mBubbleData.getBubbles().size() == 1) {
                    if (mBubbleData.getBubbles().size() == 1) {
                        // First bubble, check if overflow visibility needs to change
                        // First bubble, check if overflow visibility needs to change
@@ -2192,9 +2191,9 @@ public class BubbleStackView extends FrameLayout
                    }
                    }
                }
                }


            // Make the container of the expanded view transparent before removing the expanded view
                // Make the container of the expanded view transparent before removing the expanded
            // from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
                // view from it. Otherwise a punch hole created by {@link android.view.SurfaceView}
            // expanded view becomes visible on the screen. See b/126856255
                // in the expanded view becomes visible on the screen. See b/126856255
                mExpandedViewContainer.setAlpha(0.0f);
                mExpandedViewContainer.setAlpha(0.0f);
                mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
                mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
                    if (previouslySelected != null) {
                    if (previouslySelected != null) {
@@ -2211,6 +2210,12 @@ public class BubbleStackView extends FrameLayout
                    notifyExpansionChanged(previouslySelected, false /* expanded */);
                    notifyExpansionChanged(previouslySelected, false /* expanded */);
                    notifyExpansionChanged(bubbleToSelect, true /* expanded */);
                    notifyExpansionChanged(bubbleToSelect, true /* expanded */);
                });
                });
            };
            if (mPositioner.isImeVisible()) {
                hideCurrentInputMethod(onImeHidden);
            } else {
                onImeHidden.run();
            }
        }
        }
    }
    }