1 /* 2 * Copyright (C) 2020 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.internal.graphics.drawable; 18 19 import android.annotation.ColorInt; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UiThread; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.ColorFilter; 27 import android.graphics.Paint; 28 import android.graphics.Path; 29 import android.graphics.PixelFormat; 30 import android.graphics.PorterDuff; 31 import android.graphics.PorterDuffXfermode; 32 import android.graphics.Rect; 33 import android.graphics.RenderNode; 34 import android.graphics.drawable.Drawable; 35 import android.util.ArraySet; 36 import android.util.Log; 37 import android.util.LongSparseArray; 38 import android.view.ViewRootImpl; 39 import android.view.ViewTreeObserver; 40 41 import com.android.internal.R; 42 import com.android.internal.annotations.GuardedBy; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 /** 46 * A drawable that keeps track of a blur region, pokes a hole under it, and propagates its state 47 * to SurfaceFlinger. 48 */ 49 public final class BackgroundBlurDrawable extends Drawable { 50 51 private static final String TAG = BackgroundBlurDrawable.class.getSimpleName(); 52 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 53 54 private final Aggregator mAggregator; 55 private final RenderNode mRenderNode; 56 private final Paint mPaint = new Paint(); 57 private final Path mRectPath = new Path(); 58 private final float[] mTmpRadii = new float[8]; 59 60 private boolean mVisible = true; 61 62 // Confined to UiThread. The values are copied into a BlurRegion, which lives on 63 // RenderThread to avoid interference with UiThread updates. 64 private int mBlurRadius; 65 private float mCornerRadiusTL; 66 private float mCornerRadiusTR; 67 private float mCornerRadiusBL; 68 private float mCornerRadiusBR; 69 private float mAlpha = 1; 70 71 // Do not update from UiThread. This holds the latest position for this drawable. It is used 72 // by the Aggregator from RenderThread to get the final position of the blur region sent to SF 73 private final Rect mRect = new Rect(); 74 // This is called from a thread pool. The callbacks might come out of order w.r.t. the frame 75 // number, so we send a Runnable holding the actual update to the Aggregator. The Aggregator 76 // can apply the update on RenderThread when processing that same frame. 77 @VisibleForTesting 78 public final RenderNode.PositionUpdateListener mPositionUpdateListener = 79 new RenderNode.PositionUpdateListener() { 80 @Override 81 public void positionChanged(long frameNumber, int left, int top, int right, 82 int bottom) { 83 mAggregator.onRenderNodePositionChanged(frameNumber, () -> { 84 mRect.set(left, top, right, bottom); 85 }); 86 } 87 88 @Override 89 public void positionLost(long frameNumber) { 90 mAggregator.onRenderNodePositionChanged(frameNumber, () -> { 91 mRect.setEmpty(); 92 }); 93 } 94 }; 95 BackgroundBlurDrawable(Aggregator aggregator)96 private BackgroundBlurDrawable(Aggregator aggregator) { 97 mAggregator = aggregator; 98 mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 99 mPaint.setColor(Color.TRANSPARENT); 100 mPaint.setAntiAlias(true); 101 mRenderNode = new RenderNode("BackgroundBlurDrawable"); 102 mRenderNode.addPositionUpdateListener(mPositionUpdateListener); 103 } 104 105 @Override draw(@onNull Canvas canvas)106 public void draw(@NonNull Canvas canvas) { 107 if (mRectPath.isEmpty() || !isVisible() || getAlpha() == 0) { 108 return; 109 } 110 111 canvas.drawPath(mRectPath, mPaint); 112 canvas.drawRenderNode(mRenderNode); 113 } 114 115 /** 116 * Color that will be alpha blended on top of the blur. 117 */ setColor(@olorInt int color)118 public void setColor(@ColorInt int color) { 119 mPaint.setColor(color); 120 } 121 122 @Override setVisible(boolean visible, boolean restart)123 public boolean setVisible(boolean visible, boolean restart) { 124 boolean changed = super.setVisible(visible, restart); 125 if (changed) { 126 mVisible = visible; 127 mAggregator.onBlurDrawableUpdated(this); 128 } 129 return changed; 130 } 131 132 @Override setAlpha(int alpha)133 public void setAlpha(int alpha) { 134 if (mAlpha != alpha / 255f) { 135 mAlpha = alpha / 255f; 136 invalidateSelf(); 137 mAggregator.onBlurDrawableUpdated(this); 138 } 139 } 140 141 /** 142 * Blur radius in pixels. 143 */ setBlurRadius(int blurRadius)144 public void setBlurRadius(int blurRadius) { 145 if (mBlurRadius != blurRadius) { 146 mBlurRadius = blurRadius; 147 invalidateSelf(); 148 mAggregator.onBlurDrawableUpdated(this); 149 } 150 } 151 152 /** 153 * Sets the corner radius, in degrees. 154 */ setCornerRadius(float cornerRadius)155 public void setCornerRadius(float cornerRadius) { 156 setCornerRadius(cornerRadius, cornerRadius, cornerRadius, cornerRadius); 157 } 158 159 /** 160 * Sets the corner radius in degrees. 161 * @param cornerRadiusTL top left radius. 162 * @param cornerRadiusTR top right radius. 163 * @param cornerRadiusBL bottom left radius. 164 * @param cornerRadiusBR bottom right radius. 165 */ setCornerRadius(float cornerRadiusTL, float cornerRadiusTR, float cornerRadiusBL, float cornerRadiusBR)166 public void setCornerRadius(float cornerRadiusTL, float cornerRadiusTR, float cornerRadiusBL, 167 float cornerRadiusBR) { 168 if (mCornerRadiusTL != cornerRadiusTL 169 || mCornerRadiusTR != cornerRadiusTR 170 || mCornerRadiusBL != cornerRadiusBL 171 || mCornerRadiusBR != cornerRadiusBR) { 172 mCornerRadiusTL = cornerRadiusTL; 173 mCornerRadiusTR = cornerRadiusTR; 174 mCornerRadiusBL = cornerRadiusBL; 175 mCornerRadiusBR = cornerRadiusBR; 176 updatePath(); 177 invalidateSelf(); 178 mAggregator.onBlurDrawableUpdated(this); 179 } 180 } 181 182 @Override setBounds(int left, int top, int right, int bottom)183 public void setBounds(int left, int top, int right, int bottom) { 184 super.setBounds(left, top, right, bottom); 185 mRenderNode.setPosition(left, top, right, bottom); 186 updatePath(); 187 } 188 updatePath()189 private void updatePath() { 190 mTmpRadii[0] = mTmpRadii[1] = mCornerRadiusTL; 191 mTmpRadii[2] = mTmpRadii[3] = mCornerRadiusTR; 192 mTmpRadii[4] = mTmpRadii[5] = mCornerRadiusBL; 193 mTmpRadii[6] = mTmpRadii[7] = mCornerRadiusBR; 194 mRectPath.reset(); 195 if (getAlpha() == 0 || !isVisible()) { 196 return; 197 } 198 Rect bounds = getBounds(); 199 mRectPath.addRoundRect(bounds.left, bounds.top, bounds.right, bounds.bottom, mTmpRadii, 200 Path.Direction.CW); 201 } 202 203 @Override setColorFilter(@ullable ColorFilter colorFilter)204 public void setColorFilter(@Nullable ColorFilter colorFilter) { 205 throw new IllegalArgumentException("not implemented"); 206 } 207 208 @Override getOpacity()209 public int getOpacity() { 210 return PixelFormat.TRANSLUCENT; 211 } 212 213 @Override toString()214 public String toString() { 215 return "BackgroundBlurDrawable{" 216 + "blurRadius=" + mBlurRadius 217 + ", corners={" + mCornerRadiusTL 218 + "," + mCornerRadiusTR 219 + "," + mCornerRadiusBL 220 + "," + mCornerRadiusBR 221 + "}, alpha=" + mAlpha 222 + ", visible=" + mVisible 223 + "}"; 224 } 225 226 /** 227 * Responsible for keeping track of all blur regions of a {@link ViewRootImpl} and posting a 228 * message when it's time to propagate them. 229 */ 230 public static final class Aggregator { 231 private final Object mRtLock = new Object(); 232 // Maintains a list of all *visible* blur drawables. Confined to UI thread 233 private final ArraySet<BackgroundBlurDrawable> mDrawables = new ArraySet(); 234 @GuardedBy("mRtLock") 235 private final LongSparseArray<ArraySet<Runnable>> mFrameRtUpdates = new LongSparseArray(); 236 private long mLastFrameNumber = 0; 237 private BlurRegion[] mLastFrameBlurRegions = null; 238 private final ViewRootImpl mViewRoot; 239 private BlurRegion[] mTmpBlurRegionsForFrame = new BlurRegion[0]; 240 private boolean mHasUiUpdates; 241 private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener; 242 Aggregator(ViewRootImpl viewRoot)243 public Aggregator(ViewRootImpl viewRoot) { 244 mViewRoot = viewRoot; 245 } 246 247 /** 248 * Creates a blur region with default radius. 249 */ createBackgroundBlurDrawable(Context context)250 public BackgroundBlurDrawable createBackgroundBlurDrawable(Context context) { 251 BackgroundBlurDrawable drawable = new BackgroundBlurDrawable(this); 252 drawable.setBlurRadius(context.getResources().getDimensionPixelSize( 253 R.dimen.default_background_blur_radius)); 254 return drawable; 255 } 256 257 /** 258 * Called when a BackgroundBlurDrawable has been updated 259 */ 260 @UiThread onBlurDrawableUpdated(BackgroundBlurDrawable drawable)261 void onBlurDrawableUpdated(BackgroundBlurDrawable drawable) { 262 final boolean shouldBeDrawn = 263 drawable.mAlpha != 0 && drawable.mBlurRadius > 0 && drawable.mVisible; 264 final boolean isDrawn = mDrawables.contains(drawable); 265 if (shouldBeDrawn) { 266 mHasUiUpdates = true; 267 if (!isDrawn) { 268 mDrawables.add(drawable); 269 if (DEBUG) { 270 Log.d(TAG, "Add " + drawable); 271 } 272 } else { 273 if (DEBUG) { 274 Log.d(TAG, "Update " + drawable); 275 } 276 } 277 } else if (!shouldBeDrawn && isDrawn) { 278 mHasUiUpdates = true; 279 mDrawables.remove(drawable); 280 if (DEBUG) { 281 Log.d(TAG, "Remove " + drawable); 282 } 283 } 284 285 if (mOnPreDrawListener == null && mViewRoot.getView() != null 286 && hasRegions()) { 287 registerPreDrawListener(); 288 } 289 } 290 registerPreDrawListener()291 private void registerPreDrawListener() { 292 mOnPreDrawListener = () -> { 293 final boolean hasUiUpdates = hasUpdates(); 294 295 if (hasUiUpdates || hasRegions()) { 296 final BlurRegion[] blurRegionsForNextFrame = getBlurRegionsCopyForRT(); 297 298 mViewRoot.registerRtFrameCallback(frame -> { 299 synchronized (mRtLock) { 300 mLastFrameNumber = frame; 301 mLastFrameBlurRegions = blurRegionsForNextFrame; 302 handleDispatchBlurTransactionLocked( 303 frame, blurRegionsForNextFrame, hasUiUpdates); 304 } 305 }); 306 } 307 if (!hasRegions() && mViewRoot.getView() != null) { 308 mViewRoot.getView().getViewTreeObserver() 309 .removeOnPreDrawListener(mOnPreDrawListener); 310 mOnPreDrawListener = null; 311 } 312 return true; 313 }; 314 315 mViewRoot.getView().getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener); 316 } 317 318 // Called from a thread pool onRenderNodePositionChanged(long frameNumber, Runnable update)319 void onRenderNodePositionChanged(long frameNumber, Runnable update) { 320 // One of the blur region's position has changed, so we have to send an updated list 321 // of blur regions to SurfaceFlinger for this frame. 322 synchronized (mRtLock) { 323 ArraySet<Runnable> frameRtUpdates = mFrameRtUpdates.get(frameNumber); 324 if (frameRtUpdates == null) { 325 frameRtUpdates = new ArraySet<>(); 326 mFrameRtUpdates.put(frameNumber, frameRtUpdates); 327 } 328 frameRtUpdates.add(update); 329 330 if (mLastFrameNumber == frameNumber) { 331 // The transaction for this frame has already been sent, so we have to manually 332 // trigger sending a transaction here in order to apply this position update 333 handleDispatchBlurTransactionLocked(frameNumber, mLastFrameBlurRegions, true); 334 } 335 } 336 337 } 338 339 /** 340 * @return true if there are any updates that need to be sent to SF 341 */ 342 @UiThread hasUpdates()343 public boolean hasUpdates() { 344 return mHasUiUpdates; 345 } 346 347 /** 348 * @return true if there are any visible blur regions 349 */ 350 @UiThread hasRegions()351 public boolean hasRegions() { 352 return mDrawables.size() > 0; 353 } 354 355 /** 356 * @return an array of BlurRegions, which are holding a copy of the information in 357 * all the currently visible BackgroundBlurDrawables 358 */ 359 @UiThread getBlurRegionsCopyForRT()360 public BlurRegion[] getBlurRegionsCopyForRT() { 361 if (mHasUiUpdates) { 362 mTmpBlurRegionsForFrame = new BlurRegion[mDrawables.size()]; 363 for (int i = 0; i < mDrawables.size(); i++) { 364 mTmpBlurRegionsForFrame[i] = new BlurRegion(mDrawables.valueAt(i)); 365 } 366 mHasUiUpdates = false; 367 } 368 369 return mTmpBlurRegionsForFrame; 370 } 371 372 /** 373 * Called on RenderThread. 374 * 375 * @return true if it is necessary to send an update to Sf this frame 376 */ 377 @GuardedBy("mRtLock") 378 @VisibleForTesting getBlurRegionsForFrameLocked(long frameNumber, BlurRegion[] blurRegionsForFrame, boolean forceUpdate)379 public float[][] getBlurRegionsForFrameLocked(long frameNumber, 380 BlurRegion[] blurRegionsForFrame, boolean forceUpdate) { 381 if (!forceUpdate && (mFrameRtUpdates.size() == 0 382 || mFrameRtUpdates.keyAt(0) > frameNumber)) { 383 return null; 384 } 385 386 // mFrameRtUpdates holds position updates coming from a thread pool span from 387 // RenderThread. At this point, all position updates for frame frameNumber should 388 // have been added to mFrameRtUpdates. 389 // Here, we apply all updates for frames <= frameNumber in case some previous update 390 // has been missed. This also protects mFrameRtUpdates from memory leaks. 391 while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) { 392 final ArraySet<Runnable> frameUpdates = mFrameRtUpdates.valueAt(0); 393 mFrameRtUpdates.removeAt(0); 394 for (int i = 0; i < frameUpdates.size(); i++) { 395 frameUpdates.valueAt(i).run(); 396 } 397 } 398 399 if (DEBUG) { 400 Log.d(TAG, "Dispatching " + blurRegionsForFrame.length + " blur regions:"); 401 } 402 403 final float[][] blurRegionsArray = new float[blurRegionsForFrame.length][]; 404 for (int i = 0; i < blurRegionsArray.length; i++) { 405 blurRegionsArray[i] = blurRegionsForFrame[i].toFloatArray(); 406 if (DEBUG) { 407 Log.d(TAG, blurRegionsForFrame[i].toString()); 408 } 409 } 410 return blurRegionsArray; 411 } 412 413 /** 414 * Dispatch all blur regions if there are any ui or position updates for that frame. 415 */ 416 @GuardedBy("mRtLock") handleDispatchBlurTransactionLocked(long frameNumber, BlurRegion[] blurRegions, boolean forceUpdate)417 private void handleDispatchBlurTransactionLocked(long frameNumber, BlurRegion[] blurRegions, 418 boolean forceUpdate) { 419 float[][] blurRegionsArray = 420 getBlurRegionsForFrameLocked(frameNumber, blurRegions, forceUpdate); 421 if (blurRegionsArray != null) { 422 mViewRoot.dispatchBlurRegions(blurRegionsArray, frameNumber); 423 } 424 } 425 426 } 427 428 /** 429 * Wrapper for sending blur data to SurfaceFlinger 430 * Confined to RenderThread. 431 */ 432 public static final class BlurRegion { 433 public final int blurRadius; 434 public final float cornerRadiusTL; 435 public final float cornerRadiusTR; 436 public final float cornerRadiusBL; 437 public final float cornerRadiusBR; 438 public final float alpha; 439 public final Rect rect; 440 BlurRegion(BackgroundBlurDrawable drawable)441 BlurRegion(BackgroundBlurDrawable drawable) { 442 alpha = drawable.mAlpha; 443 blurRadius = drawable.mBlurRadius; 444 cornerRadiusTL = drawable.mCornerRadiusTL; 445 cornerRadiusTR = drawable.mCornerRadiusTR; 446 cornerRadiusBL = drawable.mCornerRadiusBL; 447 cornerRadiusBR = drawable.mCornerRadiusBR; 448 rect = drawable.mRect; 449 } 450 451 /** 452 * Serializes this class into a float array that's more JNI friendly. 453 */ toFloatArray()454 float[] toFloatArray() { 455 final float[] floatArray = new float[10]; 456 floatArray[0] = blurRadius; 457 floatArray[1] = alpha; 458 floatArray[2] = rect.left; 459 floatArray[3] = rect.top; 460 floatArray[4] = rect.right; 461 floatArray[5] = rect.bottom; 462 floatArray[6] = cornerRadiusTL; 463 floatArray[7] = cornerRadiusTR; 464 floatArray[8] = cornerRadiusBL; 465 floatArray[9] = cornerRadiusBR; 466 return floatArray; 467 } 468 469 @Override toString()470 public String toString() { 471 return "BlurRegion{" 472 + "blurRadius=" + blurRadius 473 + ", corners={" + cornerRadiusTL 474 + "," + cornerRadiusTR 475 + "," + cornerRadiusBL 476 + "," + cornerRadiusBR 477 + "}, alpha=" + alpha 478 + ", rect=" + rect 479 + "}"; 480 } 481 } 482 } 483