summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt70
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt152
3 files changed, 228 insertions, 1 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt
new file mode 100644
index 000000000000..a34d7bed497b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.graphics.Rect
+import android.view.InsetsSource
+import android.view.InsetsState
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener
+
+abstract class ImeListener(
+ private val mDisplayController: DisplayController,
+ private val mDisplayId: Int
+) : OnInsetsChangedListener {
+ // The last insets state
+ private val mInsetsState = InsetsState()
+ private val mTmpBounds = Rect()
+
+ override fun insetsChanged(insetsState: InsetsState) {
+ if (mInsetsState == insetsState) {
+ return
+ }
+
+ // Get the stable bounds that account for display cutout and system bars to calculate the
+ // relative IME height
+ val layout = mDisplayController.getDisplayLayout(mDisplayId)
+ if (layout == null) {
+ return
+ }
+ layout.getStableBounds(mTmpBounds)
+
+ val wasVisible = getImeVisibilityAndHeight(mInsetsState).first
+ val oldHeight = getImeVisibilityAndHeight(mInsetsState).second
+
+ val isVisible = getImeVisibilityAndHeight(insetsState).first
+ val newHeight = getImeVisibilityAndHeight(insetsState).second
+
+ mInsetsState.set(insetsState, true)
+ if (wasVisible != isVisible || oldHeight != newHeight) {
+ onImeVisibilityChanged(isVisible, newHeight)
+ }
+ }
+
+ private fun getImeVisibilityAndHeight(
+ insetsState: InsetsState): Pair<Boolean, Int> {
+ val source = insetsState.peekSource(InsetsSource.ID_IME)
+ val frame = if (source != null && source.isVisible) source.frame else null
+ val height = if (frame != null) mTmpBounds.bottom - frame.top else 0
+ val visible = source?.isVisible ?: false
+ return Pair(visible, height)
+ }
+
+ /**
+ * To be overridden by implementations to handle IME changes.
+ */
+ protected abstract fun onImeVisibilityChanged(imeVisible: Boolean, imeHeight: Int)
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index b939b169d8bd..8aa093379ee7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -44,6 +44,7 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ExternalInterfaceBinder;
+import com.android.wm.shell.common.ImeListener;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -56,7 +57,6 @@ import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.Pip;
-import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -201,6 +201,11 @@ public class PipController implements ConfigurationChangeListener,
.getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));
}
});
+ mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
+ new ImeListener(mDisplayController, mPipDisplayLayoutState.getDisplayId()) {
+ @Override
+ public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {}
+ });
// Allow other outside processes to bind to PiP controller using the key below.
mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt
new file mode 100644
index 000000000000..3b0a0722968b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Insets
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.DisplayCutout
+import android.view.DisplayInfo
+import android.view.InsetsSource.ID_IME
+import android.view.InsetsState
+import android.view.Surface
+import android.view.WindowInsets.Type
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.wm.shell.ShellTestCase
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.kotlin.whenever
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ImeListenerTest : ShellTestCase() {
+ private lateinit var imeListener: CachingImeListener
+ private lateinit var displayLayout: DisplayLayout
+
+ @Mock private lateinit var displayController: DisplayController
+ @Before
+ fun setUp() {
+ val resources = createResources(40, 50, false)
+ val displayInfo = createDisplayInfo(1000, 1500, 0, Surface.ROTATION_0)
+ displayLayout = DisplayLayout(displayInfo, resources, false, false)
+ whenever(displayController.getDisplayLayout(DEFAULT_DISPLAY_ID)).thenReturn(displayLayout)
+ imeListener = CachingImeListener(displayController, DEFAULT_DISPLAY_ID)
+ }
+
+ @Test
+ fun testImeAppears() {
+ val insetsState = createInsetsStateWithIme(true, DEFAULT_IME_HEIGHT)
+ imeListener.insetsChanged(insetsState)
+ assertTrue("Ime insets source should become visible", imeListener.cachedImeVisible)
+ assertEquals(DEFAULT_IME_HEIGHT, imeListener.cachedImeHeight)
+ }
+
+ @Test
+ fun testImeAppears_thenDisappears() {
+ // Send insetsState with an IME as a visible source.
+ val insetsStateWithIme = createInsetsStateWithIme(true, DEFAULT_IME_HEIGHT)
+ imeListener.insetsChanged(insetsStateWithIme)
+
+ // Send insetsState without IME.
+ val insetsStateWithoutIme = createInsetsStateWithIme(false, 0)
+ imeListener.insetsChanged(insetsStateWithoutIme)
+
+ assertFalse("Ime insets source should become invisible",
+ imeListener.cachedImeVisible)
+ assertEquals(0, imeListener.cachedImeHeight)
+ }
+
+ private fun createInsetsStateWithIme(isVisible: Boolean, imeHeight: Int): InsetsState {
+ val stableBounds = Rect()
+ displayLayout.getStableBounds(stableBounds)
+ val insetsState = InsetsState()
+
+ val insetsSource = insetsState.getOrCreateSource(ID_IME, Type.ime())
+ insetsSource.setVisible(isVisible)
+ insetsSource.setFrame(stableBounds.left, stableBounds.bottom - imeHeight,
+ stableBounds.right, stableBounds.bottom)
+ return insetsState
+ }
+
+ private fun createDisplayInfo(width: Int, height: Int, cutoutHeight: Int,
+ rotation: Int): DisplayInfo {
+ val info = DisplayInfo()
+ info.logicalWidth = width
+ info.logicalHeight = height
+ info.rotation = rotation
+ if (cutoutHeight > 0) {
+ info.displayCutout = DisplayCutout(
+ Insets.of(0, cutoutHeight, 0, 0) /* safeInsets */,
+ null /* boundLeft */,
+ Rect(width / 2 - cutoutHeight, 0, width / 2 + cutoutHeight,
+ cutoutHeight) /* boundTop */, null /* boundRight */,
+ null /* boundBottom */)
+ } else {
+ info.displayCutout = DisplayCutout.NO_CUTOUT
+ }
+ info.logicalDensityDpi = 300
+ return info
+ }
+
+ private fun createResources(navLand: Int, navPort: Int, navMoves: Boolean): Resources {
+ val cfg = Configuration()
+ cfg.uiMode = Configuration.UI_MODE_TYPE_NORMAL
+ val res = Mockito.mock(Resources::class.java)
+ Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_height_landscape_car_mode)
+ Mockito.doReturn(navPort).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_height_car_mode)
+ Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_width_car_mode)
+ Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_height_landscape)
+ Mockito.doReturn(navPort).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_height)
+ Mockito.doReturn(navLand).whenever(res).getDimensionPixelSize(
+ R.dimen.navigation_bar_width)
+ Mockito.doReturn(navMoves).whenever(res).getBoolean(R.bool.config_navBarCanMove)
+ Mockito.doReturn(cfg).whenever(res).configuration
+ return res
+ }
+
+ private class CachingImeListener(
+ displayController: DisplayController,
+ displayId: Int
+ ) : ImeListener(displayController, displayId) {
+ var cachedImeVisible = false
+ var cachedImeHeight = 0
+ public override fun onImeVisibilityChanged(imeVisible: Boolean, imeHeight: Int) {
+ cachedImeVisible = imeVisible
+ cachedImeHeight = imeHeight
+ }
+ }
+
+ companion object {
+ private const val DEFAULT_DISPLAY_ID = 0
+ private const val DEFAULT_IME_HEIGHT = 500
+ }
+} \ No newline at end of file