1 /*
2  * Copyright (C) 2021 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.startingsurface;
18 
19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
20 
21 import android.animation.AnimatorListenerAdapter;
22 import android.annotation.ColorInt;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.res.Resources;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.ColorFilter;
30 import android.graphics.Matrix;
31 import android.graphics.Paint;
32 import android.graphics.Path;
33 import android.graphics.PixelFormat;
34 import android.graphics.Rect;
35 import android.graphics.drawable.AdaptiveIconDrawable;
36 import android.graphics.drawable.Animatable;
37 import android.graphics.drawable.AnimatedVectorDrawable;
38 import android.graphics.drawable.AnimationDrawable;
39 import android.graphics.drawable.Drawable;
40 import android.os.Handler;
41 import android.os.Trace;
42 import android.util.Log;
43 import android.util.PathParser;
44 import android.window.SplashScreenView;
45 
46 import com.android.internal.R;
47 
48 import java.io.Closeable;
49 import java.util.function.LongConsumer;
50 
51 /**
52  * Creating a lightweight Drawable object used for splash screen.
53  *
54  * @hide
55  */
56 public class SplashscreenIconDrawableFactory {
57 
58     private static final String TAG = StartingWindowController.TAG;
59 
60     /**
61      * @return An array containing the foreground drawable at index 0 and if needed a background
62      * drawable at index 1.
63      */
makeIconDrawable(@olorInt int backgroundColor, @ColorInt int themeColor, @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize, boolean loadInDetail, Handler preDrawHandler)64     static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor,
65             @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize,
66             boolean loadInDetail, Handler preDrawHandler) {
67         Drawable foreground;
68         Drawable background = null;
69         boolean drawBackground =
70                 backgroundColor != Color.TRANSPARENT && backgroundColor != themeColor;
71 
72         if (foregroundDrawable instanceof Animatable) {
73             foreground = new AnimatableIconAnimateListener(foregroundDrawable);
74         } else if (foregroundDrawable instanceof AdaptiveIconDrawable) {
75             // If the icon is Adaptive, we already use the icon background.
76             drawBackground = false;
77             foreground = new ImmobileIconDrawable(foregroundDrawable,
78                     srcIconSize, iconSize, loadInDetail, preDrawHandler);
79         } else {
80             // Adaptive icon don't handle transparency so we draw the background of the adaptive
81             // icon with the same color as the window background color instead of using two layers
82             foreground = new ImmobileIconDrawable(
83                     new AdaptiveForegroundDrawable(foregroundDrawable),
84                     srcIconSize, iconSize, loadInDetail, preDrawHandler);
85         }
86 
87         if (drawBackground) {
88             background = new MaskBackgroundDrawable(backgroundColor);
89         }
90 
91         return new Drawable[]{foreground, background};
92     }
93 
makeLegacyIconDrawable(@onNull Drawable iconDrawable, int srcIconSize, int iconSize, boolean loadInDetail, Handler preDrawHandler)94     static Drawable[] makeLegacyIconDrawable(@NonNull Drawable iconDrawable, int srcIconSize,
95             int iconSize, boolean loadInDetail, Handler preDrawHandler) {
96         return new Drawable[]{new ImmobileIconDrawable(iconDrawable, srcIconSize, iconSize,
97                 loadInDetail, preDrawHandler)};
98     }
99 
100     /**
101      * Drawable pre-drawing the scaled icon in a separate thread to increase the speed of the
102      * final drawing.
103      */
104     private static class ImmobileIconDrawable extends Drawable implements Closeable {
105         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
106                 | Paint.FILTER_BITMAP_FLAG);
107         private final Matrix mMatrix = new Matrix();
108         private Bitmap mIconBitmap;
109 
ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail, Handler preDrawHandler)110         ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail,
111                 Handler preDrawHandler) {
112             // This icon has lower density, don't scale it.
113             if (loadInDetail) {
114                 preDrawHandler.post(() -> preDrawIcon(drawable, iconSize));
115             } else {
116                 final float scale = (float) iconSize / srcIconSize;
117                 mMatrix.setScale(scale, scale);
118                 preDrawHandler.post(() -> preDrawIcon(drawable, srcIconSize));
119             }
120         }
121 
preDrawIcon(Drawable drawable, int size)122         private void preDrawIcon(Drawable drawable, int size) {
123             synchronized (mPaint) {
124                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "preDrawIcon");
125                 mIconBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
126                 final Canvas canvas = new Canvas(mIconBitmap);
127                 drawable.setBounds(0, 0, size, size);
128                 drawable.draw(canvas);
129                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
130             }
131         }
132 
133         @Override
draw(Canvas canvas)134         public void draw(Canvas canvas) {
135             synchronized (mPaint) {
136                 if (mIconBitmap != null) {
137                     canvas.drawBitmap(mIconBitmap, mMatrix, mPaint);
138                 } else {
139                     // this shouldn't happen, but if it really happen, invalidate self to wait
140                     // for bitmap to be ready.
141                     invalidateSelf();
142                 }
143             }
144         }
145 
146         @Override
setAlpha(int alpha)147         public void setAlpha(int alpha) {
148         }
149 
150         @Override
setColorFilter(ColorFilter colorFilter)151         public void setColorFilter(ColorFilter colorFilter) {
152         }
153 
154         @Override
getOpacity()155         public int getOpacity() {
156             return 1;
157         }
158 
159         @Override
close()160         public void close() {
161             synchronized (mPaint) {
162                 if (mIconBitmap != null) {
163                     mIconBitmap.recycle();
164                     mIconBitmap = null;
165                 }
166             }
167         }
168     }
169 
170     /**
171      * Base class the draw a background clipped by the system mask.
172      */
173     public static class MaskBackgroundDrawable extends Drawable {
174         private static final float MASK_SIZE = AdaptiveIconDrawable.MASK_SIZE;
175         private static final float EXTRA_INSET_PERCENTAGE = 1 / 4f;
176         static final float DEFAULT_VIEW_PORT_SCALE = 1f / (1 + 2 * EXTRA_INSET_PERCENTAGE);
177         /**
178          * Clip path defined in R.string.config_icon_mask.
179          */
180         private static Path sMask;
181         private final Path mMaskScaleOnly;
182         private final Matrix mMaskMatrix;
183 
184         @Nullable
185         private final Paint mBackgroundPaint;
186 
MaskBackgroundDrawable(@olorInt int backgroundColor)187         public MaskBackgroundDrawable(@ColorInt int backgroundColor) {
188             final Resources r = Resources.getSystem();
189             sMask = PathParser.createPathFromPathData(r.getString(R.string.config_icon_mask));
190             Path mask = new Path(sMask);
191             mMaskScaleOnly = new Path(mask);
192             mMaskMatrix = new Matrix();
193             if (backgroundColor != Color.TRANSPARENT) {
194                 mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
195                         | Paint.FILTER_BITMAP_FLAG);
196                 mBackgroundPaint.setColor(backgroundColor);
197                 mBackgroundPaint.setStyle(Paint.Style.FILL);
198             } else {
199                 mBackgroundPaint = null;
200             }
201         }
202 
203         @Override
onBoundsChange(Rect bounds)204         protected void onBoundsChange(Rect bounds) {
205             if (bounds.isEmpty()) {
206                 return;
207             }
208             updateLayerBounds(bounds);
209         }
210 
updateLayerBounds(Rect bounds)211         protected void updateLayerBounds(Rect bounds) {
212             // reset everything that depends on the view bounds
213             mMaskMatrix.setScale(bounds.width() / MASK_SIZE, bounds.height() / MASK_SIZE);
214             sMask.transform(mMaskMatrix, mMaskScaleOnly);
215         }
216 
217         @Override
draw(Canvas canvas)218         public void draw(Canvas canvas) {
219             canvas.clipPath(mMaskScaleOnly);
220             if (mBackgroundPaint != null) {
221                 canvas.drawPath(mMaskScaleOnly, mBackgroundPaint);
222             }
223         }
224 
225         @Override
setAlpha(int alpha)226         public void setAlpha(int alpha) {
227             if (mBackgroundPaint != null) {
228                 mBackgroundPaint.setAlpha(alpha);
229             }
230         }
231 
232         @Override
getOpacity()233         public int getOpacity() {
234             return PixelFormat.RGBA_8888;
235         }
236 
237         @Override
setColorFilter(ColorFilter colorFilter)238         public void setColorFilter(ColorFilter colorFilter) {
239         }
240     }
241 
242     private static class AdaptiveForegroundDrawable extends MaskBackgroundDrawable {
243 
244         @NonNull
245         protected final Drawable mForegroundDrawable;
246         private final Rect mTmpOutRect = new Rect();
247 
AdaptiveForegroundDrawable(@onNull Drawable foregroundDrawable)248         AdaptiveForegroundDrawable(@NonNull Drawable foregroundDrawable) {
249             super(Color.TRANSPARENT);
250             mForegroundDrawable = foregroundDrawable;
251         }
252 
253         @Override
updateLayerBounds(Rect bounds)254         protected void updateLayerBounds(Rect bounds) {
255             super.updateLayerBounds(bounds);
256             int cX = bounds.width() / 2;
257             int cY = bounds.height() / 2;
258 
259             int insetWidth = (int) (bounds.width() / (DEFAULT_VIEW_PORT_SCALE * 2));
260             int insetHeight = (int) (bounds.height() / (DEFAULT_VIEW_PORT_SCALE * 2));
261             final Rect outRect = mTmpOutRect;
262             outRect.set(cX - insetWidth, cY - insetHeight, cX + insetWidth, cY + insetHeight);
263             if (mForegroundDrawable != null) {
264                 mForegroundDrawable.setBounds(outRect);
265             }
266             invalidateSelf();
267         }
268 
269         @Override
draw(Canvas canvas)270         public void draw(Canvas canvas) {
271             super.draw(canvas);
272             mForegroundDrawable.draw(canvas);
273         }
274 
275         @Override
setColorFilter(ColorFilter colorFilter)276         public void setColorFilter(ColorFilter colorFilter) {
277             mForegroundDrawable.setColorFilter(colorFilter);
278         }
279     }
280 
281     /**
282      * A lightweight AdaptiveIconDrawable which support foreground to be Animatable, and keep this
283      * drawable masked by config_icon_mask.
284      */
285     public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable
286             implements SplashScreenView.IconAnimateListener {
287         private final Animatable mAnimatableIcon;
288         private boolean mAnimationTriggered;
289         private AnimatorListenerAdapter mJankMonitoringListener;
290         private boolean mRunning;
291         private LongConsumer mStartListener;
292 
AnimatableIconAnimateListener(@onNull Drawable foregroundDrawable)293         AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) {
294             super(foregroundDrawable);
295             Callback callback = new Callback() {
296                 @Override
297                 public void invalidateDrawable(@NonNull Drawable who) {
298                     invalidateSelf();
299                 }
300 
301                 @Override
302                 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what,
303                         long when) {
304                     scheduleSelf(what, when);
305                 }
306 
307                 @Override
308                 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
309                     unscheduleSelf(what);
310                 }
311             };
312             mForegroundDrawable.setCallback(callback);
313             mAnimatableIcon = (Animatable) mForegroundDrawable;
314         }
315 
316         @Override
setAnimationJankMonitoring(AnimatorListenerAdapter listener)317         public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) {
318             mJankMonitoringListener = listener;
319         }
320 
321         @Override
prepareAnimate(LongConsumer startListener)322         public void prepareAnimate(LongConsumer startListener) {
323             stopAnimation();
324             mStartListener = startListener;
325         }
326 
startAnimation()327         private void startAnimation() {
328             if (mJankMonitoringListener != null) {
329                 mJankMonitoringListener.onAnimationStart(null);
330             }
331             try {
332                 mAnimatableIcon.start();
333             } catch (Exception ex) {
334                 Log.e(TAG, "Error while running the splash screen animated icon", ex);
335                 mRunning = false;
336                 if (mJankMonitoringListener != null) {
337                     mJankMonitoringListener.onAnimationCancel(null);
338                 }
339                 if (mStartListener != null) {
340                     mStartListener.accept(0);
341                 }
342                 return;
343             }
344             long animDuration = 0;
345             if (mAnimatableIcon instanceof AnimatedVectorDrawable
346                     && ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration() > 0) {
347                 animDuration = ((AnimatedVectorDrawable) mAnimatableIcon).getTotalDuration();
348             } else if (mAnimatableIcon instanceof AnimationDrawable
349                     && ((AnimationDrawable) mAnimatableIcon).getTotalDuration() > 0) {
350                 animDuration = ((AnimationDrawable) mAnimatableIcon).getTotalDuration();
351             }
352             mRunning = true;
353             if (mStartListener != null) {
354                 mStartListener.accept(animDuration);
355             }
356         }
357 
onAnimationEnd()358         private void onAnimationEnd() {
359             mAnimatableIcon.stop();
360             if (mJankMonitoringListener != null) {
361                 mJankMonitoringListener.onAnimationEnd(null);
362             }
363             mStartListener = null;
364             mRunning = false;
365         }
366 
367         @Override
stopAnimation()368         public void stopAnimation() {
369             if (mRunning) {
370                 onAnimationEnd();
371                 mJankMonitoringListener = null;
372             }
373         }
374 
ensureAnimationStarted()375         private void ensureAnimationStarted() {
376             if (mAnimationTriggered) {
377                 return;
378             }
379             if (!mRunning) {
380                 startAnimation();
381             }
382             mAnimationTriggered = true;
383         }
384 
385         @Override
draw(Canvas canvas)386         public void draw(Canvas canvas) {
387             ensureAnimationStarted();
388             super.draw(canvas);
389         }
390     }
391 }
392