From d129f1e00ef7de136908dce9e74fd7e23adcaf22 Mon Sep 17 00:00:00 2001 From: Galia Peycheva Date: Sat, 6 Feb 2021 13:00:14 +0100 Subject: [PATCH] Fix synchronisation in BackgroundBlurDrawable The PositionUpdateListener in BackgroundBlurDrawable is called from a thread pool to update the position of the BlurRegion. After that, in frame drawing callback (RenderThread), the Aggregator takes all the settings in BackgroundBlurDrawable and dispatches them to SF. However, while the RenderThread is working on frame N, the Ui thread is working on frame N+1. So when the frame drawing callback is called on RenderThread for frame N, BackgroundBlurDrawable might already be holding settings for frame N+1. In this CL, we fix that by making BackgroundBlurDrawable work only on Ui thread, while BlurRegion lives only on RenderThread. Just before drawing, we copy the state of BackgroundBlurDrawable into a BlurRegion. So when render thread starts doing its thing for frame N, Ui thread can start setting up BackgroundBlurDrawable for frame N+1. The settings for frame N are already copied over to BlurRegion. In frame drawing callback, we take the blur regions if there is a pending update and send them to SF. Bug: 167166562 Test: m && atest BlurAggregatorTest Change-Id: Icec0fa8e97de30b7bad15b97f848ae642076652f --- core/java/android/view/SurfaceControl.java | 60 ---- core/java/android/view/ViewRootImpl.java | 21 +- .../drawable/BackgroundBlurDrawable.java | 321 ++++++++++++++---- .../src/android/view/BlurAggregatorTest.java | 318 +++++++++++++++++ 4 files changed, 578 insertions(+), 142 deletions(-) create mode 100644 core/tests/coretests/src/android/view/BlurAggregatorTest.java diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 0832578d80c59..03dd100507240 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -3504,64 +3504,4 @@ public final class SurfaceControl implements Parcelable { public static Transaction getGlobalTransaction() { return sGlobalTransaction; } - - /** - * Wrapper for sending blur data to SurfaceFlinger. - * @hide - */ - public static final class BlurRegion { - public int blurRadius; - public float cornerRadiusTL; - public float cornerRadiusTR; - public float cornerRadiusBL; - public float cornerRadiusBR; - public float alpha = 1; - public boolean visible = true; - public final Rect rect = new Rect(); - - private final float[] mFloatArray = new float[10]; - - public BlurRegion() { - } - - public BlurRegion(BlurRegion other) { - rect.set(other.rect); - blurRadius = other.blurRadius; - alpha = other.alpha; - cornerRadiusTL = other.cornerRadiusTL; - cornerRadiusTR = other.cornerRadiusTR; - cornerRadiusBL = other.cornerRadiusBL; - cornerRadiusBR = other.cornerRadiusBR; - } - - /** - * Serializes this class into a float array that's more JNI friendly. - */ - public float[] toFloatArray() { - mFloatArray[0] = blurRadius; - mFloatArray[1] = alpha; - mFloatArray[2] = rect.left; - mFloatArray[3] = rect.top; - mFloatArray[4] = rect.right; - mFloatArray[5] = rect.bottom; - mFloatArray[6] = cornerRadiusTL; - mFloatArray[7] = cornerRadiusTR; - mFloatArray[8] = cornerRadiusBL; - mFloatArray[9] = cornerRadiusBR; - return mFloatArray; - } - - @Override - public String toString() { - return "BlurRegion{" - + "blurRadius=" + blurRadius - + ", corners={" + cornerRadiusTL - + "," + cornerRadiusTR - + "," + cornerRadiusBL - + "," + cornerRadiusBR - + "}, alpha=" + alpha - + ", rect=" + rect - + "}"; - } - } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 144691d3eaa0e..b3b547f50b40c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3962,11 +3962,12 @@ public final class ViewRootImpl implements ViewParent, } private void addFrameCallbackIfNeeded() { - boolean nextDrawUseBlastSync = mNextDrawUseBlastSync; - boolean hasBlur = mBlurRegionAggregator.hasRegions(); - boolean reportNextDraw = mReportNextDraw; + final boolean nextDrawUseBlastSync = mNextDrawUseBlastSync; + final boolean reportNextDraw = mReportNextDraw; + final boolean hasBlurUpdates = mBlurRegionAggregator.hasUpdates(); + final boolean needsCallbackForBlur = hasBlurUpdates || mBlurRegionAggregator.hasRegions(); - if (!nextDrawUseBlastSync && !reportNextDraw && !hasBlur) { + if (!nextDrawUseBlastSync && !reportNextDraw && !needsCallbackForBlur) { return; } @@ -3974,18 +3975,22 @@ public final class ViewRootImpl implements ViewParent, Log.d(mTag, "Creating frameDrawingCallback" + " nextDrawUseBlastSync=" + nextDrawUseBlastSync + " reportNextDraw=" + reportNextDraw - + " hasBlur=" + hasBlur); + + " hasBlurUpdates=" + hasBlurUpdates); } - // The callback will run on a worker thread pool from the render thread. + final BackgroundBlurDrawable.BlurRegion[] blurRegionsForFrame = + needsCallbackForBlur ? mBlurRegionAggregator.getBlurRegionsCopyForRT() : null; + + // The callback will run on the render thread. HardwareRenderer.FrameDrawingCallback frameDrawingCallback = frame -> { if (DEBUG_BLAST) { Log.d(mTag, "Received frameDrawingCallback frameNum=" + frame + "." + " Creating transactionCompleteCallback=" + nextDrawUseBlastSync); } - if (hasBlur) { - mBlurRegionAggregator.dispatchBlurTransactionIfNeeded(frame); + if (needsCallbackForBlur) { + mBlurRegionAggregator + .dispatchBlurTransactionIfNeeded(frame, blurRegionsForFrame, hasBlurUpdates); } if (mBlastBufferQueue == null) { diff --git a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java index 96dac565eb3dc..402d7fed90c5a 100644 --- a/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java +++ b/core/java/com/android/internal/graphics/drawable/BackgroundBlurDrawable.java @@ -19,6 +19,7 @@ package com.android.internal.graphics.drawable; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UiThread; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -31,12 +32,14 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; -import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Log; -import android.view.SurfaceControl; +import android.util.LongSparseArray; import android.view.ViewRootImpl; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; /** * A drawable that keeps track of a blur region, pokes a hole under it, and propagates its state @@ -52,26 +55,40 @@ public final class BackgroundBlurDrawable extends Drawable { private final Paint mPaint = new Paint(); private final Path mRectPath = new Path(); private final float[] mTmpRadii = new float[8]; - private final SurfaceControl.BlurRegion mBlurRegion = new SurfaceControl.BlurRegion(); - // This will be called from a thread pool. - private final RenderNode.PositionUpdateListener mPositionUpdateListener = + private boolean mVisible = true; + + // Confined to UiThread. The values are copied into a BlurRegion, which lives on + // RenderThread to avoid interference with UiThread updates. + private int mBlurRadius; + private float mCornerRadiusTL; + private float mCornerRadiusTR; + private float mCornerRadiusBL; + private float mCornerRadiusBR; + private float mAlpha = 1; + + // Do not update from UiThread. This holds the latest position for this drawable. It is used + // by the Aggregator from RenderThread to get the final position of the blur region sent to SF + private final Rect mRect = new Rect(); + // This is called from a thread pool. The callbacks might come out of order w.r.t. the frame + // number, so we send a Runnable holding the actual update to the Aggregator. The Aggregator + // can apply the update on RenderThread when processing that same frame. + @VisibleForTesting + public final RenderNode.PositionUpdateListener mPositionUpdateListener = new RenderNode.PositionUpdateListener() { @Override public void positionChanged(long frameNumber, int left, int top, int right, int bottom) { - synchronized (mAggregator) { - mBlurRegion.rect.set(left, top, right, bottom); - mAggregator.onBlurRegionUpdated(BackgroundBlurDrawable.this, mBlurRegion); - } + mAggregator.onRenderNodePositionChanged(frameNumber, () -> { + mRect.set(left, top, right, bottom); + }); } @Override public void positionLost(long frameNumber) { - synchronized (mAggregator) { - mBlurRegion.rect.setEmpty(); - mAggregator.onBlurRegionUpdated(BackgroundBlurDrawable.this, mBlurRegion); - } + mAggregator.onRenderNodePositionChanged(frameNumber, () -> { + mRect.setEmpty(); + }); } }; @@ -79,6 +96,7 @@ public final class BackgroundBlurDrawable extends Drawable { mAggregator = aggregator; mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); mPaint.setColor(Color.TRANSPARENT); + mPaint.setAntiAlias(true); mRenderNode = new RenderNode("BackgroundBlurDrawable"); mRenderNode.addPositionUpdateListener(mPositionUpdateListener); } @@ -104,23 +122,30 @@ public final class BackgroundBlurDrawable extends Drawable { public boolean setVisible(boolean visible, boolean restart) { boolean changed = super.setVisible(visible, restart); if (changed) { - mBlurRegion.visible = visible; + mVisible = visible; + mAggregator.onBlurDrawableUpdated(this); } return changed; } @Override public void setAlpha(int alpha) { - mBlurRegion.alpha = alpha / 255f; - invalidateSelf(); + if (mAlpha != alpha / 255f) { + mAlpha = alpha / 255f; + invalidateSelf(); + mAggregator.onBlurDrawableUpdated(this); + } } /** * Blur radius in pixels. */ public void setBlurRadius(int blurRadius) { - mBlurRegion.blurRadius = blurRadius; - invalidateSelf(); + if (mBlurRadius != blurRadius) { + mBlurRadius = blurRadius; + invalidateSelf(); + mAggregator.onBlurDrawableUpdated(this); + } } /** @@ -139,14 +164,18 @@ public final class BackgroundBlurDrawable extends Drawable { */ public void setCornerRadius(float cornerRadiusTL, float cornerRadiusTR, float cornerRadiusBL, float cornerRadiusBR) { - synchronized (mAggregator) { - mBlurRegion.cornerRadiusTL = cornerRadiusTL; - mBlurRegion.cornerRadiusTR = cornerRadiusTR; - mBlurRegion.cornerRadiusBL = cornerRadiusBL; - mBlurRegion.cornerRadiusBR = cornerRadiusBR; + if (mCornerRadiusTL != cornerRadiusTL + || mCornerRadiusTR != cornerRadiusTR + || mCornerRadiusBL != cornerRadiusBL + || mCornerRadiusBR != cornerRadiusBR) { + mCornerRadiusTL = cornerRadiusTL; + mCornerRadiusTR = cornerRadiusTR; + mCornerRadiusBL = cornerRadiusBL; + mCornerRadiusBR = cornerRadiusBR; + updatePath(); + invalidateSelf(); + mAggregator.onBlurDrawableUpdated(this); } - updatePath(); - invalidateSelf(); } @Override @@ -157,12 +186,10 @@ public final class BackgroundBlurDrawable extends Drawable { } private void updatePath() { - synchronized (mAggregator) { - mTmpRadii[0] = mTmpRadii[1] = mBlurRegion.cornerRadiusTL; - mTmpRadii[2] = mTmpRadii[3] = mBlurRegion.cornerRadiusTR; - mTmpRadii[4] = mTmpRadii[5] = mBlurRegion.cornerRadiusBL; - mTmpRadii[6] = mTmpRadii[7] = mBlurRegion.cornerRadiusBR; - } + mTmpRadii[0] = mTmpRadii[1] = mCornerRadiusTL; + mTmpRadii[2] = mTmpRadii[3] = mCornerRadiusTR; + mTmpRadii[4] = mTmpRadii[5] = mCornerRadiusBL; + mTmpRadii[6] = mTmpRadii[7] = mCornerRadiusBR; mRectPath.reset(); if (getAlpha() == 0 || !isVisible()) { return; @@ -182,17 +209,32 @@ public final class BackgroundBlurDrawable extends Drawable { return PixelFormat.TRANSLUCENT; } + @Override + public String toString() { + return "BackgroundBlurDrawable{" + + "blurRadius=" + mBlurRadius + + ", corners={" + mCornerRadiusTL + + "," + mCornerRadiusTR + + "," + mCornerRadiusBL + + "," + mCornerRadiusBR + + "}, alpha=" + mAlpha + + ", visible=" + mVisible + + "}"; + } + /** * Responsible for keeping track of all blur regions of a {@link ViewRootImpl} and posting a * message when it's time to propagate them. */ public static final class Aggregator { - - private final ArrayMap mBlurRegions = - new ArrayMap<>(); + private final Object mRtLock = new Object(); + // Maintains a list of all *visible* blur drawables. Confined to UI thread + private final ArraySet mDrawables = new ArraySet(); + @GuardedBy("mRtLock") + private final LongSparseArray> mFrameRtUpdates = new LongSparseArray(); private final ViewRootImpl mViewRoot; - private float[][] mTmpBlurRegionsArray; - private boolean mNeedsUpdate; + private BlurRegion[] mTmpBlurRegionsForFrame = new BlurRegion[0]; + private boolean mHasUiUpdates; public Aggregator(ViewRootImpl viewRoot) { mViewRoot = viewRoot; @@ -209,60 +251,191 @@ public final class BackgroundBlurDrawable extends Drawable { } /** - * Called from RenderThread only, already locked. - * @param drawable - * @param blurRegion + * Called when a BackgroundBlurDrawable has been updated */ - void onBlurRegionUpdated(BackgroundBlurDrawable drawable, - SurfaceControl.BlurRegion blurRegion) { - if (blurRegion.rect.isEmpty() || blurRegion.alpha == 0 || blurRegion.blurRadius == 0 - || !blurRegion.visible) { - mBlurRegions.remove(drawable); - mNeedsUpdate = true; - if (DEBUG) { - Log.d(TAG, "Remove " + blurRegion); + @UiThread + void onBlurDrawableUpdated(BackgroundBlurDrawable drawable) { + final boolean shouldBeDrawn = + drawable.mAlpha != 0 && drawable.mBlurRadius > 0 && drawable.mVisible; + final boolean isDrawn = mDrawables.contains(drawable); + if (shouldBeDrawn) { + mHasUiUpdates = true; + if (!isDrawn) { + mDrawables.add(drawable); + if (DEBUG) { + Log.d(TAG, "Add " + drawable); + } + } else { + if (DEBUG) { + Log.d(TAG, "Update " + drawable); + } } - } else { - mBlurRegions.put(drawable, blurRegion); - mNeedsUpdate = true; + } else if (!shouldBeDrawn && isDrawn) { + mHasUiUpdates = true; + mDrawables.remove(drawable); if (DEBUG) { - Log.d(TAG, "Update " + blurRegion); + Log.d(TAG, "Remove " + drawable); } } } + // Called from a thread pool + void onRenderNodePositionChanged(long frameNumber, Runnable update) { + // One of the blur region's position has changed, so we have to send an updated list + // of blur regions to SurfaceFlinger for this frame. + synchronized (mRtLock) { + ArraySet frameRtUpdates = mFrameRtUpdates.get(frameNumber); + if (frameRtUpdates == null) { + frameRtUpdates = new ArraySet<>(); + mFrameRtUpdates.put(frameNumber, frameRtUpdates); + } + frameRtUpdates.add(update); + } + } + /** - * If there are any blur regions visible on the screen at the moment. + * @return true if there are any updates that need to be sent to SF */ + @UiThread + public boolean hasUpdates() { + return mHasUiUpdates; + } + + /** + * @return true if there are any visible blur regions + */ + @UiThread public boolean hasRegions() { - return mBlurRegions.size() > 0; + return mDrawables.size() > 0; } /** - * Dispatch blur updates, if there were any. - * @param frameNumber Frame where the update should happen. + * @return an array of BlurRegions, which are holding a copy of the information in + * all the currently visible BackgroundBlurDrawables */ - public void dispatchBlurTransactionIfNeeded(long frameNumber) { - synchronized (this) { - if (!mNeedsUpdate) { - return; + @UiThread + public BlurRegion[] getBlurRegionsCopyForRT() { + if (mHasUiUpdates) { + mTmpBlurRegionsForFrame = new BlurRegion[mDrawables.size()]; + for (int i = 0; i < mDrawables.size(); i++) { + mTmpBlurRegionsForFrame[i] = new BlurRegion(mDrawables.valueAt(i)); } - mNeedsUpdate = false; - - if (mTmpBlurRegionsArray == null - || mTmpBlurRegionsArray.length != mBlurRegions.size()) { - mTmpBlurRegionsArray = new float[mBlurRegions.size()][]; - } - if (DEBUG) { - Log.d(TAG, "onBlurRegionUpdated will dispatch " + mTmpBlurRegionsArray.length - + " regions for frame " + frameNumber); - } - for (int i = 0; i < mTmpBlurRegionsArray.length; i++) { - mTmpBlurRegionsArray[i] = mBlurRegions.valueAt(i).toFloatArray(); - } - - mViewRoot.dispatchBlurRegions(mTmpBlurRegionsArray, frameNumber); + mHasUiUpdates = false; } + + return mTmpBlurRegionsForFrame; + } + + /** + * Called on RenderThread. + * + * @return all blur regions if there are any ui or position updates for this frame, + * null otherwise + */ + @VisibleForTesting + public float[][] getBlurRegionsToDispatchToSf(long frameNumber, + BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) { + synchronized (mRtLock) { + if (!hasUiUpdatesForFrame && (mFrameRtUpdates.size() == 0 + || mFrameRtUpdates.keyAt(0) > frameNumber)) { + return null; + } + + // mFrameRtUpdates holds position updates coming from a thread pool span from + // RenderThread. At this point, all position updates for frame frameNumber should + // have been added to mFrameRtUpdates. + // Here, we apply all updates for frames <= frameNumber in case some previous update + // has been missed. This also protects mFrameRtUpdates from memory leaks. + while (mFrameRtUpdates.size() != 0 && mFrameRtUpdates.keyAt(0) <= frameNumber) { + final ArraySet frameUpdates = mFrameRtUpdates.valueAt(0); + mFrameRtUpdates.removeAt(0); + for (int i = 0; i < frameUpdates.size(); i++) { + frameUpdates.valueAt(i).run(); + } + } + } + + if (DEBUG) { + Log.d(TAG, "Dispatching " + blurRegionsForFrame.length + " blur regions:"); + } + + final float[][] blurRegionsArray = new float[blurRegionsForFrame.length][]; + for (int i = 0; i < blurRegionsArray.length; i++) { + blurRegionsArray[i] = blurRegionsForFrame[i].toFloatArray(); + if (DEBUG) { + Log.d(TAG, blurRegionsForFrame[i].toString()); + } + } + return blurRegionsArray; + } + + /** + * Called on RenderThread in FrameDrawingCallback. + * Dispatch all blur regions if there are any ui or position updates. + */ + public void dispatchBlurTransactionIfNeeded(long frameNumber, + BlurRegion[] blurRegionsForFrame, boolean hasUiUpdatesForFrame) { + final float[][] blurRegionsArray = getBlurRegionsToDispatchToSf(frameNumber, + blurRegionsForFrame, hasUiUpdatesForFrame); + if (blurRegionsArray != null) { + mViewRoot.dispatchBlurRegions(blurRegionsArray, frameNumber); + } + } + + } + + /** + * Wrapper for sending blur data to SurfaceFlinger + * Confined to RenderThread. + */ + public static final class BlurRegion { + public final int blurRadius; + public final float cornerRadiusTL; + public final float cornerRadiusTR; + public final float cornerRadiusBL; + public final float cornerRadiusBR; + public final float alpha; + public final Rect rect; + + BlurRegion(BackgroundBlurDrawable drawable) { + alpha = drawable.mAlpha; + blurRadius = drawable.mBlurRadius; + cornerRadiusTL = drawable.mCornerRadiusTL; + cornerRadiusTR = drawable.mCornerRadiusTR; + cornerRadiusBL = drawable.mCornerRadiusBL; + cornerRadiusBR = drawable.mCornerRadiusBR; + rect = drawable.mRect; + } + + /** + * Serializes this class into a float array that's more JNI friendly. + */ + float[] toFloatArray() { + final float[] floatArray = new float[10]; + floatArray[0] = blurRadius; + floatArray[1] = alpha; + floatArray[2] = rect.left; + floatArray[3] = rect.top; + floatArray[4] = rect.right; + floatArray[5] = rect.bottom; + floatArray[6] = cornerRadiusTL; + floatArray[7] = cornerRadiusTR; + floatArray[8] = cornerRadiusBL; + floatArray[9] = cornerRadiusBR; + return floatArray; + } + + @Override + public String toString() { + return "BlurRegion{" + + "blurRadius=" + blurRadius + + ", corners={" + cornerRadiusTL + + "," + cornerRadiusTR + + "," + cornerRadiusBL + + "," + cornerRadiusBR + + "}, alpha=" + alpha + + ", rect=" + rect + + "}"; } } } diff --git a/core/tests/coretests/src/android/view/BlurAggregatorTest.java b/core/tests/coretests/src/android/view/BlurAggregatorTest.java new file mode 100644 index 0000000000000..b01f2755efdda --- /dev/null +++ b/core/tests/coretests/src/android/view/BlurAggregatorTest.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.graphics.drawable.BackgroundBlurDrawable; +import com.android.internal.graphics.drawable.BackgroundBlurDrawable.Aggregator; +import com.android.internal.graphics.drawable.BackgroundBlurDrawable.BlurRegion; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BlurAggregatorTest { + private static final int TEST_BLUR_RADIUS = 30; + private static final int TEST_FRAME_NUMBER = 1; + + private Context mContext; + + private Aggregator mAggregator; + private BackgroundBlurDrawable mDrawable; + + private ViewRootImpl mViewRoot; + + @Before + public void setUp() { + mContext = getInstrumentation().getTargetContext(); + getInstrumentation().runOnMainSync(() -> { + mViewRoot = new ViewRootImpl(mContext, mContext.getDisplayNoVerify()); + }); + mAggregator = new Aggregator(mViewRoot); + mDrawable = createTestBackgroundBlurDrawable(); + } + + private BackgroundBlurDrawable createTestBackgroundBlurDrawable() { + final BackgroundBlurDrawable drawable = mAggregator.createBackgroundBlurDrawable(mContext); + drawable.setBlurRadius(TEST_BLUR_RADIUS); + final boolean hasUpdates = mAggregator.hasUpdates(); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions, hasUpdates); + return drawable; + } + + @Test + public void testBlurRadiusUpdatePropagatesToRenderThreadIfNeeded() { + mDrawable.setBlurRadius(TEST_BLUR_RADIUS); + assertFalse(mAggregator.hasUpdates()); + + mDrawable.setBlurRadius(0); + assertTrue(mAggregator.hasUpdates()); + BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(0, blurRegions.length); + assertFalse(mAggregator.hasUpdates()); + + mDrawable.setBlurRadius(TEST_BLUR_RADIUS); + assertTrue(mAggregator.hasUpdates()); + blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + assertEquals(TEST_BLUR_RADIUS, blurRegions[0].blurRadius); + assertFalse(mAggregator.hasUpdates()); + + } + + @Test + public void testAlphaUpdatePropagatesToRenderThreadIfNeeded() { + mDrawable.setAlpha(20); + assertTrue(mAggregator.hasUpdates()); + BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + assertEquals(20 / 255f, blurRegions[0].alpha); + assertFalse(mAggregator.hasUpdates()); + + mDrawable.setAlpha(20); + assertFalse(mAggregator.hasUpdates()); + + mDrawable.setAlpha(0); + assertTrue(mAggregator.hasUpdates()); + blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(0, blurRegions.length); + assertFalse(mAggregator.hasUpdates()); + } + + @Test + public void testCornerRadiusUpdatePropagatesToRenderThreadIfNeeded() { + mDrawable.setCornerRadius(1f, 2f, 3f, 4f); + assertTrue(mAggregator.hasUpdates()); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + assertEquals(1f, blurRegions[0].cornerRadiusTL); + assertEquals(2f, blurRegions[0].cornerRadiusTR); + assertEquals(3f, blurRegions[0].cornerRadiusBL); + assertEquals(4f, blurRegions[0].cornerRadiusBR); + assertFalse(mAggregator.hasUpdates()); + } + + @Test + public void testVisibleUpdatePropagatesToRenderThreadIfNeeded() { + mDrawable.setVisible(false, /* restart= */false); + assertTrue(mAggregator.hasUpdates()); + BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(0, blurRegions.length); + assertFalse(mAggregator.hasUpdates()); + + mDrawable.setVisible(true, /* restart= */ false); + assertTrue(mAggregator.hasUpdates()); + blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + assertEquals(TEST_BLUR_RADIUS, blurRegions[0].blurRadius); + assertFalse(mAggregator.hasUpdates()); + } + + @Test + public void testBlurRegionCopyForRtIsSameIfNoUiUpdates() { + mDrawable.setBlurRadius(30); + BlurRegion[] blurRegions1 = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions1.length); + assertEquals(30, blurRegions1[0].blurRadius); + + BlurRegion[] blurRegions2 = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(blurRegions1, blurRegions2); + } + + @Test + public void testPositionUpdateAppearsInBlurRegion() { + BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + + mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); + mAggregator.getBlurRegionsToDispatchToSf(TEST_FRAME_NUMBER, blurRegions, + mAggregator.hasUpdates()); + assertEquals(1, blurRegions[0].rect.left); + assertEquals(2, blurRegions[0].rect.top); + assertEquals(3, blurRegions[0].rect.right); + assertEquals(4, blurRegions[0].rect.bottom); + } + + @Test + public void testNoBlurRegionsDispatchedWhenNoUpdates() { + final boolean hasUpdates = mAggregator.hasUpdates(); + assertFalse(hasUpdates); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + + float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, hasUpdates); + assertNull(blurRegionsForSf); + } + + @Test + public void testBlurRegionDispatchedIfOnlyDrawableUpdated() { + mDrawable.setBlurRadius(50); + final boolean hasUpdates = mAggregator.hasUpdates(); + assertTrue(hasUpdates); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + + float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, hasUpdates); + assertNotNull(blurRegionsForSf); + assertEquals(1, blurRegionsForSf.length); + assertEquals(50f, blurRegionsForSf[0][0]); + } + + @Test + public void testBlurRegionDispatchedIfOnlyPositionUpdated() { + final boolean hasUpdates = mAggregator.hasUpdates(); + assertFalse(hasUpdates); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + + mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); + float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, hasUpdates); + assertNotNull(blurRegionsForSf); + assertEquals(1, blurRegionsForSf.length); + assertEquals((float) TEST_BLUR_RADIUS, blurRegionsForSf[0][0]); + assertEquals(1f, blurRegionsForSf[0][2]); + assertEquals(2f, blurRegionsForSf[0][3]); + assertEquals(3f, blurRegionsForSf[0][4]); + assertEquals(4f, blurRegionsForSf[0][5]); + } + + @Test + public void testPositionUpdateIsAppliedInNextFrameIfMissed() { + final boolean hasUpdates = mAggregator.hasUpdates(); + assertFalse(hasUpdates); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + + mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); + float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates); + assertNotNull(blurRegionsForSf); + assertEquals(1, blurRegionsForSf.length); + assertEquals((float) TEST_BLUR_RADIUS, blurRegionsForSf[0][0]); + assertEquals(1f, blurRegionsForSf[0][2]); + assertEquals(2f, blurRegionsForSf[0][3]); + assertEquals(3f, blurRegionsForSf[0][4]); + assertEquals(4f, blurRegionsForSf[0][5]); + } + + @Test + public void testMultipleDrawablesDispatchedToSfIfOneIsUpdated() { + final BackgroundBlurDrawable drawable2 = createTestBackgroundBlurDrawable(); + drawable2.setBlurRadius(50); + final boolean hasUpdates = mAggregator.hasUpdates(); + assertTrue(hasUpdates); + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(2, blurRegions.length); + + // Check that an update in one of the drawables triggers a dispatch of all blur regions + float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, hasUpdates); + assertNotNull(blurRegionsForSf); + assertEquals(2, blurRegionsForSf.length); + + // Check that the Aggregator deleted all position updates for frame TEST_FRAME_NUMBER + blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false); + assertNull(blurRegionsForSf); + + // Check that a position update triggers a dispatch of all blur regions + drawable2.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); + blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER + 1, blurRegions, hasUpdates); + assertNotNull(blurRegionsForSf); + assertEquals(2, blurRegionsForSf.length); + } + + @Test + public void testUiThreadUpdatesDoNotChangeStateOnRenderThread() { + // Updates for frame N + mDrawable.setBlurRadius(50); + mDrawable.setCornerRadius(1, 2, 3, 4); + mDrawable.setAlpha(20); + + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + assertEquals(1, blurRegions.length); + assertEquals(50, blurRegions[0].blurRadius); + assertEquals(20 / 255f, blurRegions[0].alpha); + assertEquals(1f, blurRegions[0].cornerRadiusTL); + assertEquals(2f, blurRegions[0].cornerRadiusTR); + assertEquals(3f, blurRegions[0].cornerRadiusBL); + assertEquals(4f, blurRegions[0].cornerRadiusBR); + + // Updates for frame N+1 + mDrawable.setBlurRadius(60); + mDrawable.setCornerRadius(10, 20, 30, 40); + mDrawable.setAlpha(40); + + // Assert state for frame N is untouched + assertEquals(50, blurRegions[0].blurRadius); + assertEquals(20 / 255f, blurRegions[0].alpha); + assertEquals(1f, blurRegions[0].cornerRadiusTL); + assertEquals(2f, blurRegions[0].cornerRadiusTR); + assertEquals(3f, blurRegions[0].cornerRadiusBL); + assertEquals(4f, blurRegions[0].cornerRadiusBR); + } + + @Test + public void testPositionUpdatesForFutureFramesAreNotAppliedForCurrentFrame() { + final BlurRegion[] blurRegions = mAggregator.getBlurRegionsCopyForRT(); + + mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER, 1, 2, 3, 4); + mDrawable.mPositionUpdateListener.positionChanged(TEST_FRAME_NUMBER + 1, 5, 6, 7, 8); + + final float[][] blurRegionsForSf = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER, blurRegions, /* hasUiUpdates= */ false); + assertNotNull(blurRegionsForSf); + assertEquals(1, blurRegionsForSf.length); + // Assert state for first frame is not affected by update for second frame + assertEquals((float) TEST_BLUR_RADIUS, blurRegionsForSf[0][0]); + assertEquals(1f, blurRegionsForSf[0][2]); + assertEquals(2f, blurRegionsForSf[0][3]); + assertEquals(3f, blurRegionsForSf[0][4]); + assertEquals(4f, blurRegionsForSf[0][5]); + + final float[][] blurRegionsForSfForNextFrame = mAggregator.getBlurRegionsToDispatchToSf( + TEST_FRAME_NUMBER + 1, blurRegions, /* hasUiUpdates= */ false); + assertNotNull(blurRegionsForSfForNextFrame); + assertEquals(1, blurRegionsForSfForNextFrame.length); + // Assert second frame updates are applied normally + assertEquals((float) TEST_BLUR_RADIUS, blurRegionsForSfForNextFrame[0][0]); + assertEquals(5f, blurRegionsForSfForNextFrame[0][2]); + assertEquals(6f, blurRegionsForSfForNextFrame[0][3]); + assertEquals(7f, blurRegionsForSfForNextFrame[0][4]); + assertEquals(8f, blurRegionsForSfForNextFrame[0][5]); + } + +}