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