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

Commit 82997f25 authored by Maryam Dehaini's avatar Maryam Dehaini Committed by Android (Google) Code Review
Browse files

Merge "[2/N] WindowDecor refactor: WindowDecoration2 and CaptionController" into main

parents 6d28e081 c21500df
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -16,6 +16,6 @@

package com.android.wm.shell.windowdecor;

interface TaskFocusStateConsumer {
public interface TaskFocusStateConsumer {
    void setTaskFocusState(boolean focused);
}
+0 −70
Original line number Diff line number Diff line
@@ -17,8 +17,6 @@
package com.android.wm.shell.windowdecor;

import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
@@ -55,7 +53,6 @@ import android.window.DesktopExperienceFlags;
import android.window.DesktopModeFlags;
import android.window.SurfaceSyncGroup;
import android.window.TaskConstants;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import com.android.internal.annotations.VisibleForTesting;
@@ -72,9 +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.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Supplier;

@@ -1022,69 +1017,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
            return new SurfaceControlViewHost(c, d, wmm, callsite);
        }
    }

    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 mTaskFrame;
        private final Rect[] mBoundingRects;
        private final @InsetsSource.Flags int mFlags;
        private final boolean mShouldAddCaptionInset;
        private final boolean mExcludedFromAppBounds;

        private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame,
                Rect taskFrame, Rect[] boundingRects, @InsetsSource.Flags int flags,
                boolean shouldAddCaptionInset, boolean excludedFromAppBounds) {
            mToken = token;
            mOwner = owner;
            mFrame = frame;
            mTaskFrame = taskFrame;
            mBoundingRects = boundingRects;
            mFlags = flags;
            mShouldAddCaptionInset = shouldAddCaptionInset;
            mExcludedFromAppBounds = excludedFromAppBounds;
        }

        void update(WindowContainerTransaction wct) {
            if (mShouldAddCaptionInset) {
                wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects,
                        mFlags);
                wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
                        mBoundingRects, 0 /* flags */);
                if (mExcludedFromAppBounds) {
                    final Rect appBounds = new Rect(mTaskFrame);
                    appBounds.top += mFrame.height();
                    wct.setAppBounds(mToken, appBounds);
                }
            }
        }

        void remove(WindowContainerTransaction wct) {
            wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar());
            wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures());
            if (mExcludedFromAppBounds) {
                wct.setAppBounds(mToken, new Rect());
            }
        }

        @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.equals(mTaskFrame, that.mTaskFrame)
                    && Objects.deepEquals(mBoundingRects, that.mBoundingRects)
                    && mFlags == that.mFlags
                    && mShouldAddCaptionInset == that.mShouldAddCaptionInset
                    && mExcludedFromAppBounds == that.mExcludedFromAppBounds;
        }

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

File added.

Preview size limit exceeded, changes collapsed.

+84 −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.view.InsetsSource
import android.view.WindowInsets
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction

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

    /** Updates the caption insets. */
    fun update(wct: WindowContainerTransaction) {
        if (!shouldAddCaptionInset) return
        wct.addInsetsSource(
            token,
            owner,
            INDEX,
            WindowInsets.Type.captionBar(),
            frame,
            boundingRects,
            flags,
        )
        wct.addInsetsSource(
            token,
            owner,
            INDEX,
            WindowInsets.Type.mandatorySystemGestures(),
            frame,
            boundingRects,
            /* flags= */ 0,
        )
        if (excludedFromAppBounds) {
            val appBounds = Rect(taskFrame)
            appBounds.top += frame.height()
            wct.setAppBounds(token, appBounds)
        }
    }

    /** Removes the caption insets. */
    fun remove(wct: WindowContainerTransaction) {
        wct.removeInsetsSource(token, owner, INDEX, WindowInsets.Type.captionBar())
        wct.removeInsetsSource(
            token,
            owner,
            INDEX,
            WindowInsets.Type.mandatorySystemGestures()
        )
        if (excludedFromAppBounds) {
            wct.setAppBounds(token, Rect())
        }
    }

    companion object {
        /** Index for caption insets source. */
        private const val INDEX = 0
    }
}
+431 −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.caption

import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.Region
import android.os.Binder
import android.view.Display
import android.view.SurfaceControl
import android.view.View
import android.view.WindowManager
import android.window.WindowContainerTransaction
import com.android.app.tracing.traceSection
import com.android.wm.shell.windowdecor.TaskFocusStateConsumer
import com.android.wm.shell.windowdecor.WindowDecoration2.RelayoutParams
import com.android.wm.shell.windowdecor.WindowDecoration2.RelayoutParams.OccludingCaptionElement.Alignment
import com.android.wm.shell.windowdecor.WindowDecorationInsets
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier
import com.android.wm.shell.windowdecor.extension.getDimensionPixelSize
import com.android.wm.shell.windowdecor.extension.isRtl


/**
 * Creates, updates, and removes the caption and its related menus based on [RunningTaskInfo]
 * changes and user interactions.
 *
 * @param <T> The type of the caption's root view
 */
abstract class CaptionController<T>(
    private val windowDecorViewHostSupplier:
    WindowDecorViewHostSupplier<WindowDecorViewHost>,
) where T : View, T : TaskFocusStateConsumer {

    private lateinit var taskInfo: RunningTaskInfo
    private var captionInsets: WindowDecorationInsets? = null
    private val insetsOwner = Binder()
    private var captionViewHost: WindowDecorViewHost? = null
    private var rootView: T? = null

    private var isCaptionVisible = false

    /** Inflates the correct caption view. */
    abstract fun inflateCaptionView(): T

    /** Resource Id of caption layout. */
    abstract val captionResId: Int

    /**
     * Returns the caption height given the additional padding that will be added to the top of the
     * caption.
     */
    abstract fun getCaptionHeight(captionPadding: Int): Int

    /**
     * Called by [WindowDecoration2] to trigger a new relayout to update the caption and its views.
     */
    open fun relayout(
        params: RelayoutParams,
        parentContainer: SurfaceControl,
        display: Display,
        decorWindowContext: Context,
        startT: SurfaceControl.Transaction,
        finishT: SurfaceControl.Transaction,
        wct: WindowContainerTransaction,
    ): CaptionRelayoutResult? = traceSection("CaptionController#relayout") {
        inflateViewsIfNeeded()
        isCaptionVisible = params.isCaptionVisible
        val view = rootView ?: return null
        val viewHost = getOrCreateViewHost(decorWindowContext, display)
        val resources = decorWindowContext.resources
        val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds
        val captionHeight = getCaptionHeight(params.captionTopPadding)
        val captionWidth = resources.getDimensionPixelSize(
            params.captionWidthId,
            taskBounds.width()
        )
        val captionX = (taskBounds.width() - captionWidth) / 2
        val captionY = 0
        val captionTopPadding = params.captionTopPadding

        updateCaptionContainerSurface(
            parentContainer,
            startT,
            captionWidth,
            captionHeight,
            captionX
        )
        val customizableCaptionRegion =
            updateCaptionInsets(params, decorWindowContext, wct, captionHeight, taskBounds)

        traceSection("CaptionController#relayout-updateViewHost") {
            view.setPadding(
                view.paddingLeft,
                params.captionTopPadding,
                view.paddingRight,
                view.paddingBottom
            )
            view.setTaskFocusState(params.hasGlobalFocus)
            val localCaptionBounds = Rect(
                captionX,
                captionY,
                captionX + captionWidth,
                captionY + captionHeight
            )
            val touchableRegion = if (params.limitTouchRegionToSystemAreas) {
                calculateLimitedTouchableRegion(
                    params,
                    decorWindowContext,
                    localCaptionBounds
                )
            } else null
            updateViewHierarchy(
                params,
                viewHost,
                view,
                captionWidth,
                captionHeight,
                startT,
                touchableRegion
            )
        }

        return CaptionRelayoutResult(
            captionHeight = captionHeight,
            captionWidth = captionWidth,
            captionX = captionX,
            captionY = captionY,
            captionTopPadding = captionTopPadding,
            customizableCaptionRegion = customizableCaptionRegion,
            captionRootView = view,
        )
    }

    private fun updateViewHierarchy(
        params: RelayoutParams,
        viewHost: WindowDecorViewHost,
        view: View,
        captionWidth: Int,
        captionHeight: Int,
        startT: SurfaceControl.Transaction,
        touchableRegion: Region?
    ) = traceSection("CaptionController#updateViewHierarchy") {
        val lp = WindowManager.LayoutParams(
            captionWidth,
            captionHeight,
            WindowManager.LayoutParams.TYPE_APPLICATION,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSPARENT
        ).apply {
            title = "Caption of Task=" + taskInfo.taskId
            setTrustedOverlay()
            inputFeatures = params.inputFeatures
        }
        if (params.asyncViewHost) {
            require(!params.applyStartTransactionOnDraw) {
                "Cannot use sync draw tx with async relayout"
            }
            viewHost.updateViewAsync(
                view,
                lp,
                taskInfo.configuration,
                touchableRegion
            )
        } else {
            viewHost.updateView(
                view,
                lp,
                taskInfo.configuration,
                touchableRegion,
                if (params.applyStartTransactionOnDraw) startT else null
            )
        }
    }

    private fun calculateLimitedTouchableRegion(
        params: RelayoutParams,
        decorWindowContext: Context,
        localCaptionBounds: Rect,
    ): Region {
        // Make caption bounds relative to display to align with exclusion region.
        val positionInParent = params.runningTaskInfo.positionInParent
        val captionBoundsInDisplay = Rect(localCaptionBounds).apply {
            offsetTo(positionInParent.x, positionInParent.y)
        }

        val boundingRects = calculateBoundingRectsRegion(
            params,
            decorWindowContext,
            captionBoundsInDisplay
        )

        val customizedRegion = Region.obtain().apply {
            set(captionBoundsInDisplay)
            op(boundingRects, Region.Op.DIFFERENCE)
            op(params.displayExclusionRegion, Region.Op.INTERSECT)
        }

        val touchableRegion = Region.obtain().apply {
            set(captionBoundsInDisplay)
            op(customizedRegion, Region.Op.DIFFERENCE)
            // Return resulting region back to window coordinates.
            translate(-positionInParent.x, -positionInParent.y)
        }

        boundingRects.recycle()
        customizedRegion.recycle()
        return touchableRegion
    }

    private fun calculateBoundingRectsRegion(
        params: RelayoutParams,
        decorWindowContext: Context,
        captionBoundsInDisplay: Rect,
    ): Region {
        val numOfElements = params.occludingCaptionElements.size
        val region = Region.obtain()
        if (numOfElements == 0) {
            // The entire caption is a bounding rect.
            region.set(captionBoundsInDisplay)
            return region
        }
        val resources = decorWindowContext.resources
        params.occludingCaptionElements.forEach { element ->
            val elementWidthPx = resources.getDimensionPixelSize(element.widthResId)
            val boundingRect = calculateBoundingRectLocal(
                element,
                elementWidthPx,
                captionBoundsInDisplay,
                decorWindowContext,
            )
            // Bounding rect is initially calculated relative to the caption, so offset it to make
            // it relative to the display.
            boundingRect.offset(captionBoundsInDisplay.left, captionBoundsInDisplay.top)
            region.union(boundingRect)
        }
        return region
    }

    private fun calculateBoundingRectLocal(
        element: RelayoutParams.OccludingCaptionElement,
        elementWidthPx: Int,
        captionRect: Rect,
        decorWindowContext: Context,
    ): Rect {
        val isRtl = decorWindowContext.isRtl
        return when (element.alignment) {
            Alignment.START -> {
                if (isRtl) {
                    Rect(
                        captionRect.width() - elementWidthPx,
                        0,
                        captionRect.width(),
                        captionRect.height()
                    )
                } else {
                    Rect(0, 0, elementWidthPx, captionRect.height())
                }
            }

            Alignment.END -> {
                if (isRtl) {
                    Rect(0, 0, elementWidthPx, captionRect.height())
                } else {
                    Rect(
                        captionRect.width() - elementWidthPx, 0,
                        captionRect.width(), captionRect.height()
                    )
                }
            }
        }
    }

    private fun updateCaptionContainerSurface(
        parentContainer: SurfaceControl,
        startT: SurfaceControl.Transaction,
        captionWidth: Int,
        captionHeight: Int,
        captionX: Int
    ) {
        val captionSurface = captionViewHost?.surfaceControl ?: return
        startT.reparent(captionSurface, parentContainer)
            .setWindowCrop(captionSurface, captionWidth, captionHeight)
            .setPosition(captionSurface, captionX.toFloat(), /* y= */ 0f)
            .setLayer(captionSurface, CAPTION_LAYER_Z_ORDER)
            .show(captionSurface)
    }

    private fun updateCaptionInsets(
        params: RelayoutParams,
        decorWindowContext: Context,
        wct: WindowContainerTransaction,
        captionHeight: Int,
        taskBounds: Rect
    ): Region {
        if (!isCaptionVisible || !params.isInsetSource) {
            captionInsets?.remove(wct)
            captionInsets = null
            return Region.obtain()
        }
        // 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.
        val captionInsetsRect = Rect(taskBounds)
        captionInsetsRect.bottom = captionInsetsRect.top + captionHeight

        // 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.
        val boundingRects: Array<Rect>?
        val numOfElements = params.occludingCaptionElements.size
        val customizableCaptionRegion = Region.obtain()
        if (numOfElements == 0) {
            boundingRects = null
        } else {
            // 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(
                        element,
                        elementWidthPx,
                        captionInsetsRect,
                        decorWindowContext
                    )
                )
                // Subtract the regions used by the caption elements, the rest is
                // customizable.
                if (params.hasInputFeatureSpy()) {
                    customizableCaptionRegion.op(
                        boundingRects[i],
                        Region.Op.DIFFERENCE
                    )
                }
            }
        }

        val newInsets = WindowDecorationInsets(
            taskInfo.token,
            insetsOwner,
            captionInsetsRect,
            taskBounds,
            boundingRects,
            params.insetSourceFlags,
            params.isInsetSource,
            params.shouldSetAppBounds
        )
        if (newInsets != captionInsets) {
            // Add or update this caption as an insets source.
            captionInsets = newInsets
            newInsets.update(wct)
        }

        return customizableCaptionRegion
    }

    private fun inflateViewsIfNeeded() {
        if (rootView == null) {
            rootView = inflateCaptionView()
        }
    }

    /** Releases all caption views. Returns true if caption view host is released. */
    fun releaseViews(
        wct: WindowContainerTransaction,
        t: SurfaceControl.Transaction
    ): Boolean {
        captionInsets?.remove(wct)
        captionInsets = null

        val viewHost = captionViewHost ?: return false
        viewHost.release(t)
        captionViewHost = null
        return true
    }

    private fun getOrCreateViewHost(
        context: Context,
        display: Display
    ): WindowDecorViewHost = traceSection("CaptionController#getOrCreateViewHost") {
        return captionViewHost ?: windowDecorViewHostSupplier.acquire(context, display)
    }

    /** Caption data calculated during [relayout]. */
    data class CaptionRelayoutResult(
        // The caption height with caption padding included
        val captionHeight: Int,
        val captionWidth: Int,
        val captionX: Int,
        val captionY: Int,
        val captionTopPadding: Int,
        val customizableCaptionRegion: Region,
        val captionRootView: View,
    )

    companion object {
        /**
         * The Z-order of the caption surface.
         *
         *
         * We use [decorationContainerSurface] to define input window for task resizing; by
         * layering it in front of the caption surface, we can allow it to handle input
         * prior to caption view itself, treating corner inputs as resize events rather than
         * repositioning.
         */
        private const val CAPTION_LAYER_Z_ORDER: Int = -1
    }
}
Loading