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