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

Commit 635439ac authored by Tracy Zhou's avatar Tracy Zhou
Browse files

Introduce new shell module for app zoom out (display area approach)

Bug: 370560660
Flag: com.android.systemui.spatial_model_app_pushback
Test: http://recall/-/fRr65P8aom6gXYO7mGjYFn/eXZzVEk2tUREHqWn7kErRd
Change-Id: I553df3afcadce9792255cbab642657305ba547e1
parent c66a0b2b
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -120,6 +120,14 @@ public class DisplayAreaOrganizer extends WindowOrganizer {
     */
    public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9;

    /**
     * Display area for rendering app zoom out. When there are multiple layers on the screen,
     * we want to render these layers based on a depth model. Here we zoom out the layer behind,
     * whether it's an app or the homescreen.
     * @hide
     */
    public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10;

    /**
     * The last boundary of display area for system features
     */
+33 −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 com.android.wm.shell.shared.annotations.ExternalThread;

/**
 * Interface to engage with the app zoom out feature.
 */
@ExternalThread
public interface AppZoomOut {

    /**
     * Called when the zoom out progress is updated, which is used to scale down the current app
     * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges
     * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level.
     */
    void setProgress(float progress);
}
+158 −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 static android.view.Display.DEFAULT_DISPLAY;

import android.app.ActivityManager;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.Configuration;
import android.util.Slog;
import android.window.DisplayAreaInfo;
import android.window.WindowContainerTransaction;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;

/** Class that manages the app zoom out UI and states. */
public class AppZoomOutController implements RemoteCallable<AppZoomOutController>,
        ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener {

    private static final String TAG = "AppZoomOutController";

    private final Context mContext;
    private final ShellTaskOrganizer mTaskOrganizer;
    private final DisplayController mDisplayController;
    private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer;
    private final ShellExecutor mMainExecutor;
    private final AppZoomOutImpl mImpl = new AppZoomOutImpl();

    private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener =
            new DisplayController.OnDisplaysChangedListener() {
                @Override
                public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
                    if (displayId != DEFAULT_DISPLAY) {
                        return;
                    }
                    updateDisplayLayout(displayId);
                }

                @Override
                public void onDisplayAdded(int displayId) {
                    if (displayId != DEFAULT_DISPLAY) {
                        return;
                    }
                    updateDisplayLayout(displayId);
                }
            };


    public static AppZoomOutController create(Context context, ShellInit shellInit,
            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
            DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) {
        AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer(
                context, displayLayout, mainExecutor);
        return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController,
                displayAreaOrganizer, mainExecutor);
    }

    @VisibleForTesting
    AppZoomOutController(Context context, ShellInit shellInit,
            ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController,
            AppZoomOutDisplayAreaOrganizer displayAreaOrganizer,
            @ShellMainThread ShellExecutor mainExecutor) {
        mContext = context;
        mTaskOrganizer = shellTaskOrganizer;
        mDisplayController = displayController;
        mDisplayAreaOrganizer = displayAreaOrganizer;
        mMainExecutor = mainExecutor;

        shellInit.addInitCallback(this::onInit, this);
    }

    private void onInit() {
        mTaskOrganizer.addFocusListener(this);

        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
        mDisplayController.addDisplayChangingController(this);

        mDisplayAreaOrganizer.registerOrganizer();
    }

    public AppZoomOut asAppZoomOut() {
        return mImpl;
    }

    public void setProgress(float progress) {
        mDisplayAreaOrganizer.setProgress(progress);
    }

    void updateDisplayLayout(int displayId) {
        final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId);
        if (newDisplayLayout == null) {
            Slog.w(TAG, "Failed to get new DisplayLayout.");
            return;
        }
        mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout);
    }

    @Override
    public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
        if (taskInfo == null) {
            return;
        }
        if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) {
            mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused);
        }
    }

    @Override
    public void onDisplayChange(int displayId, int fromRotation, int toRotation,
            @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
        // TODO: verify if there is synchronization issues.
        mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation);
    }

    @Override
    public Context getContext() {
        return mContext;
    }

    @Override
    public ShellExecutor getRemoteCallExecutor() {
        return mMainExecutor;
    }

    @ExternalThread
    private class AppZoomOutImpl implements AppZoomOut {
        @Override
        public void setProgress(float progress) {
            mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress));
        }
    }
}
+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.appzoomout;

import android.annotation.Nullable;
import android.content.Context;
import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
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.List;
import java.util.Map;
import java.util.concurrent.Executor;

/** Display area organizer that manages the app zoom out UI and states. */
public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer {

    private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f;
    private static final float PUSHBACK_SCALE_FOR_APP = 0.025f;
    private static final float INVALID_PROGRESS = -1;

    private final DisplayLayout mDisplayLayout = new DisplayLayout();
    private final Context mContext;
    private final float mCornerRadius;
    private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap =
            new ArrayMap<>();

    private float mProgress = INVALID_PROGRESS;
    // Denote whether the home task is focused, null when it's not yet initialized.
    @Nullable private Boolean mIsHomeTaskFocused;

    public AppZoomOutDisplayAreaOrganizer(Context context,
            DisplayLayout displayLayout, Executor mainExecutor) {
        super(mainExecutor);
        mContext = context;
        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
        setDisplayLayout(displayLayout);
    }

    @Override
    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) {
        leash.setUnreleasedWarningCallSite(
                "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared");
        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);
    }

    public void registerOrganizer() {
        final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer(
                AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT);
        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 setProgress(float progress) {
        if (mProgress == progress) {
            return;
        }

        mProgress = progress;
        apply();
    }

    void setIsHomeTaskFocused(boolean isHomeTaskFocused) {
        if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) {
            return;
        }

        mIsHomeTaskFocused = isHomeTaskFocused;
        apply();
    }

    private void apply() {
        if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) {
            return;
        }

        SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
        float scale = mProgress * (mIsHomeTaskFocused
                ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP);
        mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale));
        tx.apply();
    }

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

    private void reset() {
        setProgress(0);
        mProgress = INVALID_PROGRESS;
        mIsHomeTaskFocused = null;
    }

    private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) {
        if (scale == 0) {
            // Reset when scale is set back to 0.
            tx
                    .setCrop(leash, null)
                    .setScale(leash, 1, 1)
                    .setPosition(leash, 0, 0)
                    .setCornerRadius(leash, 0);
            return;
        }

        tx
                // Rounded corner can only be applied if a crop is set.
                .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height())
                .setScale(leash, 1 - scale, 1 - scale)
                .setPosition(leash, scale * mDisplayLayout.width() * 0.5f,
                        scale * mDisplayLayout.height() * 0.5f)
                .setCornerRadius(leash, mCornerRadius * (1 - scale));
    }

    void onRotateDisplay(Context context, int toRotation) {
        if (mDisplayLayout.rotation() == toRotation) {
            return;
        }
        mDisplayLayout.rotateTo(context.getResources(), toRotation);
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.os.HandlerThread;

import androidx.annotation.Nullable;

import com.android.wm.shell.appzoomout.AppZoomOut;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.desktopmode.DesktopMode;
@@ -112,4 +113,7 @@ public interface WMComponent {
     */
    @WMSingleton
    Optional<DesktopMode> getDesktopMode();

    @WMSingleton
    Optional<AppZoomOut> getAppZoomOut();
}
Loading