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