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

Commit 9fe6cf03 authored by Liran Binyamin's avatar Liran Binyamin Committed by Android (Google) Code Review
Browse files

Merge "Add unit tests for IME visibility fix after lock" into main

parents 6b6ce463 57256b57
Loading
Loading
Loading
Loading
+237 −67
Original line number Diff line number Diff line
@@ -19,12 +19,17 @@ package com.android.wm.shell.bubbles
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.content.res.Resources
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.os.Handler
import android.os.UserHandle
import android.os.UserManager
import android.view.IWindowManager
import android.view.InsetsSource
import android.view.InsetsState
import android.view.WindowInsets
import android.view.WindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,19 +38,22 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.protolog.ProtoLog
import com.android.internal.statusbar.IStatusBarService
import com.android.wm.shell.R
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.ImeListener
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.TaskStackListenerImpl
import com.android.wm.shell.common.TestShellExecutor
import com.android.wm.shell.common.TestSyncExecutor
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.recents.RecentTasksController
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -53,16 +61,18 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.taskview.TaskViewRepository
import com.android.wm.shell.taskview.TaskViewTransitions
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.unfold.UnfoldAnimationController
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import java.util.Optional
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.Optional

/** Tests for [BubbleController] */
@SmallTest
@@ -79,6 +89,10 @@ class BubbleControllerTest {
    private lateinit var bgExecutor: TestShellExecutor
    private lateinit var bubbleData: BubbleData
    private lateinit var eduController: BubbleEducationController
    private lateinit var displayController: DisplayController
    private lateinit var displayImeController: DisplayImeController
    private lateinit var displayInsetsController: DisplayInsetsController
    private lateinit var imeListener: ImeListener

    @Before
    fun setUp() {
@@ -94,18 +108,26 @@ class BubbleControllerTest {

        // Tests don't have permission to add our window to windowManager, so we mock it :(
        val windowManager = mock<WindowManager>()
        val realWindowManager = context.getSystemService(WindowManager::class.java)
        val realWindowManager = context.getSystemService(WindowManager::class.java)!!
        // But we do want the metrics from the real one
        whenever(windowManager.currentWindowMetrics)
            .thenReturn(realWindowManager.currentWindowMetrics)
        whenever(windowManager.defaultDisplay).thenReturn(realWindowManager.defaultDisplay)

        bubblePositioner = BubblePositioner(context, windowManager)
        bubblePositioner.setShowingInBubbleBar(true)

        bubbleData = BubbleData(
            context, bubbleLogger, bubblePositioner, eduController,
            mainExecutor, bgExecutor
        bubbleData =
            BubbleData(
                context,
                bubbleLogger,
                bubblePositioner,
                eduController,
                mainExecutor,
                bgExecutor
            )
        displayController = mock<DisplayController>()
        displayImeController = mock<DisplayImeController>()
        displayInsetsController = mock<DisplayInsetsController>()

        bubbleController =
            createBubbleController(
@@ -116,9 +138,16 @@ class BubbleControllerTest {
                mainExecutor,
                bgExecutor,
            )
        bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java))
        bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>())
        // Flush so that proxy gets set
        mainExecutor.flushAll()

        whenever(displayController.getDisplayLayout(anyInt()))
            .thenReturn(DisplayLayout(context, realWindowManager.defaultDisplay))
        val insetsChangedListenerCaptor = argumentCaptor<ImeListener>()
        verify(displayInsetsController)
            .addInsetsChangedListener(anyInt(), insetsChangedListenerCaptor.capture())
        imeListener = insetsChangedListenerCaptor.lastValue
    }

    @After
@@ -143,10 +172,149 @@ class BubbleControllerTest {
        assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)!!.isNote).isTrue()
    }

    @Test
    fun onDeviceLocked_expanded_imeHidden_shouldCollapseImmediately() {
        val bubble = createBubble("key")
        bubblePositioner.setImeVisible(false, 0)
        getInstrumentation().runOnMainSync {
            bubbleController.inflateAndAdd(
                bubble,
                /* suppressFlyout= */ true,
                /* showInShade= */ true
            )
        }
        assertThat(bubbleData.hasBubbles()).isTrue()

        // expand and lock the device
        getInstrumentation().runOnMainSync {
            bubbleController.expandStackAndSelectBubble(bubble)
            assertThat(bubbleData.isExpanded).isTrue()
            bubbleController.onStatusBarStateChanged(/* isShade= */ false)
        }
        // verify that we collapsed immediately, since the IME is hidden
        assertThat(bubbleData.isExpanded).isFalse()
    }

    fun createBubbleController(
    @Test
    fun onDeviceLocked_expanded_imeVisible_shouldHideImeBeforeCollapsing() {
        val bubble = createBubble("key")
        getInstrumentation().runOnMainSync {
            bubbleController.inflateAndAdd(
                bubble,
                /* suppressFlyout= */ true,
                /* showInShade= */ true
            )
        }
        assertThat(bubbleData.hasBubbles()).isTrue()

        // expand and show the IME. then lock the device
        val imeVisibleInsetsState = createFakeInsetsState(imeVisible = true)
        getInstrumentation().runOnMainSync {
            bubbleController.expandStackAndSelectBubble(bubble)
            assertThat(bubbleData.isExpanded).isTrue()
            imeListener.insetsChanged(imeVisibleInsetsState)
            assertThat(bubblePositioner.isImeVisible).isTrue()
            bubbleController.onStatusBarStateChanged(/* isShade= */ false)
        }
        // check that we haven't actually started collapsing because we weren't notified yet that
        // the IME is hidden
        assertThat(bubbleData.isExpanded).isTrue()
        // collapsing while the device is locked goes through display ime controller
        verify(displayImeController).hideImeForBubblesWhenLocked(anyInt())

        // notify that the IME was hidden
        val imeHiddenInsetsState = createFakeInsetsState(imeVisible = false)
        getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeHiddenInsetsState) }
        assertThat(bubblePositioner.isImeVisible).isFalse()
        // bubbles should be collapsed now
        assertThat(bubbleData.isExpanded).isFalse()
    }

    @Test
    fun onDeviceLocked_whileHidingImeDuringCollapse() {
        val bubble = createBubble("key")
        val expandListener = FakeBubbleExpandListener()
        bubbleController.setExpandListener(expandListener)

        getInstrumentation().runOnMainSync {
            bubbleController.inflateAndAdd(
                bubble,
                /* suppressFlyout= */ true,
                /* showInShade= */ true
            )
        }
        assertThat(bubbleData.hasBubbles()).isTrue()

        // expand
        getInstrumentation().runOnMainSync {
            bubbleController.expandStackAndSelectBubble(bubble)
            assertThat(bubbleData.isExpanded).isTrue()
            mainExecutor.flushAll()
        }

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

        // show the IME
        val imeVisibleInsetsState = createFakeInsetsState(imeVisible = true)
        getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeVisibleInsetsState) }

        assertThat(bubblePositioner.isImeVisible).isTrue()

        // collapse the stack
        getInstrumentation().runOnMainSync { bubbleController.collapseStack() }
        assertThat(bubbleData.isExpanded).isFalse()
        // since we started to collapse while the IME was visible, we will wait to be notified that
        // the IME is hidden before completing the collapse. check that the expand listener was not
        // yet called
        assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("key" to true))

        // lock the device during this state
        getInstrumentation().runOnMainSync {
            bubbleController.onStatusBarStateChanged(/* isShade= */ false)
        }
        verify(displayImeController).hideImeForBubblesWhenLocked(anyInt())

        // notify that the IME is hidden
        val imeHiddenInsetsState = createFakeInsetsState(imeVisible = false)
        getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeHiddenInsetsState) }
        assertThat(bubblePositioner.isImeVisible).isFalse()
        // verify the collapse action completed
        assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("key" to false))
    }

    private fun createBubble(key: String): Bubble {
        val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
        val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
        val bubble =
            Bubble(
                key,
                shortcutInfo,
                /* desiredHeight= */ 0,
                Resources.ID_NULL,
                "title",
                /* taskId= */ 0,
                "locus",
                /* isDismissable= */ true,
                directExecutor(),
                directExecutor()
            ) {}
        return bubble
    }

    private fun createFakeInsetsState(imeVisible: Boolean): InsetsState {
        val insetsState = InsetsState()
        if (imeVisible) {
            insetsState
                .getOrCreateSource(InsetsSource.ID_IME, WindowInsets.Type.ime())
                .setFrame(Rect(0, 100, 100, 200))
                .setVisible(true)
        }
        return insetsState
    }

    private fun createBubbleController(
        bubbleData: BubbleData,
        windowManager: WindowManager?,
        windowManager: WindowManager,
        bubbleLogger: BubbleLogger,
        bubblePositioner: BubblePositioner,
        mainExecutor: TestShellExecutor,
@@ -159,7 +327,7 @@ class BubbleControllerTest {
                context,
                shellInit,
                shellCommandHandler,
                mock<DisplayInsetsController>(),
                displayInsetsController,
                mainExecutor,
            )
        val surfaceSynchronizer = { obj: Runnable -> obj.run() }
@@ -172,26 +340,20 @@ class BubbleControllerTest {
                BubblePersistentRepository(context),
            )

        val shellTaskOrganizer = ShellTaskOrganizer(
            Mockito.mock<ShellInit>(ShellInit::class.java),
        val shellTaskOrganizer =
            ShellTaskOrganizer(
                mock<ShellInit>(),
                ShellCommandHandler(),
                null,
            Optional.empty<UnfoldAnimationController>(),
            Optional.empty<RecentTasksController>(),
                Optional.empty(),
                Optional.empty(),
                TestSyncExecutor()
            )

        val resizeChecker: ResizabilityChecker =
            object : ResizabilityChecker {
                override fun isResizableActivity(
                    intent: Intent?,
                    packageManager: PackageManager, key: String
                ): Boolean {
                    return true
                }
            }
        val resizeChecker = ResizabilityChecker { _, _, _ -> true }

        val bubbleController = BubbleController(
        val bubbleController =
            BubbleController(
                context,
                shellInit,
                shellCommandHandler,
@@ -202,15 +364,15 @@ class BubbleControllerTest {
                bubbleDataRepository,
                mock<IStatusBarService>(),
                windowManager,
            mock<DisplayInsetsController>(),
            mock<DisplayImeController>(),
                displayInsetsController,
                displayImeController,
                mock<UserManager>(),
                mock<LauncherApps>(),
                bubbleLogger,
                mock<TaskStackListenerImpl>(),
                shellTaskOrganizer,
                bubblePositioner,
            mock<DisplayController>(),
                displayController,
                Optional.empty(),
                mock<DragAndDropController>(),
                mainExecutor,
@@ -228,4 +390,12 @@ class BubbleControllerTest {

        return bubbleController
    }

    private class FakeBubbleExpandListener : BubbleExpandListener {
        val bubblesExpandedState = mutableMapOf<String, Boolean>()

        override fun onBubbleExpandChanged(isExpanding: Boolean, key: String) {
            bubblesExpandedState[key] = isExpanding
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ import android.content.pm.PackageManager
/**
 * Interface to check whether the activity backed by a specific intent is resizable.
 */
interface ResizabilityChecker {
fun interface ResizabilityChecker {

    /**
     * Returns whether the provided intent represents a resizable activity.