diff options
| author | 2024-07-17 15:58:20 -0700 | |
|---|---|---|
| committer | 2024-07-25 05:48:59 -0700 | |
| commit | fc197f12ef9255c68e00a502965ef4a935aab6c9 (patch) | |
| tree | 54f27f8929c520f47ab8297f1e5f4d214a1b305b | |
| parent | c32f9542f400809fd0575c55a660c8413077cff1 (diff) | |
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
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 |