Introduce SurfaceControlCompat.Transaction#setFrameRate API

Relnote: "Add setFrameRate/clearFrameRate APIs to
SurfaceControlCompat.Transaction in order to control the
frame rate alongside the change strategy for seamless or
default transitions."

Bug: 304785682
Test: Added tests to SurfaceControlCompatTest
Change-Id: I6045cfe9bba4247fe93f7eb6cdc67e61d65dbf29
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index 83cce7a..f8106c2 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -327,7 +327,11 @@
     field public static final int BUFFER_TRANSFORM_ROTATE_180 = 3; // 0x3
     field public static final int BUFFER_TRANSFORM_ROTATE_270 = 7; // 0x7
     field public static final int BUFFER_TRANSFORM_ROTATE_90 = 4; // 0x4
+    field public static final int CHANGE_FRAME_RATE_ALWAYS = 1; // 0x1
+    field public static final int CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0; // 0x0
     field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+    field public static final int FRAME_RATE_COMPATIBILITY_DEFAULT = 0; // 0x0
+    field public static final int FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1; // 0x1
   }
 
   public static final class SurfaceControlCompat.Builder {
@@ -344,6 +348,7 @@
   @RequiresApi(android.os.Build.VERSION_CODES.Q) public static final class SurfaceControlCompat.Transaction implements java.lang.AutoCloseable {
     ctor public SurfaceControlCompat.Transaction();
     method @RequiresApi(android.os.Build.VERSION_CODES.S) public androidx.graphics.surface.SurfaceControlCompat.Transaction addTransactionCommittedListener(java.util.concurrent.Executor executor, androidx.graphics.surface.SurfaceControlCompat.TransactionCommittedListener listener);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction clearFrameRate(androidx.graphics.surface.SurfaceControlCompat surfaceControl);
     method public void close();
     method public void commit();
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void commitTransactionOnDraw(android.view.AttachedSurfaceControl attachedSurfaceControl);
@@ -358,6 +363,7 @@
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDamageRegion(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Region? region);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDataSpace(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int dataSpace);
     method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public androidx.graphics.surface.SurfaceControlCompat.Transaction setExtendedRangeBrightness(androidx.graphics.surface.SurfaceControlCompat surfaceControl, @FloatRange(from=1.0, fromInclusive=true) float currentBufferRatio, @FloatRange(from=1.0, fromInclusive=true) float desiredRatio);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setFrameRate(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float frameRate, int compatibility, int changeFrameRateStrategy);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setLayer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int z);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setOpaque(androidx.graphics.surface.SurfaceControlCompat surfaceControl, boolean isOpaque);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setPosition(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float x, float y);
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index 1977cae..ea20f69 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -328,7 +328,11 @@
     field public static final int BUFFER_TRANSFORM_ROTATE_180 = 3; // 0x3
     field public static final int BUFFER_TRANSFORM_ROTATE_270 = 7; // 0x7
     field public static final int BUFFER_TRANSFORM_ROTATE_90 = 4; // 0x4
+    field public static final int CHANGE_FRAME_RATE_ALWAYS = 1; // 0x1
+    field public static final int CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0; // 0x0
     field public static final androidx.graphics.surface.SurfaceControlCompat.Companion Companion;
+    field public static final int FRAME_RATE_COMPATIBILITY_DEFAULT = 0; // 0x0
+    field public static final int FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1; // 0x1
   }
 
   public static final class SurfaceControlCompat.Builder {
@@ -345,6 +349,7 @@
   @RequiresApi(android.os.Build.VERSION_CODES.Q) public static final class SurfaceControlCompat.Transaction implements java.lang.AutoCloseable {
     ctor public SurfaceControlCompat.Transaction();
     method @RequiresApi(android.os.Build.VERSION_CODES.S) public androidx.graphics.surface.SurfaceControlCompat.Transaction addTransactionCommittedListener(java.util.concurrent.Executor executor, androidx.graphics.surface.SurfaceControlCompat.TransactionCommittedListener listener);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction clearFrameRate(androidx.graphics.surface.SurfaceControlCompat surfaceControl);
     method public void close();
     method public void commit();
     method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public void commitTransactionOnDraw(android.view.AttachedSurfaceControl attachedSurfaceControl);
@@ -359,6 +364,7 @@
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDamageRegion(androidx.graphics.surface.SurfaceControlCompat surfaceControl, android.graphics.Region? region);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setDataSpace(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int dataSpace);
     method @RequiresApi(android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public androidx.graphics.surface.SurfaceControlCompat.Transaction setExtendedRangeBrightness(androidx.graphics.surface.SurfaceControlCompat surfaceControl, @FloatRange(from=1.0, fromInclusive=true) float currentBufferRatio, @FloatRange(from=1.0, fromInclusive=true) float desiredRatio);
+    method public androidx.graphics.surface.SurfaceControlCompat.Transaction setFrameRate(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float frameRate, int compatibility, int changeFrameRateStrategy);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setLayer(androidx.graphics.surface.SurfaceControlCompat surfaceControl, int z);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setOpaque(androidx.graphics.surface.SurfaceControlCompat surfaceControl, boolean isOpaque);
     method public androidx.graphics.surface.SurfaceControlCompat.Transaction setPosition(androidx.graphics.surface.SurfaceControlCompat surfaceControl, float x, float y);
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index b42c7c6..a63e68b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -1482,6 +1482,137 @@
             }
     }
 
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testSetFrameRate120WithDefaultCompatibilityAndAlwaysChangeStrategy() {
+        testFrameRate(
+            120f,
+            SurfaceControlCompat.FRAME_RATE_COMPATIBILITY_DEFAULT,
+            SurfaceControlCompat.CHANGE_FRAME_RATE_ALWAYS
+        )
+    }
+
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testSetFrameRateNegativeWithDefaultCompatibilityAndAlwaysChangeStrategy() {
+        testFrameRate(
+            -50f,
+            SurfaceControlCompat.FRAME_RATE_COMPATIBILITY_DEFAULT,
+            SurfaceControlCompat.CHANGE_FRAME_RATE_ALWAYS
+        )
+    }
+
+    @SuppressLint("NewApi")
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testSetFrameRateZeroWithDefaultCompatibilityAndAlwaysChangeStrategy() {
+        testFrameRate(
+            0f,
+            SurfaceControlCompat.FRAME_RATE_COMPATIBILITY_DEFAULT,
+            SurfaceControlCompat.CHANGE_FRAME_RATE_ALWAYS
+        )
+    }
+
+    @SuppressLint("NewApi")
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testSetFrameRateInvalidCompatibility() {
+        testFrameRate(
+            120f,
+            42,
+            SurfaceControlCompat.CHANGE_FRAME_RATE_ALWAYS
+        )
+    }
+
+    @SuppressLint("NewApi")
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testSetFrameRateInvalidStrategy() {
+        testFrameRate(
+            120f,
+            SurfaceControlCompat.FRAME_RATE_COMPATIBILITY_DEFAULT,
+            108
+        )
+    }
+
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testClearFrameRate() {
+        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+            .moveToState(
+                Lifecycle.State.CREATED
+            ).onActivity {
+                val callback = object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
+
+                        val surfaceControl = SurfaceControlCompat.Builder()
+                            .setName("testSurfaceControl")
+                            .setParent(it.mSurfaceView)
+                            .build()
+                        SurfaceControlCompat.Transaction()
+                            .clearFrameRate(surfaceControl)
+                            .commit()
+                    }
+                }
+
+                it.addSurface(it.mSurfaceView, callback)
+            }
+    }
+
+    private fun testFrameRate(frameRate: Float, compatibility: Int, strategy: Int) {
+        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+            .moveToState(
+                Lifecycle.State.CREATED
+            ).onActivity {
+                val callback = object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
+
+                        val surfaceControl = SurfaceControlCompat.Builder()
+                            .setName("testSurfaceControl")
+                            .setParent(it.mSurfaceView)
+                            .build()
+                        SurfaceControlCompat.Transaction()
+                            .setFrameRate(surfaceControl, frameRate, compatibility, strategy)
+                            .commit()
+                    }
+                }
+
+                it.addSurface(it.mSurfaceView, callback)
+            }
+    }
+
+    @SuppressLint("NewApi")
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.S_V2)
+    @Test
+    fun testSetDataSpaceThrowsOnUnsupportedPlatforms() {
+        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
+            .moveToState(
+                Lifecycle.State.CREATED
+            ).onActivity {
+                val callback = object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
+
+                        assertThrows(UnsupportedOperationException::class.java) {
+                            val surfaceControl = SurfaceControlCompat.Builder()
+                                .setName("testSurfaceControl")
+                                .setParent(it.mSurfaceView)
+                                .build()
+
+                            val extendedDataspace = DataSpace.pack(
+                                DataSpace.STANDARD_BT709,
+                                DataSpace.TRANSFER_SRGB, DataSpace.RANGE_EXTENDED
+                            )
+                            SurfaceControlCompat.Transaction()
+                                .setDataSpace(surfaceControl, extendedDataspace)
+                                .commit()
+                        }
+                    }
+                }
+
+                it.addSurface(it.mSurfaceView, callback)
+            }
+    }
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testSetExtendedRangeBrightness() {
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
index 47a529f..4603022 100644
--- a/graphics/graphics-core/src/main/cpp/graphics-core.cpp
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -486,6 +486,28 @@
     return static_cast<jint>(fd);
 }
 
+void JniBindings_nSetFrameRate(JNIEnv *env, jclass,
+                               jlong surfaceTransaction,
+                               jlong surfaceControl,
+                               jfloat framerate,
+                               jint compatibility,
+                               jint changeFrameRateStrategy) {
+    auto st = reinterpret_cast<ASurfaceTransaction *>(surfaceTransaction);
+    auto sc = reinterpret_cast<ASurfaceControl *>(surfaceControl);
+
+    if (android_get_device_api_level() >= 31) {
+        ASurfaceTransaction_setFrameRateWithChangeStrategy(
+                st,
+                sc,
+                framerate,
+                compatibility,
+                changeFrameRateStrategy
+        );
+    } else if (android_get_device_api_level() >= 30) {
+        ASurfaceTransaction_setFrameRate(st, sc, framerate, compatibility);
+    }
+}
+
 void loadRectInfo(JNIEnv *env) {
     gRectInfo.clazz = env->FindClass("android/graphics/Rect");
 
@@ -621,6 +643,11 @@
             "nGetPreviousReleaseFenceFd",
                 "(JJ)I",
                 (void *)JniBindings_nGetPreviousReleaseFenceFd
+        },
+        {
+            "nSetFrameRate",
+                "(JJFII)V",
+                (void *) JniBindings_nSetFrameRate
         }
 };
 
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index 355dbda..d616a42 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -53,12 +53,13 @@
     internal val scImpl: SurfaceControlImpl
 ) {
 
-    /**
-     * Constants for [Transaction.setBufferTransform].
-     *
-     * Various transformations that can be applied to a buffer.
-     */
     companion object {
+
+        /**
+         * Constants for [Transaction.setBufferTransform].
+         *
+         * Various transformations that can be applied to a buffer.
+         */
         @Suppress("AcronymName")
         @IntDef(
             value = [BUFFER_TRANSFORM_IDENTITY, BUFFER_TRANSFORM_MIRROR_HORIZONTAL,
@@ -96,6 +97,55 @@
          * Rotates the buffer 270 degrees clockwise. Maps a point (x, y) to (y, -x)
          */
         const val BUFFER_TRANSFORM_ROTATE_270 = 7
+
+        /**
+         * Constants for [Transaction.setFrameRate]
+         */
+        @IntDef(
+            value = [CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS, CHANGE_FRAME_RATE_ALWAYS]
+        )
+        internal annotation class ChangeFrameRateStrategy
+
+        /**
+         * Change the frame rate only if the transition is going to be seamless.
+         */
+        const val CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0
+
+        /**
+         * Change the frame rate even if the transition is going to be non-seamless, i.e.
+         * with visual interruptions for the user.
+         */
+        const val CHANGE_FRAME_RATE_ALWAYS = 1
+
+        /**
+         * Constants for configuring compatibility for [Transaction.setFrameRate]
+         */
+        @IntDef(
+            value = [FRAME_RATE_COMPATIBILITY_DEFAULT, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE]
+        )
+        internal annotation class FrameRateCompatibility
+
+        /**
+         * There are no inherent restrictions on the frame rate. When the system selects a frame
+         * rate other than what the app requested, the app will be able to run at the system frame
+         * rate without requiring pull down (the mechanical process of "pulling", physically moving,
+         * frame content downward to advance it from one frame to the next at a repetitive rate).
+         * This value should be used when displaying game content, UIs, and anything that isn't
+         * video.
+         */
+        const val FRAME_RATE_COMPATIBILITY_DEFAULT = 0
+
+        /**
+         * This compositing layer is being used to display content with an inherently fixed frame
+         * rate, e.g. a video that has a specific frame rate. When the system selects a frame rate
+         * other than what the app requested, the app will need to do pull down or use some other
+         * technique to adapt to the system's frame rate. Pull down involves the mechanical process
+         * of "pulling", physically moving, frame content downward to advance it from one frame to
+         * the next at a repetitive rate). The user experience is likely to be worse
+         * (e.g. more frame stuttering) than it would be if the system had chosen the app's
+         * requested frame rate. This value should be used for video content.
+         */
+        const val FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1
     }
 
     /**
@@ -486,6 +536,74 @@
         }
 
         /**
+         * Sets the intended frame rate for [SurfaceControlCompat].
+         *
+         * On devices that are capable of running the display at different refresh rates, the
+         * system may choose a display refresh rate to better match this surface's frame rate.
+         * Usage of this API won't directly affect the application's frame production pipeline.
+         * However, because the system may change the display refresh rate, calls to this function
+         * may result in changes to Choreographer callback timings, and changes to the time interval
+         * at which the system releases buffers back to the application.
+         *
+         * This method is only supported on Android R+ and is ignored on older platform versions.
+         *
+         * @param surfaceControl The target [SurfaceControlCompat] that will have it's frame rate
+         * changed
+         * @param frameRate The intended frame rate of this surface, in frames per second. 0 is a
+         * special value that indicates the app will accept the system's choice for the display
+         * frame rate, which is the default behavior if this function isn't called. Must be
+         * greater than or equal to 0.
+         * The frameRate param does not need to be a valid refresh rate for this device's display
+         * - e.g., it's fine to pass 30fps to a device that can only run the display at 60fps.
+         * @param compatibility The frame rate compatibility of this surface. The compatibility
+         * value may influence the system's choice of display frame rate. This must be either
+         * [FRAME_RATE_COMPATIBILITY_DEFAULT] or [FRAME_RATE_COMPATIBILITY_FIXED_SOURCE]
+         * This parameter is ignored when frameRate is 0.
+         * @param changeFrameRateStrategy Whether display refresh rate transitions should be
+         * seamless. A seamless transition does not have any visual interruptions, such as a black
+         * screen for a second or two. Must be either [CHANGE_FRAME_RATE_ALWAYS] or
+         * [CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS]. This parameter is only supported on Android S and
+         * when [frameRate] is not 0. This is ignored on older Android versions and when [frameRate]
+         * is 0.
+         */
+        fun setFrameRate(
+            surfaceControl: SurfaceControlCompat,
+            frameRate: Float,
+            @FrameRateCompatibility compatibility: Int,
+            @ChangeFrameRateStrategy changeFrameRateStrategy: Int
+        ): Transaction {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                mImpl.setFrameRate(
+                    surfaceControl.scImpl,
+                    frameRate,
+                    compatibility,
+                    changeFrameRateStrategy
+                )
+            }
+            return this
+        }
+
+        /**
+         * Clears the frame rate which was set for the surface SurfaceControl.
+         *
+         * This is equivalent to calling [setFrameRate] with 0 for the framerate and
+         * [FRAME_RATE_COMPATIBILITY_DEFAULT]
+         *
+         * Note that this only has an effect for surfaces presented on the display. If this surface
+         * is consumed by something other than the system compositor, e.g. a media codec, this call
+         * has no effect.
+         *
+         * This is only supported on Android R and above. This is ignored on older Android versions.
+         * @param surfaceControl [SurfaceControlCompat] to clear the frame rate
+         */
+        fun clearFrameRate(surfaceControl: SurfaceControlCompat): Transaction {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                mImpl.clearFrameRate(surfaceControl.scImpl)
+            }
+            return this
+        }
+
+        /**
          * Sets the desired extended range brightness for the layer. This only applies for layers
          * that are displaying [HardwareBuffer] instances with a DataSpace of
          * [DataSpace.RANGE_EXTENDED].
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
index 3dc97ae..6362285 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlImpl.kt
@@ -328,6 +328,25 @@
         ): Transaction
 
         /**
+         * See [SurfaceControlCompat.Transaction.setFrameRate]
+         */
+        @RequiresApi(Build.VERSION_CODES.R)
+        fun setFrameRate(
+            scImpl: SurfaceControlImpl,
+            frameRate: Float,
+            compatibility: Int,
+            changeFrameRateStrategy: Int
+        ): Transaction
+
+        /**
+         * See [SurfaceControlCompat.Transaction.clearFrameRate]
+         */
+        @RequiresApi(Build.VERSION_CODES.R)
+        fun clearFrameRate(
+            scImpl: SurfaceControlImpl,
+        ): Transaction
+
+        /**
          * Commit the transaction, clearing it's state, and making it usable as a new transaction.
          * This will not release any resources and [SurfaceControlImpl.Transaction.close] must be
          * called to release the transaction.
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
index 7440f62..8d5a96f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV29.kt
@@ -28,6 +28,8 @@
 import androidx.graphics.lowlatency.FrontBufferUtils
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
 import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
+import androidx.graphics.surface.SurfaceControlCompat.Companion.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+import androidx.graphics.surface.SurfaceControlCompat.Companion.FRAME_RATE_COMPATIBILITY_DEFAULT
 import androidx.hardware.SyncFenceCompat
 import androidx.hardware.SyncFenceImpl
 import androidx.hardware.SyncFenceV19
@@ -401,6 +403,37 @@
         }
 
         /**
+         * See [SurfaceControlCompat.Transaction.setFrameRate]
+         */
+        override fun setFrameRate(
+            scImpl: SurfaceControlImpl,
+            frameRate: Float,
+            compatibility: Int,
+            changeFrameRateStrategy: Int
+        ): Transaction {
+            transaction.setFrameRate(
+                scImpl.asWrapperSurfaceControl(),
+                frameRate,
+                compatibility,
+                changeFrameRateStrategy
+            )
+            return this
+        }
+
+        /**
+         * See [SurfaceControlCompat.Transaction.clearFrameRate]
+         */
+        override fun clearFrameRate(scImpl: SurfaceControlImpl): SurfaceControlImpl.Transaction {
+            setFrameRate(
+                scImpl,
+                0f,
+                FRAME_RATE_COMPATIBILITY_DEFAULT,
+                CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+            )
+            return this
+        }
+
+        /**
          * See [SurfaceControlWrapper.Transaction.close]
          */
         override fun close() {
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
index 6633e744..6ea12ea 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlV33.kt
@@ -305,6 +305,41 @@
             return this
         }
 
+        override fun setFrameRate(
+            scImpl: SurfaceControlImpl,
+            frameRate: Float,
+            compatibility: Int,
+            changeFrameRateStrategy: Int
+        ): Transaction {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                SurfaceControlVerificationHelperV31.setFrameRate(
+                    mTransaction,
+                    scImpl.asFrameworkSurfaceControl(),
+                    frameRate,
+                    compatibility,
+                    changeFrameRateStrategy
+                )
+            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                SurfaceControlVerificationHelperV30.setFrameRate(
+                    mTransaction,
+                    scImpl.asFrameworkSurfaceControl(),
+                    frameRate,
+                    compatibility
+                )
+            }
+            return this
+        }
+
+        override fun clearFrameRate(scImpl: SurfaceControlImpl): SurfaceControlImpl.Transaction {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                SurfaceControlVerificationHelperV34.clearFrameRate(
+                    mTransaction,
+                    scImpl.asFrameworkSurfaceControl()
+                )
+            }
+            return this
+        }
+
         /**
          * See [SurfaceControlImpl.Transaction.commit]
          */
@@ -367,3 +402,42 @@
         transaction.setDataSpace(surfaceControl, dataspace)
     }
 }
+
+@RequiresApi(Build.VERSION_CODES.S)
+private object SurfaceControlVerificationHelperV31 {
+    @androidx.annotation.DoNotInline
+    fun setFrameRate(
+        transaction: Transaction,
+        surfaceControl: SurfaceControl,
+        frameRate: Float,
+        compatibility: Int,
+        strategy: Int
+    ) {
+        transaction.setFrameRate(surfaceControl, frameRate, compatibility, strategy)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.R)
+private object SurfaceControlVerificationHelperV30 {
+    @androidx.annotation.DoNotInline
+    fun setFrameRate(
+        transaction: Transaction,
+        surfaceControl: SurfaceControl,
+        frameRate: Float,
+        compatibility: Int
+    ) {
+        transaction.setFrameRate(surfaceControl, frameRate, compatibility)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private object SurfaceControlVerificationHelperV34 {
+
+    @androidx.annotation.DoNotInline
+    fun clearFrameRate(
+        transaction: Transaction,
+        surfaceControl: SurfaceControl
+    ) {
+        transaction.clearFrameRate(surfaceControl)
+    }
+}
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
index 2459469..6c6b44c 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
@@ -192,6 +192,16 @@
         @JniVisible
         external fun nGetPreviousReleaseFenceFd(surfaceControl: Long, transactionStats: Long): Int
 
+        @JvmStatic
+        @JniVisible
+        external fun nSetFrameRate(
+            surfaceTransaction: Long,
+            surfaceControl: Long,
+            frameRate: Float,
+            compatibility: Int,
+            changeFrameRateStrategy: Int
+            )
+
         init {
             System.loadLibrary("graphics-core")
         }
@@ -658,6 +668,22 @@
             return this
         }
 
+        fun setFrameRate(
+            surfaceControl: SurfaceControlWrapper,
+            frameRate: Float,
+            compatibility: Int,
+            changeFrameRateStrategy: Int
+        ): Transaction {
+            JniBindings.nSetFrameRate(
+                mNativeSurfaceTransaction,
+                surfaceControl.mNativeSurfaceControl,
+                frameRate,
+                compatibility,
+                changeFrameRateStrategy
+            )
+            return this
+        }
+
         /**
          * Destroys the transaction object.
          */