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

Commit fc197f12 authored by Ikram Gabiyev's avatar Ikram Gabiyev
Browse files

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
parent c32f9542
Loading
Loading
Loading
Loading
+70 −0
Original line number Diff line number Diff line
/*
 * 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
+6 −1
Original line number Diff line number Diff line
@@ -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,
+152 −0
Original line number Diff line number Diff line
/*
 * 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