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

Commit e47b907e authored by Ikram Gabiyev's avatar Ikram Gabiyev Committed by Android (Google) Code Review
Browse files

Merge "Use insets updates to get IME changes [1/N]" into main

parents 300b8690 fc197f12
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