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

Commit 8fefb0f6 authored by Johannes Gallmann's avatar Johannes Gallmann
Browse files

[Floaty] Remove TopLevel Window

This CL refactors the assistant invocation effect to not use its own
window anymore for animating the squeeze border. Instead, the squeeze
border is animated directly via surface transaction in WM Shell. This
is done to prevent gpu memory regressions.

This comes at the cost of non pixel-perfect corners since these can only
be clipped by a round radius whereas many devices do not have perfectly
round corners (but use a custom path instead).

Bug: 411435519
Flag: com.android.systemui.shared.enable_lpp_assist_invocation_effect
Test: TopLevelWindowEffectsTest
Test: Manual, i.e. extensive stress testing of the squeeze effect on
      multiple devices
Change-Id: I9d2ba3134a11b01dbcf7da1112decc517c072a4c
parent d93ecf61
Loading
Loading
Loading
Loading
+9 −10
Original line number Diff line number Diff line
@@ -32,21 +32,20 @@ public interface AppZoomOut {
    void setProgress(float progress);

    /**
     * Sets the top-level scaling factor applied to all content on the screen during a zoom-out.
     * Sets the squeeze effect progress.
     *
     * <p>The {@code scale} parameter determines the current zoom level, ranging from {@code 0f} to
     * {@code 1f}.
     * <p>The {@code progress} parameter determines the current zoom level and surface crop, ranging
     * from {@code 0f} to {@code 1f}.
     * <ul>
     * <li>A value of {@code 1.0f} indicates no scaling (content is displayed at its original
     * size).</li>
     * <li>A value of {@code 0.0f} represents the maximum zoom-out, effectively scaling the
     * content to zero size (though visually it might be constrained).</li>
     * <li>A value of {@code 0f} indicates no scaling and cropping (content is displayed at its
     * original size).</li>
     * <li>A value of {@code 0.0f} represents the maximum zoom-out, effectively scaling and cropping
     * the content to the max pushback level</li>
     * <li>Values between {@code 0.0f} and {@code 1.0f} represent intermediate zoom levels.</li>
     * </ul>
     *
     * @param scale The scaling factor to apply, where {@code 1.0f} is no scale and {@code 0.0f} is
     *              maximum zoom-out.
     * @param progress The progress to set the squeeze zoom effect to.
     */
    void setTopLevelScale(float scale);
    void setTopLevelProgress(float progress);

}
+7 −7
Original line number Diff line number Diff line
@@ -85,7 +85,7 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController
        AppZoomOutDisplayAreaOrganizer appDisplayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer(
                context, displayLayout, mainExecutor);
        TopLevelZoomOutDisplayAreaOrganizer topLevelDisplayAreaOrganizer =
                new TopLevelZoomOutDisplayAreaOrganizer(displayLayout, mainExecutor);
                new TopLevelZoomOutDisplayAreaOrganizer(displayLayout, context, mainExecutor);
        return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController,
                appDisplayAreaOrganizer, topLevelDisplayAreaOrganizer, mainExecutor);
    }
@@ -135,13 +135,13 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController

    /**
     * Scales all content on the screen belonging to
     * {@link DisplayAreaOrganizer#FEATURE_WINDOWED_MAGNIFICATION}.
     * {@link DisplayAreaOrganizer#FEATURE_WINDOWED_MAGNIFICATION} and applies a cropping.
     *
     * @param scale scale factor to be applied to the surfaces.
     * @param progress progress to be applied to the top-level zoom effect.
     */
    private void setTopLevelScale(float scale) {
    private void setTopLevelProgress(float progress) {
        if (enableLppAssistInvocationEffect()) {
            mTopLevelDisplayAreaOrganizer.setScale(scale);
            mTopLevelDisplayAreaOrganizer.setProgress(progress);
        }
    }

@@ -197,8 +197,8 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController
        }

        @Override
        public void setTopLevelScale(float scale) {
            mMainExecutor.execute(() -> AppZoomOutController.this.setTopLevelScale(scale));
        public void setTopLevelProgress(float progress) {
            mMainExecutor.execute(() -> AppZoomOutController.this.setTopLevelProgress(progress));
        }
    }
}
+0 −120
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.appzoomout;

import android.content.Context;
import android.util.ArrayMap;
import android.view.Display;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
import android.window.DisplayAreaInfo;
import android.window.DisplayAreaOrganizer;
import android.window.WindowContainerToken;

import com.android.wm.shell.common.DisplayLayout;

import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;

/** Display area organizer that manages the top level zoom out UI and states. */
public class TopLevelZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer {
    private final DisplayLayout mDisplayLayout = new DisplayLayout();
    private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
            new ArrayMap<>();

    private float mScale = 1f;

    public TopLevelZoomOutDisplayAreaOrganizer(DisplayLayout displayLayout, Executor mainExecutor) {
        super(mainExecutor);
        setDisplayLayout(displayLayout);
    }

    @Override
    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) {
        leash.setUnreleasedWarningCallSite(
                "TopLevelZoomDisplayAreaOrganizer.onDisplayAreaAppeared");
        if (displayAreaInfo.displayId == Display.DEFAULT_DISPLAY) {
            mDisplayAreaTokenMap.put(displayAreaInfo.token, leash);
        }
    }

    @Override
    public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
        final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token);
        if (leash != null) {
            leash.release();
        }
        mDisplayAreaTokenMap.remove(displayAreaInfo.token);
    }

    /**
     * Registers the TopLevelZoomOutDisplayAreaOrganizer to manage the display area of
     * {@link DisplayAreaOrganizer#FEATURE_WINDOWED_MAGNIFICATION}.
     */
    void registerOrganizer() {
        final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer(
                DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION);
        for (int i = 0; i < displayAreaInfos.size(); i++) {
            final DisplayAreaAppearedInfo info = displayAreaInfos.get(i);
            onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash());
        }
    }

    @Override
    public void unregisterOrganizer() {
        super.unregisterOrganizer();
        reset();
    }

    void setScale(float scale) {
        if (mScale == scale) {
            return;
        }

        mScale = scale;
        apply();
    }

    private void apply() {
        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
        mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, mScale));
        tx.apply();
    }

    private void reset() {
        setScale(1f);
    }

    private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) {
        tx
                .setScale(leash, scale, scale)
                .setPosition(leash, (1f - scale) * mDisplayLayout.width() * 0.5f,
                        (1f - scale) * mDisplayLayout.height() * 0.5f);
    }

    void setDisplayLayout(DisplayLayout displayLayout) {
        mDisplayLayout.set(displayLayout);
    }

    void onRotateDisplay(Context context, int toRotation) {
        if (mDisplayLayout.rotation() == toRotation) {
            return;
        }
        mDisplayLayout.rotateTo(context.getResources(), toRotation);
    }
}
+178 −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.appzoomout

import android.content.Context
import android.util.ArrayMap
import android.view.Choreographer
import android.view.Display
import android.view.SurfaceControl
import android.window.DisplayAreaInfo
import android.window.DisplayAreaOrganizer
import android.window.WindowContainerToken
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.wm.shell.common.DisplayLayout
import java.util.concurrent.Executor
import kotlin.math.max

private const val SqueezeEffectMaxThicknessDp = 16
// Defines the amount the squeeze border overlaps the shrinking content on the shorter display edge.
// At full progress, the overlap is 4 dp on the shorter display edge. On the longer display edge, it
// will be more than 4 dp, depending on the display aspect ratio.
private const val SqueezeEffectOverlapShortEdgeThicknessDp = 4

/** Display area organizer that manages the top level zoom out UI and states.  */
class TopLevelZoomOutDisplayAreaOrganizer(
    displayLayout: DisplayLayout,
    private val context: Context,
    mainExecutor: Executor
) : DisplayAreaOrganizer(mainExecutor) {

    private val mDisplayAreaTokenMap: MutableMap<WindowContainerToken, SurfaceControl> = ArrayMap()
    private val mDisplayLayout = DisplayLayout()
    private var cornerRadius = 1f
    private var mProgress = 1f

    init {
        setDisplayLayout(displayLayout)
    }

    override fun onDisplayAreaAppeared(displayAreaInfo: DisplayAreaInfo, leash: SurfaceControl) {
        leash.setUnreleasedWarningCallSite("TopLevelZoomDisplayAreaOrganizer.onDisplayAreaAppeared")
        if (displayAreaInfo.displayId == Display.DEFAULT_DISPLAY) {
            mDisplayAreaTokenMap[displayAreaInfo.token] = leash
        }
    }

    override fun onDisplayAreaVanished(displayAreaInfo: DisplayAreaInfo) {
        val leash = mDisplayAreaTokenMap[displayAreaInfo.token]
        leash?.release()
        mDisplayAreaTokenMap.remove(displayAreaInfo.token)
    }

    /**
     * Registers the TopLevelZoomOutDisplayAreaOrganizer to manage the display area of
     * [DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION].
     */
    fun registerOrganizer() {
        val displayAreaInfos = registerOrganizer(FEATURE_WINDOWED_MAGNIFICATION)
        for (i in displayAreaInfos.indices) {
            val info = displayAreaInfos[i]
            onDisplayAreaAppeared(info.displayAreaInfo, info.leash)
        }
    }

    override fun unregisterOrganizer() {
        super.unregisterOrganizer()
        reset()
    }

    fun setProgress(progress: Float) {
        if (mProgress == progress) {
            return
        }

        mProgress = progress
        apply()
    }

    private fun apply() {
        val tx = SurfaceControl.Transaction()
        mDisplayAreaTokenMap.values.forEach { leash: SurfaceControl ->
            updateSurface(tx, leash, mProgress)
        }
        tx.apply()
    }

    private fun reset() {
        setProgress(1f)
    }

    private fun updateSurface(
        tx: SurfaceControl.Transaction,
        leash: SurfaceControl,
        progress: Float
    ) {
        if (progress == 0f) {
            // Reset when scale is set back to 0.
            tx
                .setCrop(leash, null)
                .setScale(leash, 1f, 1f)
                .setPosition(leash, 0f, 0f)
                .setCornerRadius(leash, 0f)
            return
        }
        // Get display dimensions once
        val displayWidth = mDisplayLayout.width()
        val displayHeight = mDisplayLayout.height()
        val displayWidthF = displayWidth.toFloat()
        val displayHeightF = displayHeight.toFloat()

        // Convert DP thickness values to pixels
        val maxThicknessPx = mDisplayLayout.dpToPx(SqueezeEffectMaxThicknessDp)
        val overlapShortEdgeThicknessPx = mDisplayLayout.dpToPx(SqueezeEffectOverlapShortEdgeThicknessDp)

        // Determine the longer edge of the display
        val longEdgePx = max(displayWidth, displayHeight) // Will be Int, but division with Float promotes

        // Calculate the potential for zooming based on thickness parameters
        // This represents how much the content "shrinks" due to the squeeze effect on both sides.
        val zoomPotentialPx = (maxThicknessPx - overlapShortEdgeThicknessPx) * 2f

        val zoomOutScale = 1f - (progress * zoomPotentialPx / longEdgePx)

        // Calculate the current thickness of the squeeze effect based on progress
        val squeezeThickness = maxThicknessPx * progress

        // Calculate the X and Y offsets needed to center the scaled content.
        // These values are also used to adjust the crop region.
        // (1f - zoomOutScale) is the percentage of size reduction.
        // Half of this reduction, applied to the width/height, gives the offset for centering.
        val positionXOffset = (1f - zoomOutScale) * displayWidthF * 0.5f
        val positionYOffset = (1f - zoomOutScale) * displayHeightF * 0.5f

        // Calculate crop values.
        // The squeezeThickness acts as an initial margin/inset.
        // This margin is then reduced by the positionOffset, because as the view scales down
        // and moves towards the center, less cropping is needed to achieve the same visual margin
        // relative to the scaled content.
        val horizontalCrop = squeezeThickness - positionXOffset
        val verticalCrop = squeezeThickness - positionYOffset

        // Calculate the right and bottom crop coordinates
        val cropRight = displayWidthF - horizontalCrop
        val cropBottom = displayHeightF - verticalCrop

        tx
            .setCrop(leash, horizontalCrop, verticalCrop, cropRight, cropBottom)
            .setCornerRadius(leash, cornerRadius * zoomOutScale)
            .setScale(leash, zoomOutScale, zoomOutScale)
            .setPosition(leash, positionXOffset, positionYOffset)
            .setFrameTimelineVsync(Choreographer.getInstance().vsyncId)
    }

    fun setDisplayLayout(displayLayout: DisplayLayout) {
        mDisplayLayout.set(displayLayout)
        cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
    }

    fun onRotateDisplay(context: Context, toRotation: Int) {
        if (mDisplayLayout.rotation() == toRotation) {
            return
        }
        mDisplayLayout.rotateTo(context.resources, toRotation)
    }
}
+234 −61

File changed.

Preview size limit exceeded, changes collapsed.

Loading