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