1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.appzoomout; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.util.ArrayMap; 22 import android.view.SurfaceControl; 23 import android.window.DisplayAreaAppearedInfo; 24 import android.window.DisplayAreaInfo; 25 import android.window.DisplayAreaOrganizer; 26 import android.window.WindowContainerToken; 27 28 import com.android.internal.policy.ScreenDecorationsUtils; 29 import com.android.wm.shell.common.DisplayLayout; 30 31 import java.util.List; 32 import java.util.Map; 33 import java.util.concurrent.Executor; 34 35 /** Display area organizer that manages the app zoom out UI and states. */ 36 public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer { 37 38 private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f; 39 private static final float PUSHBACK_SCALE_FOR_APP = 0.025f; 40 private static final float INVALID_PROGRESS = -1; 41 42 private final DisplayLayout mDisplayLayout = new DisplayLayout(); 43 private final Context mContext; 44 private final float mCornerRadius; 45 private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = 46 new ArrayMap<>(); 47 48 private float mProgress = INVALID_PROGRESS; 49 // Denote whether the home task is focused, null when it's not yet initialized. 50 @Nullable private Boolean mIsHomeTaskFocused; 51 AppZoomOutDisplayAreaOrganizer(Context context, DisplayLayout displayLayout, Executor mainExecutor)52 public AppZoomOutDisplayAreaOrganizer(Context context, 53 DisplayLayout displayLayout, Executor mainExecutor) { 54 super(mainExecutor); 55 mContext = context; 56 mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); 57 setDisplayLayout(displayLayout); 58 } 59 60 @Override onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash)61 public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) { 62 leash.setUnreleasedWarningCallSite( 63 "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared"); 64 mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); 65 } 66 67 @Override onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo)68 public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { 69 final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); 70 if (leash != null) { 71 leash.release(); 72 } 73 mDisplayAreaTokenMap.remove(displayAreaInfo.token); 74 } 75 registerOrganizer()76 public void registerOrganizer() { 77 final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer( 78 AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT); 79 for (int i = 0; i < displayAreaInfos.size(); i++) { 80 final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); 81 onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); 82 } 83 } 84 85 @Override unregisterOrganizer()86 public void unregisterOrganizer() { 87 super.unregisterOrganizer(); 88 reset(); 89 } 90 setProgress(float progress)91 void setProgress(float progress) { 92 if (mProgress == progress) { 93 return; 94 } 95 96 mProgress = progress; 97 apply(); 98 } 99 setIsHomeTaskFocused(boolean isHomeTaskFocused)100 void setIsHomeTaskFocused(boolean isHomeTaskFocused) { 101 if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) { 102 return; 103 } 104 105 mIsHomeTaskFocused = isHomeTaskFocused; 106 apply(); 107 } 108 apply()109 private void apply() { 110 if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) { 111 return; 112 } 113 114 SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); 115 float scale = mProgress * (mIsHomeTaskFocused 116 ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP); 117 mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale)); 118 tx.apply(); 119 } 120 setDisplayLayout(DisplayLayout displayLayout)121 void setDisplayLayout(DisplayLayout displayLayout) { 122 mDisplayLayout.set(displayLayout); 123 } 124 reset()125 private void reset() { 126 setProgress(0); 127 mProgress = INVALID_PROGRESS; 128 mIsHomeTaskFocused = null; 129 } 130 updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale)131 private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) { 132 if (scale == 0) { 133 // Reset when scale is set back to 0. 134 tx 135 .setCrop(leash, null) 136 .setScale(leash, 1, 1) 137 .setPosition(leash, 0, 0) 138 .setCornerRadius(leash, 0); 139 return; 140 } 141 142 tx 143 // Rounded corner can only be applied if a crop is set. 144 .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height()) 145 .setScale(leash, 1 - scale, 1 - scale) 146 .setPosition(leash, scale * mDisplayLayout.width() * 0.5f, 147 scale * mDisplayLayout.height() * 0.5f) 148 .setCornerRadius(leash, mCornerRadius * (1 - scale)); 149 } 150 onRotateDisplay(Context context, int toRotation)151 void onRotateDisplay(Context context, int toRotation) { 152 if (mDisplayLayout.rotation() == toRotation) { 153 return; 154 } 155 mDisplayLayout.rotateTo(context.getResources(), toRotation); 156 } 157 } 158