diff options
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 |