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

Commit 2a9dba8d authored by Simon (Qiong) Sun's avatar Simon (Qiong) Sun Committed by Android (Google) Code Review
Browse files

Merge "Update the PiP display layout with the navigation bar offset." into main

parents b6505cfd 466346ea
Loading
Loading
Loading
Loading
+45 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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.Insets
import android.graphics.Rect
import android.view.InsetsState
import android.view.WindowInsets.Type
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener

abstract class NavigationBarsListener(
    private val displayController: DisplayController,
    val displayId: Int
) : OnInsetsChangedListener {
    private var oldInsets = Insets.NONE

    override fun insetsChanged(insetsState: InsetsState) {
        getNavigationBarsInsets(insetsState).takeIf { it != oldInsets }?.let { newInsets ->
            oldInsets = newInsets
            onNavigationBarsVisibilityChanged(newInsets)
        }
    }

    private fun getNavigationBarsInsets(insetsState: InsetsState): Insets {
        val layout = displayController.getDisplayLayout(displayId) ?: return Insets.NONE
        return insetsState.calculateInsets(Rect(0, 0, layout.width(), layout.height()),
            Type.navigationBars(), /* ignoreVisibility= */true)
    }

    protected abstract fun onNavigationBarsVisibilityChanged(insets: Insets)
}
 No newline at end of file
+20 −7
Original line number Original line Diff line number Diff line
@@ -18,8 +18,11 @@ package com.android.wm.shell.common.pip;


import static com.android.wm.shell.common.pip.PipUtils.dpToPx;
import static com.android.wm.shell.common.pip.PipUtils.dpToPx;


import static java.lang.Math.max;

import android.content.Context;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Rect;
import android.util.Size;
import android.util.Size;
@@ -45,8 +48,8 @@ public class PipDisplayLayoutState {
    private Context mContext;
    private Context mContext;
    private int mDisplayId;
    private int mDisplayId;
    @NonNull private DisplayLayout mDisplayLayout;
    @NonNull private DisplayLayout mDisplayLayout;

    private Point mScreenEdgeInsets = null;
    private Point mScreenEdgeInsets = null;
    private Insets mNavigationBarsInsets = Insets.NONE;


    @Inject
    @Inject
    public PipDisplayLayoutState(Context context) {
    public PipDisplayLayoutState(Context context) {
@@ -81,12 +84,16 @@ public class PipDisplayLayoutState {
     * Returns the inset bounds the PIP window can be visible in.
     * Returns the inset bounds the PIP window can be visible in.
     */
     */
    public Rect getInsetBounds() {
    public Rect getInsetBounds() {
        Rect insetBounds = new Rect();
        final Rect insetBounds = new Rect();
        Rect insets = getDisplayLayout().stableInsets();
        final Rect stableInsets = getDisplayLayout().stableInsets();
        insetBounds.set(insets.left + getScreenEdgeInsets().x,
        final Point screenEdgeInsets = getScreenEdgeInsets();
                insets.top + getScreenEdgeInsets().y,
        final int left = max(stableInsets.left, mNavigationBarsInsets.left) + screenEdgeInsets.x;
                getDisplayLayout().width() - insets.right - getScreenEdgeInsets().x,
        final int top = max(stableInsets.top, mNavigationBarsInsets.top) + screenEdgeInsets.y;
                getDisplayLayout().height() - insets.bottom - getScreenEdgeInsets().y);
        final int right = getDisplayLayout().width()
                - max(stableInsets.right, mNavigationBarsInsets.right) - screenEdgeInsets.x;
        final int bottom = getDisplayLayout().height()
                - max(stableInsets.bottom, mNavigationBarsInsets.bottom) - screenEdgeInsets.y;
        insetBounds.set(left, top, right, bottom);
        return insetBounds;
        return insetBounds;
    }
    }


@@ -131,6 +138,11 @@ public class PipDisplayLayoutState {
        mDisplayId = displayId;
        mDisplayId = displayId;
    }
    }


    /** Set the navigationBars side and widthOrHeight. */
    public void setNavigationBarsInsets(Insets insets) {
        mNavigationBarsInsets = insets;
    }

    /** Dumps internal state. */
    /** Dumps internal state. */
    public void dump(PrintWriter pw, String prefix) {
    public void dump(PrintWriter pw, String prefix) {
        final String innerPrefix = prefix + "  ";
        final String innerPrefix = prefix + "  ";
@@ -138,5 +150,6 @@ public class PipDisplayLayoutState {
        pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
        pw.println(innerPrefix + "mDisplayId=" + mDisplayId);
        pw.println(innerPrefix + "getDisplayBounds=" + getDisplayBounds());
        pw.println(innerPrefix + "getDisplayBounds=" + getDisplayBounds());
        pw.println(innerPrefix + "mScreenEdgeInsets=" + mScreenEdgeInsets);
        pw.println(innerPrefix + "mScreenEdgeInsets=" + mScreenEdgeInsets);
        pw.println(innerPrefix + "mNavigationBarsInsets=" + mNavigationBarsInsets);
    }
    }
}
}
+11 −0
Original line number Original line Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Bundle;
import android.util.Log;
import android.util.Log;
@@ -51,6 +52,7 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.ImeListener;
import com.android.wm.shell.common.ImeListener;
import com.android.wm.shell.common.NavigationBarsListener;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -245,6 +247,15 @@ public class PipController implements ConfigurationChangeListener,
                        mPipTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
                        mPipTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight);
                    }
                    }
                });
                });
        mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
                new NavigationBarsListener(mDisplayController,
                        mPipDisplayLayoutState.getDisplayId()) {
                    @Override
                    protected void onNavigationBarsVisibilityChanged(
                            @NonNull Insets insets) {
                        mPipDisplayLayoutState.setNavigationBarsInsets(insets);
                    }
                });


        // Allow other outside processes to bind to PiP controller using the key below.
        // Allow other outside processes to bind to PiP controller using the key below.
        mShellController.addExternalInterface(IPip.DESCRIPTOR,
        mShellController.addExternalInterface(IPip.DESCRIPTOR,
+199 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2025 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
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 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 NavigationBarsListenerTest : ShellTestCase() {
    private lateinit var navigationBarsListener: TestingNavigationBarsListener
    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)
        navigationBarsListener = TestingNavigationBarsListener(displayController, DEFAULT_DISPLAY_ID)
    }

    @Test
    fun insetsChanged_bottomNavigationBars_callsOnBottomNavigationBarsAppears() {
        val insets = Insets.of(0, 0, 0, DEFAULT_NAVIGATION_BARS_SIZE)
        val insetsState = createBottomNavigationBars(insets)
        navigationBarsListener.insetsChanged(insetsState)
        assertEquals(insets, navigationBarsListener.testInsets)
    }

    @Test
    fun insetsChanged_rightNavigationBars_callsOnRightNavigationBarsAppears() {
        val insets = Insets.of(0, 0, DEFAULT_NAVIGATION_BARS_SIZE, 0)
        val insetsStateWithNavigationBars = createRightNavigationBars(insets)
        navigationBarsListener.insetsChanged(insetsStateWithNavigationBars)
        assertEquals(insets, navigationBarsListener.testInsets)
    }

    @Test
    fun insetsChanged_zeroNavigationBars_callsOnNavigationBarsDisappears() {
        val zeroInsets = Insets.of(0, 0, 0, 0)
        val insetsStateWithoutNavigationBars = createBottomNavigationBars(zeroInsets)
        navigationBarsListener.insetsChanged(insetsStateWithoutNavigationBars)
        assertEquals(Insets.NONE, navigationBarsListener.testInsets)
    }

    @Test
    fun insetsChanged_imeAppears_doesNotCallOnNavigationBarsInsetsChanged() {
        val insets = Insets.of(0, 0, 0, DEFAULT_IME_HEIGHT)
        // Send insetsState with an IME.
        val insetsState = createIme(insets)
        navigationBarsListener.insetsChanged(insetsState)
        assertEquals(Insets.NONE, navigationBarsListener.testInsets)
    }

    private fun createBottomNavigationBars(insets: Insets): InsetsState {
        val stableBounds = Rect()
        displayLayout.getStableBounds(stableBounds)
        val displayRect = Rect()
        displayRect.set(0, 0, displayLayout.width(), displayLayout.height())

        val insetsState = InsetsState()
        val insetsSource = insetsState.getOrCreateSource(
            InsetsSource.createId(
                null, 0, Type.navigationBars()), Type.navigationBars()
        )

        val frame = Rect(0, displayLayout.height() - insets.bottom,
            displayLayout.width(), displayLayout.height())
        insetsSource.setFrame(frame)
        return insetsState
    }

    private fun createRightNavigationBars(insets: Insets): InsetsState {
        val stableBounds = Rect()
        displayLayout.getStableBounds(stableBounds)
        val displayRect = Rect()
        displayRect.set(0, 0, displayLayout.width(), displayLayout.height())

        val insetsState = InsetsState()
        val insetsSource = insetsState.getOrCreateSource(
            InsetsSource.createId(
                null, 0, Type.navigationBars()), Type.navigationBars()
        )

        val frame = Rect(displayLayout.width() - insets.right, 0,
                displayLayout.width(), displayLayout.height())
        insetsSource.setFrame(frame)
        return insetsState
    }

    private fun createIme(insets: Insets): InsetsState {
        val stableBounds = Rect()
        displayLayout.getStableBounds(stableBounds)
        val displayRect = Rect()
        displayRect.set(0, 0, displayLayout.width(), displayLayout.height())

        val insetsState = InsetsState()
        val insetsSource = insetsState.getOrCreateSource(ID_IME, Type.ime())
        insetsSource.setVisible(true)
        insetsSource.setFrame(stableBounds.left, stableBounds.bottom - insets.bottom,
            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 TestingNavigationBarsListener(
        displayController: DisplayController,
        displayId: Int
    ) : NavigationBarsListener(displayController, displayId) {
        var testInsets = Insets.NONE

        override fun onNavigationBarsVisibilityChanged(insets: Insets) {
            testInsets = insets
        }
    }

    companion object {
        private const val DEFAULT_DISPLAY_ID = 0
        private const val DEFAULT_NAVIGATION_BARS_SIZE = 100
        private const val DEFAULT_IME_HEIGHT = 500
    }
}
 No newline at end of file