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

Commit 3e87c990 authored by Tracy Zhou's avatar Tracy Zhou Committed by Android (Google) Code Review
Browse files

Merge "Introduce new shell module for app zoom out (display area approach)" into main

parents 23c308ad 635439ac
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