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 4f6679b2bc972..f8e65bd0d056c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -3969,11 +3969,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; } @@ -3981,18 +3982,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]); + } + +}