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

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

Optimize window decor inset source updates

Adding or removing inset sources through WCT requires a binder
transaction that is expensive and may result in jank.

To avoid unnecessary inset updates, this change:
1) Adds a WindowDecorationInsets object caching the latest inset source
   parameters sent for its window decoration, to allow diffing whenever
   the decoration relayouts and the caption insets may need to be
   updated.
2) Bundles inset source updates into the same WCT in a #relayout. Prior
   to this change, when the caption SCVH had to be recreated during a
   #relayout, it first released its views and removed the inset source
   with one WCT, and then recreated the views and added the new inset
   source using a different WCT.

Bug: 335975211
Test: enter split-screen through overview button, check app handles
still work and get updated on resizes. Also check perfetto trace to see
reduced number of binder transactions.

Change-Id: I0ebfc27885c7047ee2e27509739317eb4ad74f5e
parent 8cc072a3
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -649,10 +649,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
    }

    @Override
    void releaseViews() {
    void releaseViews(WindowContainerTransaction wct) {
        closeHandleMenu();
        closeMaximizeMenu();
        super.releaseViews();
        super.releaseViews(wct);
    }

    /**
+82 −29
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor;

import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.statusBars;

import android.annotation.NonNull;
@@ -41,11 +43,11 @@ import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.SurfaceSyncGroup;
import android.window.TaskConstants;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import com.android.wm.shell.ShellTaskOrganizer;
@@ -54,7 +56,9 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

/**
@@ -131,8 +135,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
    TaskDragResizer mTaskDragResizer;
    private boolean mIsCaptionVisible;

    /** The most recent set of insets applied to this window decoration. */
    private WindowDecorationInsets mWindowDecorationInsets;
    private final Binder mOwner = new Binder();
    private final Rect mCaptionInsetsRect = new Rect();
    private final float[] mTmpColor = new float[3];

    WindowDecoration(
@@ -203,7 +208,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
        mLayoutResId = params.mLayoutResId;

        if (!mTaskInfo.isVisible) {
            releaseViews();
            releaseViews(wct);
            finishT.hide(mTaskSurface);
            return;
        }
@@ -226,7 +231,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
                || mDisplay.getDisplayId() != mTaskInfo.displayId
                || oldLayoutResId != mLayoutResId
                || oldNightMode != newNightMode) {
            releaseViews();
            releaseViews(wct);

            if (!obtainDisplayOrRegisterListener()) {
                outResult.mRootView = null;
@@ -300,8 +305,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
            // Caption inset is the full width of the task with the |captionHeight| and
            // positioned at the top of the task bounds, also in absolute coordinates.
            // So just reuse the task bounds and adjust the bottom coordinate.
            mCaptionInsetsRect.set(taskBounds);
            mCaptionInsetsRect.bottom = mCaptionInsetsRect.top + outResult.mCaptionHeight;
            final Rect captionInsetsRect = new Rect(taskBounds);
            captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;

            // Caption bounding rectangles: these are optional, and are used to present finer
            // insets than traditional |Insets| to apps about where their content is occluded.
@@ -313,7 +318,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
            } else {
                // The customizable region can at most be equal to the caption bar.
                if (params.hasInputFeatureSpy()) {
                    outResult.mCustomizableCaptionRegion.set(mCaptionInsetsRect);
                    outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
                }
                boundingRects = new Rect[numOfElements];
                for (int i = 0; i < numOfElements; i++) {
@@ -322,7 +327,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
                    final int elementWidthPx =
                            resources.getDimensionPixelSize(element.mWidthResId);
                    boundingRects[i] =
                            calculateBoundingRect(element, elementWidthPx, mCaptionInsetsRect);
                            calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
                    // Subtract the regions used by the caption elements, the rest is
                    // customizable.
                    if (params.hasInputFeatureSpy()) {
@@ -331,18 +336,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
                    }
                }
            }
            // Add this caption as an inset source.
            wct.addInsetsSource(mTaskInfo.token,
                    mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect,
                    boundingRects);
            wct.addInsetsSource(mTaskInfo.token,
                    mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
                    mCaptionInsetsRect, null /* boundingRects */);

            final WindowDecorationInsets newInsets = new WindowDecorationInsets(
                    mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
            if (!newInsets.equals(mWindowDecorationInsets)) {
                // Add or update this caption as an insets source.
                mWindowDecorationInsets = newInsets;
                mWindowDecorationInsets.addOrUpdate(wct);
            }
        } else {
            wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
                    WindowInsets.Type.captionBar());
            wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
                    WindowInsets.Type.mandatorySystemGestures());
            if (mWindowDecorationInsets != null) {
                mWindowDecorationInsets.remove(wct);
                mWindowDecorationInsets = null;
            }
        }

        // Task surface itself
@@ -481,7 +487,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
        return true;
    }

    void releaseViews() {
    void releaseViews(WindowContainerTransaction wct) {
        if (mViewHost != null) {
            mViewHost.release();
            mViewHost = null;
@@ -507,19 +513,21 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
            t.apply();
        }

        final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
        wct.removeInsetsSource(mTaskInfo.token,
                mOwner, 0 /* index */, WindowInsets.Type.captionBar());
        wct.removeInsetsSource(mTaskInfo.token,
                mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures());
        mTaskOrganizer.applyTransaction(wct);
        if (mWindowDecorationInsets != null) {
            mWindowDecorationInsets.remove(wct);
            mWindowDecorationInsets = null;
        }
    }

    @Override
    public void close() {
        Trace.beginSection("WindowDecoration#close");
        mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
        releaseViews();
        final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
        releaseViews(wct);
        mTaskOrganizer.applyTransaction(wct);
        mTaskSurface.release();
        Trace.endSection();
    }

    static int loadDimensionPixelSize(Resources resources, int resourceId) {
@@ -594,8 +602,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>

        final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
        final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
        wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(),
                captionInsets, null /* boundingRects */);
        final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token,
                mOwner, captionInsets, null /* boundingRets */);
        if (!newInsets.equals(mWindowDecorationInsets)) {
            mWindowDecorationInsets = newInsets;
            mWindowDecorationInsets.addOrUpdate(wct);
        }
    }

    static class RelayoutParams {
@@ -677,6 +689,47 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
        }
    }

    private static class WindowDecorationInsets {
        private static final int INDEX = 0;
        private final WindowContainerToken mToken;
        private final Binder mOwner;
        private final Rect mFrame;
        private final Rect[] mBoundingRects;

        private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame,
                Rect[] boundingRects) {
            mToken = token;
            mOwner = owner;
            mFrame = frame;
            mBoundingRects = boundingRects;
        }

        void addOrUpdate(WindowContainerTransaction wct) {
            wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects);
            wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
                    mBoundingRects);
        }

        void remove(WindowContainerTransaction wct) {
            wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar());
            wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures());
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof WindowDecoration.WindowDecorationInsets that)) return false;
            return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner,
                    that.mOwner) && Objects.equals(mFrame, that.mFrame)
                    && Objects.deepEquals(mBoundingRects, that.mBoundingRects);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects));
        }
    }

    /**
     * Subclass for additional windows associated with this WindowDecoration
     */
+143 −4
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.quality.Strictness.LENIENT;
@@ -64,6 +65,7 @@ import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
import android.window.SurfaceSyncGroup;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import androidx.test.filters.SmallTest;
@@ -610,32 +612,168 @@ public class WindowDecorationTests extends ShellTestCase {
        mockitoSession.finishMocking();
    }

    @Test
    public void testRelayout_captionHidden_insetsRemoved() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);

        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true)
                .setBounds(new Rect(0, 0, 1000, 1000))
                .build();
        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);

        // Run it once so that insets are added.
        mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
        windowDecor.relayout(taskInfo);

        // Run it again so that insets are removed.
        mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false);
        windowDecor.relayout(taskInfo);

        verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(captionBar()));
        verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()));
    }

    @Test
    public void testInsetsRemovedWhenCaptionIsHidden() {
    public void testRelayout_captionHidden_neverWasVisible_insetsNotRemoved() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);

        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true)
                .setBounds(new Rect(0, 0, 1000, 1000))
                .build();
        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);

        // Hidden from the beginning, so no insets were ever added.
        mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false);
        windowDecor.relayout(taskInfo);

        // Never added.
        verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(captionBar()), any(), any());
        verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
        // No need to remove them if they were never added.
        verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
                any(), eq(0) /* index */, eq(captionBar()));
        verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
                any(), eq(0) /* index */, eq(mandatorySystemGestures()));
    }

    @Test
    public void testClose_withExistingInsets_insetsRemoved() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);

        final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
                new ActivityManager.TaskDescription.Builder();
        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setTaskDescriptionBuilder(taskDescriptionBuilder)
                .setVisible(true)
                .setBounds(new Rect(0, 0, 1000, 1000))
                .build();
        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);

        // Relayout will add insets.
        mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
        windowDecor.relayout(taskInfo);
        verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(captionBar()), any(), any());
        verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());

        windowDecor.close();

        // Insets should be removed.
        verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(captionBar()));
        verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()));
    }

    @Test
    public void testClose_withoutExistingInsets_insetsNotRemoved() {
        final Display defaultDisplay = mock(Display.class);
        doReturn(defaultDisplay).when(mMockDisplayController)
                .getDisplay(Display.DEFAULT_DISPLAY);

        final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true)
                .setBounds(new Rect(0, 0, 1000, 1000))
                .build();
        final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);

        windowDecor.close();

        // No need to remove insets.
        verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
                any(), eq(0) /* index */, eq(captionBar()));
        verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
                any(), eq(0) /* index */, eq(mandatorySystemGestures()));
    }

    @Test
    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 = TestRunningTaskInfoBuilder.createMockWCToken();
        final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true);

        // Relayout twice with different bounds.
        final ActivityManager.RunningTaskInfo firstTaskInfo =
                builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
        final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
        windowDecor.relayout(firstTaskInfo);
        final ActivityManager.RunningTaskInfo secondTaskInfo =
                builder.setToken(token).setBounds(new Rect(50, 50, 1000, 1000)).build();
        windowDecor.relayout(secondTaskInfo);

        // Insets should be applied twice.
        verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
                eq(0) /* index */, eq(captionBar()), any(), any());
        verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
    }

    @Test
    public void testRelayout_captionFrameUnchanged_insetsNotApplied() {
        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 = TestRunningTaskInfoBuilder.createMockWCToken();
        final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
                .setDisplayId(Display.DEFAULT_DISPLAY)
                .setVisible(true);

        // Relayout twice with the same bounds.
        final ActivityManager.RunningTaskInfo firstTaskInfo =
                builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
        final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
        windowDecor.relayout(firstTaskInfo);
        final ActivityManager.RunningTaskInfo secondTaskInfo =
                builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
        windowDecor.relayout(secondTaskInfo);

        // Insets should only need to be applied once.
        verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
                eq(0) /* index */, eq(captionBar()), any(), any());
        verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
                eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
    }

    @Test
    public void testTaskPositionAndCropNotSetWhenFalse() {
        final Display defaultDisplay = mock(Display.class);
@@ -763,6 +901,7 @@ public class WindowDecorationTests extends ShellTestCase {

        void relayout(ActivityManager.RunningTaskInfo taskInfo,
                boolean applyStartTransactionOnDraw) {
            mRelayoutParams.mRunningTaskInfo = taskInfo;
            mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
            relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
                    mMockWindowContainerTransaction, mMockView, mRelayoutResult);