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