1 /* 2 * Copyright (C) 2024 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.shared.pip; 18 19 import static android.util.TypedValue.COMPLEX_UNIT_DIP; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.graphics.Bitmap; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Matrix; 29 import android.graphics.Rect; 30 import android.graphics.drawable.Drawable; 31 import android.hardware.HardwareBuffer; 32 import android.util.TypedValue; 33 import android.view.SurfaceControl; 34 import android.window.TaskSnapshot; 35 36 /** 37 * Represents the content overlay used during the entering PiP animation. 38 */ 39 public abstract class PipContentOverlay { 40 // Fixed string used in WMShellFlickerTests 41 protected static final String LAYER_NAME = "PipContentOverlay"; 42 43 protected SurfaceControl mLeash; 44 45 /** Attaches the internal {@link #mLeash} to the given parent leash. */ attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash)46 public abstract void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash); 47 48 /** Detaches the internal {@link #mLeash} from its parent by removing itself. */ detach(SurfaceControl.Transaction tx)49 public void detach(SurfaceControl.Transaction tx) { 50 if (mLeash != null && mLeash.isValid()) { 51 tx.remove(mLeash); 52 tx.apply(); 53 } 54 } 55 56 @Nullable getLeash()57 public SurfaceControl getLeash() { 58 return mLeash; 59 } 60 61 /** 62 * Animates the internal {@link #mLeash} by a given fraction. 63 * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly 64 * call apply on this transaction, it should be applied on the caller side. 65 * @param currentBounds {@link Rect} of the current animation bounds. 66 * @param fraction progress of the animation ranged from 0f to 1f. 67 */ onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction)68 public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 69 Rect currentBounds, float fraction) {} 70 71 /** 72 * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition. 73 * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly 74 * call apply on this transaction, it should be applied on the caller side. 75 * @param scale scaling to apply onto the overlay. 76 * @param fraction progress of the animation ranged from 0f to 1f. 77 * @param endBounds the final bounds PiP is animating into. 78 */ onAnimationUpdate(SurfaceControl.Transaction atomicTx, float scale, float fraction, Rect endBounds)79 public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 80 float scale, float fraction, Rect endBounds) {} 81 82 /** A {@link PipContentOverlay} uses solid color. */ 83 public static final class PipColorOverlay extends PipContentOverlay { 84 private static final String TAG = PipColorOverlay.class.getSimpleName(); 85 86 private final Context mContext; 87 PipColorOverlay(Context context)88 public PipColorOverlay(Context context) { 89 mContext = context; 90 mLeash = new SurfaceControl.Builder() 91 .setCallsite(TAG) 92 .setName(LAYER_NAME) 93 .setColorLayer() 94 .build(); 95 } 96 97 @Override attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash)98 public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { 99 tx.show(mLeash); 100 tx.setLayer(mLeash, Integer.MAX_VALUE); 101 tx.setColor(mLeash, getContentOverlayColor(mContext)); 102 tx.setAlpha(mLeash, 0f); 103 tx.reparent(mLeash, parentLeash); 104 tx.apply(); 105 } 106 107 @Override onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction)108 public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 109 Rect currentBounds, float fraction) { 110 atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); 111 } 112 getContentOverlayColor(Context context)113 private float[] getContentOverlayColor(Context context) { 114 final TypedArray ta = context.obtainStyledAttributes(new int[] { 115 android.R.attr.colorBackground }); 116 try { 117 int colorAccent = ta.getColor(0, 0); 118 return new float[] { 119 Color.red(colorAccent) / 255f, 120 Color.green(colorAccent) / 255f, 121 Color.blue(colorAccent) / 255f }; 122 } finally { 123 ta.recycle(); 124 } 125 } 126 } 127 128 /** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */ 129 public static final class PipSnapshotOverlay extends PipContentOverlay { 130 private static final String TAG = PipSnapshotOverlay.class.getSimpleName(); 131 132 private final TaskSnapshot mSnapshot; 133 private final Rect mSourceRectHint; 134 PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint)135 public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) { 136 mSnapshot = snapshot; 137 mSourceRectHint = new Rect(sourceRectHint); 138 mLeash = new SurfaceControl.Builder() 139 .setCallsite(TAG) 140 .setName(LAYER_NAME) 141 .build(); 142 } 143 144 @Override attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash)145 public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { 146 final float taskSnapshotScaleX = (float) mSnapshot.getTaskSize().x 147 / mSnapshot.getHardwareBuffer().getWidth(); 148 final float taskSnapshotScaleY = (float) mSnapshot.getTaskSize().y 149 / mSnapshot.getHardwareBuffer().getHeight(); 150 tx.show(mLeash); 151 tx.setLayer(mLeash, Integer.MAX_VALUE); 152 tx.setBuffer(mLeash, mSnapshot.getHardwareBuffer()); 153 // Relocate the content to parentLeash's coordinates. 154 tx.setPosition(mLeash, -mSourceRectHint.left, -mSourceRectHint.top); 155 tx.setScale(mLeash, taskSnapshotScaleX, taskSnapshotScaleY); 156 tx.reparent(mLeash, parentLeash); 157 tx.apply(); 158 } 159 160 @Override onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction)161 public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 162 Rect currentBounds, float fraction) { 163 // Do nothing. Keep the snapshot till animation ends. 164 } 165 } 166 167 /** A {@link PipContentOverlay} shows app icon on solid color background. */ 168 public static final class PipAppIconOverlay extends PipContentOverlay { 169 private static final String TAG = PipAppIconOverlay.class.getSimpleName(); 170 // The maximum size for app icon in pixel. 171 private static final int MAX_APP_ICON_SIZE_DP = 72; 172 173 private final Context mContext; 174 private final int mAppIconSizePx; 175 /** 176 * The bounds of the application window relative to the task leash. 177 */ 178 private final Rect mRelativeAppBounds; 179 private final int mOverlayHalfSize; 180 private final Matrix mTmpTransform = new Matrix(); 181 private final float[] mTmpFloat9 = new float[9]; 182 183 private Bitmap mBitmap; 184 185 // TODO(b/356277166): add non-match_parent support on PIP2. 186 /** 187 * @param context the {@link Context} that contains the icon information 188 * @param relativeAppBounds the bounds of the app window frame relative to the task leash 189 * @param destinationBounds the bounds for rhe PIP task 190 * @param appIcon the app icon {@link Drawable} 191 * @param appIconSizePx the icon dimension in pixel 192 */ PipAppIconOverlay(@onNull Context context, @NonNull Rect relativeAppBounds, @NonNull Rect destinationBounds, @NonNull Drawable appIcon, int appIconSizePx)193 public PipAppIconOverlay(@NonNull Context context, @NonNull Rect relativeAppBounds, 194 @NonNull Rect destinationBounds, @NonNull Drawable appIcon, int appIconSizePx) { 195 mContext = context; 196 final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, 197 MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); 198 mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); 199 200 final int overlaySize = getOverlaySize(relativeAppBounds, destinationBounds); 201 mOverlayHalfSize = overlaySize >> 1; 202 mRelativeAppBounds = relativeAppBounds; 203 204 mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); 205 prepareAppIconOverlay(appIcon); 206 mLeash = new SurfaceControl.Builder() 207 .setCallsite(TAG) 208 .setName(LAYER_NAME) 209 .build(); 210 } 211 212 /** 213 * Returns the size of the app icon overlay. 214 * 215 * In order to have the overlay always cover the pip window during the transition, 216 * the overlay will be drawn with the max size of the start and end bounds in different 217 * rotation. 218 */ getOverlaySize(Rect overlayBounds, Rect destinationBounds)219 public static int getOverlaySize(Rect overlayBounds, Rect destinationBounds) { 220 final int appWidth = overlayBounds.width(); 221 final int appHeight = overlayBounds.height(); 222 223 return Math.max(Math.max(appWidth, appHeight), 224 Math.max(destinationBounds.width(), destinationBounds.height())) + 1; 225 } 226 227 @Override attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash)228 public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { 229 final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); 230 tx.show(mLeash); 231 tx.setLayer(mLeash, Integer.MAX_VALUE); 232 tx.setBuffer(mLeash, buffer); 233 tx.setAlpha(mLeash, 0f); 234 tx.reparent(mLeash, parentLeash); 235 tx.apply(); 236 // Cleanup the bitmap and buffer after setting up the leash 237 mBitmap.recycle(); 238 mBitmap = null; 239 buffer.close(); 240 } 241 242 @Override onAnimationUpdate(SurfaceControl.Transaction atomicTx, Rect currentBounds, float fraction)243 public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, 244 Rect currentBounds, float fraction) { 245 mTmpTransform.reset(); 246 // In order for the overlay to always cover the pip window, the overlay may have a 247 // size larger than the pip window. Make sure that app icon is at the center. 248 final int appBoundsCenterX = mRelativeAppBounds.centerX(); 249 final int appBoundsCenterY = mRelativeAppBounds.centerY(); 250 mTmpTransform.setTranslate( 251 appBoundsCenterX - mOverlayHalfSize, 252 appBoundsCenterY - mOverlayHalfSize); 253 // Scale back the bitmap with the pivot point at center. 254 final float scale = Math.min( 255 (float) mRelativeAppBounds.width() / currentBounds.width(), 256 (float) mRelativeAppBounds.height() / currentBounds.height()); 257 mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY); 258 atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) 259 .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); 260 } 261 prepareAppIconOverlay(Drawable appIcon)262 private void prepareAppIconOverlay(Drawable appIcon) { 263 final Canvas canvas = new Canvas(); 264 canvas.setBitmap(mBitmap); 265 final TypedArray ta = mContext.obtainStyledAttributes(new int[] { 266 android.R.attr.colorBackground }); 267 try { 268 int colorAccent = ta.getColor(0, 0); 269 canvas.drawRGB( 270 Color.red(colorAccent), 271 Color.green(colorAccent), 272 Color.blue(colorAccent)); 273 } finally { 274 ta.recycle(); 275 } 276 final Rect appIconBounds = new Rect( 277 mOverlayHalfSize - mAppIconSizePx / 2, 278 mOverlayHalfSize - mAppIconSizePx / 2, 279 mOverlayHalfSize + mAppIconSizePx / 2, 280 mOverlayHalfSize + mAppIconSizePx / 2); 281 appIcon.setBounds(appIconBounds); 282 appIcon.draw(canvas); 283 Bitmap oldBitmap = mBitmap; 284 mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); 285 oldBitmap.recycle(); 286 } 287 } 288 } 289