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

Commit a7f66443 authored by Jorge Gil's avatar Jorge Gil
Browse files

Fix WindowDecorationInsets equals() comparison

Equivalent WindowDecorationInsets data classes were not resulting in
equals() calls returning true for three reasons:
1) Migrating the class from Java to a Kotlin data class without
   implementing a custom equals did not WAI because the |boundingRects|
   field was an Array<Rect> which can't be compared with a plain equals.
   To fix this, the field is changed to a List<Rect>
2) With the introduction of relative insets, the |frame| provided to
   WindowDecorationInsets is not fully needed to update insets - only
   its height which means having |frame| in the data class will force
   fail an equals() for different frame rects that actually have the
   same height, so to be more efficient this change makes the primary
   constructor use height so that equals() is based on just the height
   and adds a secondary constructor with the |frame| Rect to keep
   callsites unchanged.
3) The |excludedFromAppBounds| and |taskBounds| are used together to set
   the app bounds exclusing captions. This is only needed when
   |excludedFromAppBounds| is true, but having |taskBounds| in the
   primary ctor even when false means that equals() fails because
   taskBounds can change even if they won't be used. To fix this a
   nullable AppBoundsExclusion data class encapsulates the two distinct
   states.

Flag: com.android.window.flags.enable_window_decoration_refactor
Bug: 409648813
Test: relayout same decoration twice, see only 1 inset update
Test: move task to another location (so frame changes but height
doesn't), see no additional updates
Test: atest WindowDecorationInsetsTest WindowDecorationTests

Change-Id: I5766e919ee57fef15e42e5eb514bb8d46f4474f1
parent 6f585e15
Loading
Loading
Loading
Loading
+9 −10
Original line number Diff line number Diff line
@@ -69,6 +69,7 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSuppl
import com.android.wm.shell.windowdecor.extension.InsetsStateKt;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Supplier;
@@ -483,29 +484,26 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
        // Caption bounding rectangles: these are optional, and are used to present finer
        // insets than traditional |Insets| to apps about where their content is occluded.
        // These are also in absolute coordinates.
        final Rect[] boundingRects;
        final List<Rect> boundingRects = new ArrayList<>();
        final int numOfElements = params.mOccludingCaptionElements.size();
        if (numOfElements == 0) {
            boundingRects = null;
        } else {
        if (numOfElements != 0) {
            // The customizable region can at most be equal to the caption bar.
            if (params.hasInputFeatureSpy()) {
                outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
            }
            final Resources resources = mDecorWindowContext.getResources();
            boundingRects = new Rect[numOfElements];
            for (int i = 0; i < numOfElements; i++) {
                final OccludingCaptionElement element =
                        params.mOccludingCaptionElements.get(i);
                final int elementWidthPx =
                        resources.getDimensionPixelSize(element.mWidthResId);
                boundingRects[i] =
                final Rect boundingRect =
                        calculateBoundingRectLocal(element, elementWidthPx, captionInsetsRect);
                boundingRects.add(boundingRect);
                // Subtract the regions used by the caption elements, the rest is
                // customizable.
                if (params.hasInputFeatureSpy()) {
                    outResult.mCustomizableCaptionRegion.op(boundingRects[i],
                            Region.Op.DIFFERENCE);
                    outResult.mCustomizableCaptionRegion.op(boundingRect, Region.Op.DIFFERENCE);
                }
            }
        }
@@ -884,8 +882,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>

        final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
        final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token,
                mOwner, captionInsets, null  /* taskFrame */,  null /* boundingRects */,
                0 /* flags */, true /* shouldAddCaptionInset */, false /* excludedFromAppBounds */);
                mOwner, captionInsets, null  /* taskFrame */,
                Collections.emptyList() /* boundingRects */, 0 /* flags */,
                true /* shouldAddCaptionInset */, false /* excludedFromAppBounds */);
        if (!newInsets.equals(mWindowDecorationInsets)) {
            mWindowDecorationInsets = newInsets;
            mWindowDecorationInsets.update(wct);
+97 −48
Original line number Diff line number Diff line
@@ -23,31 +23,71 @@ import android.view.InsetsSource
import android.view.WindowInsets
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION

/** Adds, removes, and updates caption insets. */
data class WindowDecorationInsets(
data class WindowDecorationInsets private constructor(
    private val token: WindowContainerToken,
    private val owner: Binder,
    private val frame: Rect,
    private val taskFrame: Rect? = null,
    private val boundingRects: Array<Rect>? = null,
    private val frame: Frame,
    private val boundingRects: List<Rect> = emptyList(),
    @InsetsSource.Flags private val flags: Int = 0,
    private val shouldAddCaptionInset: Boolean = false,
    private val excludedFromAppBounds: Boolean = false,
    private val appBoundsExclusion: AppBoundsExclusion? = null,
) {
    private sealed class Frame {
        abstract val height: Int

        data class Absolute(val rect: Rect, override val height: Int = rect.height()) : Frame()
        data class Relative(override val height: Int) : Frame()
    }
    private data class AppBoundsExclusion(val taskFrame: Rect)

    constructor(
        token: WindowContainerToken,
        owner: Binder,
        frame: Rect,
        taskFrame: Rect? = null,
        boundingRects: List<Rect> = emptyList(),
        @InsetsSource.Flags flags: Int = 0,
        shouldAddCaptionInset: Boolean = false,
        excludedFromAppBounds: Boolean = false,
    ) : this(
        token,
        owner,
        if (Flags.relativeInsets()) {
            Frame.Relative(frame.height())
        } else {
            Frame.Absolute(frame)
        },
        boundingRects,
        flags,
        shouldAddCaptionInset,
        if (excludedFromAppBounds) AppBoundsExclusion(checkNotNull(taskFrame)) else null
    )

    /** Updates the caption insets. */
    fun update(wct: WindowContainerTransaction) {
        if (!shouldAddCaptionInset) return
        if (com.android.window.flags.Flags.relativeInsets()) {
            val insets = Insets.of(0, frame.height(), 0, 0)
        logD(
            "update insets for wc=%s with frame=%s, rects=%s, appBoundsExclusion=%s",
            token,
            frame,
            boundingRects,
            appBoundsExclusion,
        )
        val rects = if (boundingRects.isEmpty()) null else boundingRects.toTypedArray()
        when (frame) {
            is Frame.Absolute -> {
                wct.addInsetsSource(
                    token,
                    owner,
                    INDEX,
                    WindowInsets.Type.captionBar(),
                insets,
                boundingRects,
                    frame.rect,
                    rects,
                    flags,
                )
                wct.addInsetsSource(
@@ -55,18 +95,20 @@ data class WindowDecorationInsets(
                    owner,
                    INDEX,
                    WindowInsets.Type.mandatorySystemGestures(),
                insets,
                boundingRects,
                    frame.rect,
                    rects,
                    /* flags= */ 0,
                )
        } else {
            }
            is Frame.Relative -> {
                val insets = Insets.of(0, frame.height, 0, 0)
                wct.addInsetsSource(
                    token,
                    owner,
                    INDEX,
                    WindowInsets.Type.captionBar(),
                frame,
                boundingRects,
                    insets,
                    rects,
                    flags,
                )
                wct.addInsetsSource(
@@ -74,14 +116,16 @@ data class WindowDecorationInsets(
                    owner,
                    INDEX,
                    WindowInsets.Type.mandatorySystemGestures(),
                frame,
                boundingRects,
                    insets,
                    rects,
                    /* flags= */ 0,
                )
            }
        if (excludedFromAppBounds) {
            val appBounds = Rect(taskFrame)
            appBounds.top += frame.height()
        }
        appBoundsExclusion?.let { exclusion ->
            val appBounds = Rect(exclusion.taskFrame).apply {
                top += frame.height
            }
            wct.setAppBounds(token, appBounds)
        }
    }
@@ -95,12 +139,17 @@ data class WindowDecorationInsets(
            INDEX,
            WindowInsets.Type.mandatorySystemGestures()
        )
        if (excludedFromAppBounds) {
        appBoundsExclusion?.let {
            wct.setAppBounds(token, Rect())
        }
    }

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments)
    }

    companion object {
        private const val TAG = "WindowDecorationInsets"
        /** Index for caption insets source. */
        private const val INDEX = 0
    }
+10 −17
Original line number Diff line number Diff line
@@ -355,7 +355,7 @@ abstract class CaptionController<T>(
            owner = insetsOwner,
            frame = captionInsets,
            taskFrame = null,
            boundingRects = null,
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
@@ -388,35 +388,28 @@ abstract class CaptionController<T>(
        // These are also in absolute coordinates.
        val numOfElements = params.occludingCaptionElements.size
        val customizableCaptionRegion = Region.obtain()
        val boundingRects: Array<Rect>?
        if (numOfElements == 0) {
            boundingRects = null
        } else {
        val boundingRects = mutableListOf<Rect>()
        if (numOfElements != 0) {
            // The customizable region can at most be equal to the caption bar.
            if (params.hasInputFeatureSpy()) {
                customizableCaptionRegion.set(captionInsetsRect)
            }
            val resources = decorWindowContext.resources
            boundingRects = Array(numOfElements) { Rect() }

            for (i in 0 until numOfElements) {
                val element = params.occludingCaptionElements[i]
                val elementWidthPx = resources.getDimensionPixelSize(element.widthResId)
                boundingRects[i].set(
                    calculateBoundingRectLocal(
                val boundingRect = calculateBoundingRectLocal(
                    element,
                    elementWidthPx,
                    captionInsetsRect,
                    decorWindowContext
                )
                )
                boundingRects.add(boundingRect)
                // Subtract the regions used by the caption elements, the rest is
                // customizable.
                if (params.hasInputFeatureSpy()) {
                    customizableCaptionRegion.op(
                        boundingRects[i],
                        Region.Op.DIFFERENCE
                    )
                    customizableCaptionRegion.op(boundingRect, Region.Op.DIFFERENCE)
                }
            }
        }
+157 −0
Original line number 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.windowdecor

import android.graphics.Rect
import android.os.Binder
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

/**
 * Tests for [WindowDecorationInsets].
 *
 * Build/Install/Run:
 * atest WMShellUnitTests:WindowDecorationInsetsTest
 */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class WindowDecorationInsetsTest {
    @JvmField @Rule val setFlagsRule = SetFlagsRule()

    private val token: WindowContainerToken = mock()
    private val owner = Binder()

    @Test
    fun `equals`() {
        val frame = Rect(0, 0, 1000, 80)
        val taskFrame = Rect(0, 0, 1000, 600)
        val insets1 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = taskFrame,
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )
        val insets2 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = taskFrame,
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )

        assertThat(insets1).isEqualTo(insets2)
    }

    @Test
    fun `equals with bounding rects`() {
        val frame = Rect(0, 0, 1000, 80)
        val taskFrame = Rect(0, 0, 1000, 600)
        val rects = listOf(Rect(0, 0, 300, 80), Rect(800, 0, 1000, 80))
        val insets1 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = taskFrame,
            boundingRects = rects,
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )
        val insets2 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = taskFrame,
            boundingRects = rects,
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )

        assertThat(insets1).isEqualTo(insets2)
    }

    @Test
    @EnableFlags(Flags.FLAG_RELATIVE_INSETS)
    fun `equals with different frame but same height`() {
        val taskFrame = Rect(0, 0, 1000, 600)
        val insets1 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = Rect(0, 0, 1000, 80),
            taskFrame = taskFrame,
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )
        val insets2 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = Rect(100, 0, 1000, 80),
            taskFrame = taskFrame,
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )

        assertThat(insets1).isEqualTo(insets2)
    }

    @Test
    fun `equals with different task frame but no app bounds exclusion`() {
        val frame = Rect(0, 0, 1000, 80)
        val insets1 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = Rect(0, 0, 1000, 600),
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )
        val insets2 = WindowDecorationInsets(
            token = token,
            owner = owner,
            frame = frame,
            taskFrame = Rect(10, 0, 1010, 600),
            boundingRects = emptyList(),
            flags = 0,
            shouldAddCaptionInset = true,
            excludedFromAppBounds = false
        )

        assertThat(insets1).isEqualTo(insets2)
    }
}
+33 −1
Original line number Diff line number Diff line
@@ -62,6 +62,8 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.os.LocaleList;
import android.os.Looper;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.UsesFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.util.DisplayMetrics;
@@ -905,7 +907,8 @@ public class WindowDecorationTests extends ShellTestCase {
    }

    @Test
    public void testRelayout_captionFrameChanged_insetsReapplied() {
    @DisableFlags(Flags.FLAG_RELATIVE_INSETS)
    public void testRelayout_taskFrameChanged_insetsReapplied() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);
@@ -930,6 +933,35 @@ public class WindowDecorationTests extends ShellTestCase {
        verifyAddedInsets(2 /* times */, token, 0 /* index */, mandatorySystemGestures());
    }

    @Test
    @EnableFlags(Flags.FLAG_RELATIVE_INSETS)
    public void testRelayout_captionFrameChanged_insetsReapplied() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);
        mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
        final WindowContainerToken token = new MockToken().token();
        final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true);
        mRelayoutParams.mIsCaptionVisible = true;

        // Relayout twice with different caption heights.
        final ActivityManager.RunningTaskInfo firstTaskInfo =
                builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
        final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
        mRelayoutParams.mCaptionHeightCalculator = (context, display) -> 80;
        windowDecor.relayout(firstTaskInfo, true /* hasGlobalFocus */);
        final ActivityManager.RunningTaskInfo secondTaskInfo =
                builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
        mRelayoutParams.mCaptionHeightCalculator = (context, display) -> 100;
        windowDecor.relayout(secondTaskInfo, true /* hasGlobalFocus */);

        // Insets should be applied twice.
        verifyAddedInsets(2 /* times */, token, 0 /* index */, captionBar());
        verifyAddedInsets(2 /* times */, token, 0 /* index */, mandatorySystemGestures());
    }

    @Test
    public void testRelayout_captionFrameUnchanged_insetsNotApplied() {
        final Display defaultDisplay = mock(Display.class);