From fc197f12ef9255c68e00a502965ef4a935aab6c9 Mon Sep 17 00:00:00 2001 From: Ikram Gabiyev Date: Wed, 17 Jul 2024 15:58:20 -0700 Subject: Use insets updates to get IME changes [1/N] Use DisplayInsetsController's OnInsetsChangedListener to get callbacks into PiP upon IME visibility and height changes. This is accomplished through querying the stable bounds of the display cached by PipDisplayLayoutState and through querying the InsetsSource with the id of ID_IME. This is done as a part of PiP2's implementation of task being moved upon IME appearing/disappearing. Bug: 352596856 Flag: com.android.wm.shell.enable_pip2_implementation Test: atest ImeListenerTest Change-Id: I49ff24a3f7b0bdfee11fed01ab4d747ef3435b1f --- .../src/com/android/wm/shell/common/ImeListener.kt | 70 ++++++++++ .../android/wm/shell/pip2/phone/PipController.java | 7 +- .../com/android/wm/shell/common/ImeListenerTest.kt | 152 +++++++++++++++++++++ 3 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt create mode 100644 libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/ImeListenerTest.kt 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 { + 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 -- cgit v1.2.3-59-g8ed1b