[VRR] Support setRequestedFrameRate() with AVDs

Fixes: 342666766

Adds support for Animated Vector Drawables that operate on the
Render Thread to VRR. The votes from their respective Views
are considered on each vote.

Test: new tests, manual testing
Change-Id: I1e761d62e5bc4a706cc7fb2904f26aaa68e87584
diff --git a/core/java/android/view/NativeVectorDrawableAnimator.java b/core/java/android/view/NativeVectorDrawableAnimator.java
index b0556a3..e92bd1f 100644
--- a/core/java/android/view/NativeVectorDrawableAnimator.java
+++ b/core/java/android/view/NativeVectorDrawableAnimator.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import android.animation.Animator;
+
 /**
  * Exists just to allow for android.graphics & android.view package separation
  *
@@ -26,4 +28,7 @@
 public interface NativeVectorDrawableAnimator {
     /** @hide */
     long getAnimatorNativePtr();
+
+    /** @hide */
+    void setThreadedRendererAnimatorListener(Animator.AnimatorListener listener);
 }
diff --git a/core/java/android/view/ViewAnimationHostBridge.java b/core/java/android/view/ViewAnimationHostBridge.java
index e0fae21..62b2b6c 100644
--- a/core/java/android/view/ViewAnimationHostBridge.java
+++ b/core/java/android/view/ViewAnimationHostBridge.java
@@ -16,14 +16,19 @@
 
 package android.view;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.graphics.RenderNode;
 
+import androidx.annotation.NonNull;
+
 /**
  * Maps a View to a RenderNode's AnimationHost
  *
  * @hide
  */
-public class ViewAnimationHostBridge implements RenderNode.AnimationHost {
+public class ViewAnimationHostBridge extends AnimatorListenerAdapter
+        implements RenderNode.AnimationHost {
     private final View mView;
 
     /**
@@ -34,17 +39,35 @@
     }
 
     @Override
-    public void registerAnimatingRenderNode(RenderNode animator) {
-        mView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(animator);
+    public void registerAnimatingRenderNode(RenderNode renderNode, Animator animator) {
+        mView.mAttachInfo.mViewRootImpl.registerAnimatingRenderNode(renderNode);
+        animator.addListener(this);
     }
 
     @Override
     public void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator) {
         mView.mAttachInfo.mViewRootImpl.registerVectorDrawableAnimator(animator);
+        animator.setThreadedRendererAnimatorListener(this);
     }
 
     @Override
     public boolean isAttached() {
         return mView.mAttachInfo != null;
     }
+
+    @Override
+    public void onAnimationStart(@NonNull Animator animation) {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.addThreadedRendererView(mView);
+        }
+    }
+
+    @Override
+    public void onAnimationEnd(@NonNull Animator animation) {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.removeThreadedRendererView(mView);
+        }
+    }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 4acd5d5..40bfa92 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -667,6 +667,8 @@
     private int mMinusTwoFrameIntervalMillis = 0;
     // VRR has the invalidation idle message been posted?
     private boolean mInvalidationIdleMessagePosted = false;
+    // VRR: List of all Views that are animating with the threaded render
+    private ArrayList<View> mThreadedRendererViews = new ArrayList();
 
     /**
      * Update the Choreographer's FrameInfo object with the timing information for the current
@@ -4277,6 +4279,7 @@
             }
             setCategoryFromCategoryCounts();
             updateInfrequentCount();
+            updateFrameRateFromThreadedRendererViews();
             setPreferredFrameRate(mPreferredFrameRate);
             setPreferredFrameRateCategory(mPreferredFrameRateCategory);
             if (mPreferredFrameRate > 0
@@ -6789,8 +6792,9 @@
                         mFrameRateCategoryLowCount = 0;
                         mPreferredFrameRate = 0;
                         mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
-                        setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE);
-                        setPreferredFrameRate(0f);
+                        updateFrameRateFromThreadedRendererViews();
+                        setPreferredFrameRate(mPreferredFrameRate);
+                        setPreferredFrameRateCategory(mPreferredFrameRateCategory);
                         mInvalidationIdleMessagePosted = false;
                     } else {
                         mInvalidationIdleMessagePosted = true;
@@ -12617,6 +12621,24 @@
     }
 
     /**
+     * Views that are animating with the ThreadedRenderer don't use the normal invalidation
+     * path, so the value won't be updated through performTraversals. This reads the votes
+     * from those views.
+     */
+    private void updateFrameRateFromThreadedRendererViews() {
+        ArrayList<View> views = mThreadedRendererViews;
+        for (int i = views.size() - 1; i >= 0; i--) {
+            View view = views.get(i);
+            View.AttachInfo attachInfo = view.mAttachInfo;
+            if (attachInfo == null || attachInfo.mViewRootImpl != this) {
+                views.remove(i);
+            } else {
+                view.votePreferredFrameRate();
+            }
+        }
+    }
+
+    /**
      * Sets the mPreferredFrameRateCategory from the high, high_hint, normal, and low counts.
      */
     private void setCategoryFromCategoryCounts() {
@@ -12797,6 +12819,31 @@
     }
 
     /**
+     * Mark a View as having an active ThreadedRenderer animation. This is used for
+     * RenderNodeAnimators and AnimatedVectorDrawables. When the animation stops,
+     * {@link #removeThreadedRendererView(View)} must be called.
+     * @param view The View with the ThreadedRenderer animation that started.
+     */
+    public void addThreadedRendererView(View view) {
+        if (!mThreadedRendererViews.contains(view)) {
+            mThreadedRendererViews.add(view);
+        }
+    }
+
+    /**
+     * When a ThreadedRenderer animation ends, the View that is associated with it using
+     * {@link #addThreadedRendererView(View)} must be removed with a call to this method.
+     * @param view The View whose ThreadedRender animation has stopped.
+     */
+    public void removeThreadedRendererView(View view) {
+        mThreadedRendererViews.remove(view);
+        if (!mInvalidationIdleMessagePosted && sSurfaceFlingerBugfixFlagValue) {
+            mInvalidationIdleMessagePosted = true;
+            mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+        }
+    }
+
+    /**
      * Returns {@link #INTERMITTENT_STATE_INTERMITTENT} when the ViewRootImpl has only been
      * updated intermittently, {@link #INTERMITTENT_STATE_NOT_INTERMITTENT} when it is
      * not updated intermittently, and {@link #INTERMITTENT_STATE_IN_TRANSITION} when it
@@ -13041,7 +13088,7 @@
         mMinusOneFrameIntervalMillis = timeIntervalMillis;
 
         mLastUpdateTimeMillis = currentTimeMillis;
-        if (timeIntervalMillis + mMinusTwoFrameIntervalMillis
+        if (mThreadedRendererViews.isEmpty() && timeIntervalMillis + mMinusTwoFrameIntervalMillis
                 >= INFREQUENT_UPDATE_INTERVAL_MILLIS) {
             int infrequentUpdateCount = mInfrequentUpdateCount;
             mInfrequentUpdateCount = infrequentUpdateCount == INFREQUENT_UPDATE_COUNTS
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 6757703..07446e7 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -41,11 +41,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.platform.test.annotations.LargeTest;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.DisplayMetrics;
 import android.widget.FrameLayout;
+import android.widget.ProgressBar;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
@@ -623,6 +625,7 @@
         assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory());
     }
 
+    @LargeTest
     @Test
     @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
             FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
@@ -646,6 +649,138 @@
                 mViewRoot.getLastPreferredFrameRateCategory());
     }
 
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void vectorDrawableFrameRate() throws Throwable {
+        final ProgressBar[] progressBars = new ProgressBar[3];
+        final ViewGroup[] parents = new ViewGroup[1];
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup parent = (ViewGroup) mMovingView.getParent();
+            parents[0] = parent;
+            ProgressBar progressBar1 = new ProgressBar(mActivity);
+            parent.addView(progressBar1);
+            progressBar1.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            progressBar1.setIndeterminate(true);
+            progressBars[0] = progressBar1;
+
+            ProgressBar progressBar2 = new ProgressBar(mActivity);
+            parent.addView(progressBar2);
+            progressBar2.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NORMAL);
+            progressBar2.setIndeterminate(true);
+            progressBars[1] = progressBar2;
+
+            ProgressBar progressBar3 = new ProgressBar(mActivity);
+            parent.addView(progressBar3);
+            progressBar3.setRequestedFrameRate(45f);
+            progressBar3.setIndeterminate(true);
+            progressBars[2] = progressBar3;
+        });
+        waitForFrameRateCategoryToSettle();
+
+        // Wait for idle timeout
+        Thread.sleep(1000);
+        assertEquals(45f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRoot.getLastPreferredFrameRateCategory());
+
+        // Removing the vector drawable with NORMAL should drop the category to LOW
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[1]));
+        Thread.sleep(1000);
+        assertEquals(45f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_LOW,
+                mViewRoot.getLastPreferredFrameRateCategory());
+        // Removing the one voting for frame rate should leave only the category
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[2]));
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_LOW,
+                mViewRoot.getLastPreferredFrameRateCategory());
+        // Removing the last one should leave it with no preference
+        mActivityRule.runOnUiThread(() -> parents[0].removeView(progressBars[0]));
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void renderNodeAnimatorFrameRateCanceled() throws Throwable {
+        mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+        waitForFrameRateCategoryToSettle();
+
+        RenderNodeAnimator[] renderNodeAnimator = new RenderNodeAnimator[1];
+        renderNodeAnimator[0] = new RenderNodeAnimator(RenderNodeAnimator.ALPHA, 0f);
+        renderNodeAnimator[0].setDuration(100000);
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].setTarget(mMovingView);
+            renderNodeAnimator[0].start();
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            runAfterDraw(() -> {
+                assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(FRAME_RATE_CATEGORY_LOW,
+                        mViewRoot.getLastPreferredFrameRateCategory());
+            });
+        });
+        waitForAfterDraw();
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].cancel();
+        });
+
+        // Wait for idle timeout
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
+    @LargeTest
+    @Test
+    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+            FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+            com.android.graphics.surfaceflinger.flags.Flags.FLAG_VRR_BUGFIX_24Q4
+    })
+    public void renderNodeAnimatorFrameRateRemoved() throws Throwable {
+        mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE);
+        waitForFrameRateCategoryToSettle();
+
+        RenderNodeAnimator[] renderNodeAnimator = new RenderNodeAnimator[1];
+        renderNodeAnimator[0] = new RenderNodeAnimator(RenderNodeAnimator.ALPHA, 0f);
+        renderNodeAnimator[0].setDuration(100000);
+
+        mActivityRule.runOnUiThread(() -> {
+            renderNodeAnimator[0].setTarget(mMovingView);
+            renderNodeAnimator[0].start();
+            mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_LOW);
+            runAfterDraw(() -> {
+                assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(FRAME_RATE_CATEGORY_LOW,
+                        mViewRoot.getLastPreferredFrameRateCategory());
+            });
+        });
+        waitForAfterDraw();
+
+        mActivityRule.runOnUiThread(() -> {
+            ViewGroup parent = (ViewGroup) mMovingView.getParent();
+            assert parent != null;
+            parent.removeView(mMovingView);
+        });
+
+        Thread.sleep(1000);
+        assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+        assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+                mViewRoot.getLastPreferredFrameRateCategory());
+    }
+
     private void runAfterDraw(@NonNull Runnable runnable) {
         Handler handler = new Handler(Looper.getMainLooper());
         mAfterDrawLatch = new CountDownLatch(1);
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 0650b78..211f74a 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -16,6 +16,7 @@
 
 package android.graphics;
 
+import android.animation.Animator;
 import android.annotation.BytesLong;
 import android.annotation.ColorInt;
 import android.annotation.FloatRange;
@@ -1639,7 +1640,7 @@
      */
     public interface AnimationHost {
         /** @hide */
-        void registerAnimatingRenderNode(RenderNode animator);
+        void registerAnimatingRenderNode(RenderNode renderNode, Animator animator);
 
         /** @hide */
         void registerVectorDrawableAnimator(NativeVectorDrawableAnimator animator);
@@ -1654,7 +1655,7 @@
             throw new IllegalStateException("Cannot start this animator on a detached view!");
         }
         nAddAnimator(mNativeRenderNode, animator.getNativeAnimator());
-        mAnimationHost.registerAnimatingRenderNode(this);
+        mAnimationHost.registerAnimatingRenderNode(this, animator);
     }
 
     /** @hide */
diff --git a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
index 55f205b..d4bb461 100644
--- a/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
+++ b/graphics/java/android/graphics/drawable/AnimatedVectorDrawable.java
@@ -1266,6 +1266,7 @@
         private final IntArray mPendingAnimationActions = new IntArray();
         private final AnimatedVectorDrawable mDrawable;
         private long mTotalDuration;
+        private AnimatorListener mThreadedRendererAnimatorListener;
 
         VectorDrawableAnimatorRT(AnimatedVectorDrawable drawable) {
             mDrawable = drawable;
@@ -1689,6 +1690,9 @@
             if (mListener != null) {
                 mListener.onAnimationStart(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationStart(null);
+            }
         }
 
         // This should only be called after animator has been added to the RenderNode target.
@@ -1717,6 +1721,9 @@
             if (mListener != null) {
                 mListener.onAnimationStart(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationStart(null);
+            }
         }
 
         @Override
@@ -1725,6 +1732,11 @@
         }
 
         @Override
+        public void setThreadedRendererAnimatorListener(AnimatorListener animatorListener) {
+            mThreadedRendererAnimatorListener = animatorListener;
+        }
+
+        @Override
         public boolean canReverse() {
             return mIsReversible;
         }
@@ -1788,6 +1800,9 @@
             if (mListener != null) {
                 mListener.onAnimationEnd(null);
             }
+            if (mThreadedRendererAnimatorListener != null) {
+                mThreadedRendererAnimatorListener.onAnimationEnd(null);
+            }
         }
 
         // onFinished: should be called from native