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