add RuntimeXfermode API to the graphics platform

Flag: com.android.graphics.hwui.flags.runtime_color_filters_blenders

Test: atest CtsUiRenderingTestCases:RuntimeXfermodeTests
Bug: b/358126864

Change-Id: Idd15f937707feec51387b513b75cfadfaf0336a3
diff --git a/core/api/current.txt b/core/api/current.txt
index dc7ccd4..ccecb96 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -17491,6 +17491,25 @@
     method public void setIntUniform(@NonNull String, @NonNull int[]);
   }
 
+  @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode {
+    ctor public RuntimeXfermode(@NonNull String);
+    method public void setColorUniform(@NonNull String, @ColorInt int);
+    method public void setColorUniform(@NonNull String, @ColorLong long);
+    method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color);
+    method public void setFloatUniform(@NonNull String, float);
+    method public void setFloatUniform(@NonNull String, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float, float);
+    method public void setFloatUniform(@NonNull String, @NonNull float[]);
+    method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter);
+    method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader);
+    method public void setIntUniform(@NonNull String, int);
+    method public void setIntUniform(@NonNull String, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int, int);
+    method public void setIntUniform(@NonNull String, @NonNull int[]);
+  }
+
   public class Shader {
     ctor @Deprecated public Shader();
     method public boolean getLocalMatrix(@NonNull android.graphics.Matrix);
diff --git a/graphics/java/android/graphics/BlendMode.java b/graphics/java/android/graphics/BlendMode.java
index 5c294aa..c6ae680 100644
--- a/graphics/java/android/graphics/BlendMode.java
+++ b/graphics/java/android/graphics/BlendMode.java
@@ -571,10 +571,10 @@
     }
 
     @NonNull
-    private final Xfermode mXfermode;
+    private final PorterDuffXfermode mXfermode;
 
     BlendMode(int mode) {
-        mXfermode = new Xfermode();
+        mXfermode = new PorterDuffXfermode();
         mXfermode.porterDuffMode = mode;
     }
 
@@ -582,7 +582,7 @@
      * @hide
      */
     @NonNull
-    public Xfermode getXfermode() {
+    public PorterDuffXfermode getXfermode() {
         return mXfermode;
     }
 }
diff --git a/graphics/java/android/graphics/ComposeShader.java b/graphics/java/android/graphics/ComposeShader.java
index 977aeaa..e714568 100644
--- a/graphics/java/android/graphics/ComposeShader.java
+++ b/graphics/java/android/graphics/ComposeShader.java
@@ -40,9 +40,12 @@
      * @param mode     The mode that combines the colors from the two shaders. If mode
      *                 is null, then SRC_OVER is assumed.
      */
+    //TODO(358126864): allow a ComposeShader to accept a RuntimeXfermode
     @Deprecated
     public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode) {
-        this(shaderA, shaderB, mode.porterDuffMode);
+        this(shaderA, shaderB,
+                mode instanceof PorterDuffXfermode ? ((PorterDuffXfermode) mode).porterDuffMode
+                : BlendMode.SRC_OVER.getXfermode().porterDuffMode);
     }
 
     /**
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 56bb0f0..2c166c3 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -71,6 +71,7 @@
     private long mNativePaint;
     private long mNativeShader;
     private long mNativeColorFilter;
+    private long mNativeXfermode;
 
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
@@ -735,6 +736,7 @@
         mPathEffect = null;
         mShader = null;
         mNativeShader = 0;
+        mNativeXfermode = 0;
         mTypeface = null;
         mXfermode = null;
 
@@ -780,6 +782,7 @@
         mNativeShader = paint.mNativeShader;
         mTypeface = paint.mTypeface;
         mXfermode = paint.mXfermode;
+        mNativeXfermode = paint.mNativeXfermode;
 
         mHasCompatScaling = paint.mHasCompatScaling;
         mCompatScaling = paint.mCompatScaling;
@@ -815,7 +818,7 @@
      *
      * Note: Although this method is |synchronized|, this is simply so it
      * is not thread-hostile to multiple threads calling this method. It
-     * is still unsafe to attempt to change the Shader/ColorFilter while
+     * is still unsafe to attempt to change the Shader/ColorFilter/Xfermode while
      * another thread attempts to access the native object.
      *
      * @hide
@@ -833,6 +836,13 @@
             mNativeColorFilter = newNativeColorFilter;
             nSetColorFilter(mNativePaint, mNativeColorFilter);
         }
+        if (mXfermode instanceof RuntimeXfermode) {
+            long newNativeXfermode = ((RuntimeXfermode) mXfermode).createNativeInstance();
+            if (newNativeXfermode != mNativeXfermode) {
+                mNativeXfermode = newNativeXfermode;
+                nSetXfermode(mNativePaint, mNativeXfermode);
+            }
+        }
         return mNativePaint;
     }
 
@@ -1427,16 +1437,17 @@
     }
 
     /**
-     * Get the paint's blend mode object.
+     * Get the paint's blend mode object. Will return null if there is a Xfermode applied that
+     * cannot be represented by a blend mode (i.e. a custom {@code RuntimeXfermode}
      *
      * @return the paint's blend mode (or null)
      */
     @Nullable
     public BlendMode getBlendMode() {
-        if (mXfermode == null) {
+        if (mXfermode == null || !(mXfermode instanceof PorterDuffXfermode)) {
             return null;
         } else {
-            return BlendMode.fromValue(mXfermode.porterDuffMode);
+            return BlendMode.fromValue(((PorterDuffXfermode) mXfermode).porterDuffMode);
         }
     }
 
@@ -1459,8 +1470,15 @@
 
     @Nullable
     private Xfermode installXfermode(Xfermode xfermode) {
-        int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
-        int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
+        if (xfermode instanceof RuntimeXfermode) {
+            mXfermode = xfermode;
+            nSetXfermode(mNativePaint, ((RuntimeXfermode) xfermode).createNativeInstance());
+            return xfermode;
+        }
+        int newMode = (xfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) xfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
+        int curMode = (mXfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) mXfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
         if (newMode != curMode) {
             nSetXfermode(mNativePaint, newMode);
         }
@@ -3823,6 +3841,8 @@
     @CriticalNative
     private static native void nSetXfermode(long paintPtr, int xfermode);
     @CriticalNative
+    private static native void nSetXfermode(long paintPtr, long xfermodePtr);
+    @CriticalNative
     private static native long nSetPathEffect(long paintPtr, long effect);
     @CriticalNative
     private static native long nSetMaskFilter(long paintPtr, long maskfilter);
diff --git a/graphics/java/android/graphics/PorterDuffXfermode.java b/graphics/java/android/graphics/PorterDuffXfermode.java
index ff9ff8b..83d0507 100644
--- a/graphics/java/android/graphics/PorterDuffXfermode.java
+++ b/graphics/java/android/graphics/PorterDuffXfermode.java
@@ -29,6 +29,9 @@
      *
      * @param mode           The porter-duff mode that is applied
      */
+    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
+    int porterDuffMode = DEFAULT;
+    PorterDuffXfermode() {}
     public PorterDuffXfermode(PorterDuff.Mode mode) {
         porterDuffMode = mode.nativeInt;
     }
diff --git a/graphics/java/android/graphics/RuntimeXfermode.java b/graphics/java/android/graphics/RuntimeXfermode.java
new file mode 100644
index 0000000..f5a6568
--- /dev/null
+++ b/graphics/java/android/graphics/RuntimeXfermode.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2024 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.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.graphics.hwui.flags.Flags;
+
+import libcore.util.NativeAllocationRegistry;
+
+
+/**
+ * <p>A {@link RuntimeXfermode} calculates a per-pixel color based on the output of a user
+ *  * defined Android Graphics Shading Language (AGSL) function.</p>
+ *
+ * <p>This AGSL function takes in two input colors to be operated on. These colors are in sRGB
+ *  * and the output is also interpreted as sRGB. The AGSL function signature expects a single input
+ *  * of color (packed as a half4 or float4 or vec4).</p>
+ *
+ * <pre class="prettyprint">
+ * vec4 main(half4 src, half4 dst);
+ * </pre>
+ */
+@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS)
+public class RuntimeXfermode extends Xfermode {
+
+    private static class NoImagePreloadHolder {
+        public static final NativeAllocationRegistry sRegistry =
+                NativeAllocationRegistry.createMalloced(
+                        RuntimeXfermode.class.getClassLoader(), nativeGetFinalizer());
+    }
+
+    private long mBuilderNativeInstance;
+
+    /**
+     * Creates a new RuntimeBlender.
+     *
+     * @param agsl The text of AGSL color filter program to run.
+     */
+    public RuntimeXfermode(@NonNull String agsl) {
+        if (agsl == null) {
+            throw new NullPointerException("RuntimeShader requires a non-null AGSL string");
+        }
+        mBuilderNativeInstance = nativeCreateBlenderBuilder(agsl);
+        RuntimeXfermode.NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+                this, mBuilderNativeInstance);
+    }
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorInt int color) {
+        setUniform(uniformName, Color.valueOf(color).getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorLong long color) {
+        Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
+        if (color == null) {
+            throw new NullPointerException("The color parameter must not be null");
+        }
+        Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float or
+     * float[1] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value) {
+        setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec2 or
+     * float[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
+        setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec3 or
+     * float[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3) {
+        setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
+
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec4 or
+     * float[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4) {
+        setFloatUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float
+     * (for N=1), vecN, or float[N] where N is the length of the values param then an
+     * IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
+        setUniform(uniformName, values, false);
+    }
+
+    private void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values, isColor);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int or int[1]
+     * then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value) {
+        setIntUniform(uniformName, value, 0, 0, 0, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec2 or
+     * int[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
+        setIntUniform(uniformName, value1, value2, 0, 0, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec3 or
+     * int[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
+        setIntUniform(uniformName, value1, value2, value3, 0, 3);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec4 or
+     * int[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2,
+            int value3, int value4) {
+        setIntUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int (for N=1),
+     * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values);
+    }
+
+    private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3,
+            int value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    /**
+     * Assigns the uniform shader to the provided shader parameter.  If the shader program does not
+     * have a uniform shader with that name then an IllegalArgumentException is thrown.
+     *
+     * @param shaderName name matching the uniform declared in the AGSL program
+     * @param shader shader passed into the AGSL program for sampling
+     */
+    public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) {
+        if (shaderName == null) {
+            throw new NullPointerException("The shaderName parameter must not be null");
+        }
+        if (shader == null) {
+            throw new NullPointerException("The shader parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, shaderName, shader.getNativeInstance());
+    }
+
+    /**
+     * Assigns the uniform color filter to the provided color filter parameter.  If the shader
+     * program does not have a uniform color filter with that name then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param filterName name matching the uniform declared in the AGSL program
+     * @param colorFilter filter passed into the AGSL program for sampling
+     */
+    public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) {
+        if (filterName == null) {
+            throw new NullPointerException("The filterName parameter must not be null");
+        }
+        if (colorFilter == null) {
+            throw new NullPointerException("The colorFilter parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, filterName, colorFilter.getNativeInstance());
+    }
+
+    /** @hide */
+    public long createNativeInstance() {
+        return nativeCreateNativeInstance(mBuilderNativeInstance);
+    }
+
+    /** @hide */
+    private static native long nativeGetFinalizer();
+    private static native long nativeCreateBlenderBuilder(String agsl);
+    private static native long nativeCreateNativeInstance(long builder);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float[] uniforms, boolean isColor);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float value1, float value2, float value3,
+            float value4, int count);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int[] uniforms);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int value1, int value2, int value3,
+            int value4, int count);
+    private static native void nativeUpdateChild(long builder, String childName, long child);
+
+}
diff --git a/graphics/java/android/graphics/Xfermode.java b/graphics/java/android/graphics/Xfermode.java
index 6bb22a1..fb689e4 100644
--- a/graphics/java/android/graphics/Xfermode.java
+++ b/graphics/java/android/graphics/Xfermode.java
@@ -21,9 +21,6 @@
 
 package android.graphics;
 
-import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Build;
-
 /**
  * Xfermode is the base class for objects that are called to implement custom
  * "transfer-modes" in the drawing pipeline. The static function Create(Modes)
@@ -31,8 +28,4 @@
  * specified in the Modes enum. When an Xfermode is assigned to a Paint, then
  * objects drawn with that paint have the xfermode applied.
  */
-public class Xfermode {
-    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    int porterDuffMode = DEFAULT;
-}
+public class Xfermode {}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index fcb7efc..e2db2c9 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -355,6 +355,7 @@
         "jni/AnimatedImageDrawable.cpp",
         "jni/Bitmap.cpp",
         "jni/BitmapRegionDecoder.cpp",
+        "jni/RuntimeXfermode.cpp",
         "jni/BufferUtils.cpp",
         "jni/HardwareBufferHelpers.cpp",
         "jni/BitmapFactory.cpp",
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index 15b2bac..56de568 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -28,6 +28,7 @@
 extern int register_android_graphics_Bitmap(JNIEnv*);
 extern int register_android_graphics_BitmapFactory(JNIEnv*);
 extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*);
+extern int register_android_graphics_RuntimeXfermode(JNIEnv*);
 extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env);
 extern int register_android_graphics_Camera(JNIEnv* env);
 extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env);
@@ -107,6 +108,7 @@
             REG_JNI(register_android_graphics_Bitmap),
             REG_JNI(register_android_graphics_BitmapFactory),
             REG_JNI(register_android_graphics_BitmapRegionDecoder),
+            REG_JNI(register_android_graphics_RuntimeXfermode),
             REG_JNI(register_android_graphics_ByteBufferStreamAdaptor),
             REG_JNI(register_android_graphics_Camera),
             REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index da23792..a7d855d 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -906,6 +906,13 @@
         paint->setBlendMode(mode);
     }
 
+    static void setRuntimeXfermode(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle,
+                                   jlong xfermodeHandle) {
+        Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+        SkBlender* blender = reinterpret_cast<SkBlender*>(xfermodeHandle);
+        paint->setBlender(sk_ref_sp(blender));
+    }
+
     static jlong setPathEffect(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong effectHandle) {
         Paint* obj = reinterpret_cast<Paint*>(objHandle);
         SkPathEffect* effect  = reinterpret_cast<SkPathEffect*>(effectHandle);
@@ -1233,6 +1240,7 @@
         {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader},
         {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter},
         {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode},
+        {"nSetXfermode", "(JJ)V", (void*)PaintGlue::setRuntimeXfermode},
         {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect},
         {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter},
         {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface},
diff --git a/libs/hwui/jni/RuntimeXfermode.cpp b/libs/hwui/jni/RuntimeXfermode.cpp
new file mode 100644
index 0000000..c1c8964
--- /dev/null
+++ b/libs/hwui/jni/RuntimeXfermode.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#include "GraphicsJNI.h"
+#include "RuntimeEffectUtils.h"
+#include "SkBlender.h"
+
+using namespace android::uirenderer;
+
+static void SkRuntimeEffectBuilder_delete(SkRuntimeEffectBuilder* builder) {
+    delete builder;
+}
+
+static jlong RuntimeXfermode_getNativeFinalizer(JNIEnv*, jobject) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeEffectBuilder_delete));
+}
+
+static jlong RuntimeXfermode_createBuilder(JNIEnv* env, jobject, jstring sksl) {
+    ScopedUtfChars strSksl(env, sksl);
+    auto result =
+            SkRuntimeEffect::MakeForBlender(SkString(strSksl.c_str()), SkRuntimeEffect::Options{});
+    if (result.effect.get() == nullptr) {
+        doThrowIAE(env, result.errorText.c_str());
+        return 0;
+    }
+    return reinterpret_cast<jlong>(new SkRuntimeEffectBuilder(std::move(result.effect)));
+}
+
+static jlong RuntimeXfermode_create(JNIEnv* env, jobject, jlong builderPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    sk_sp<SkBlender> blender = builder->makeBlender();
+    if (!blender) {
+        doThrowIAE(env);
+    }
+    return reinterpret_cast<jlong>(blender.release());
+}
+
+static void RuntimeXfermode_updateFloatArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                     jstring uniformName, jfloatArray uniforms,
+                                                     jboolean isColor) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess);
+    UpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), isColor);
+}
+
+static void RuntimeXfermode_updateFloatUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                jstring uniformName, jfloat value1, jfloat value2,
+                                                jfloat value3, jfloat value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const float values[4] = {value1, value2, value3, value4};
+    UpdateFloatUniforms(env, builder, name.c_str(), values, count, false);
+}
+
+static void RuntimeXfermode_updateIntArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                   jstring uniformName, jintArray uniforms) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaIntArray autoValues(env, uniforms, 0);
+    UpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length());
+}
+
+static void RuntimeXfermode_updateIntUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                              jstring uniformName, jint value1, jint value2,
+                                              jint value3, jint value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const int values[4] = {value1, value2, value3, value4};
+    UpdateIntUniforms(env, builder, name.c_str(), values, count);
+}
+
+static void RuntimeXfermode_updateChild(JNIEnv* env, jobject, jlong builderPtr, jstring childName,
+                                        jlong childPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, childName);
+    auto* child = reinterpret_cast<SkFlattenable*>(childPtr);
+    if (child) {
+        UpdateChild(env, builder, name.c_str(), child);
+    }
+}
+
+static const JNINativeMethod gRuntimeXfermodeMethods[] = {
+        {"nativeGetFinalizer", "()J", (void*)RuntimeXfermode_getNativeFinalizer},
+        {"nativeCreateBlenderBuilder", "(Ljava/lang/String;)J",
+         (void*)RuntimeXfermode_createBuilder},
+        {"nativeCreateNativeInstance", "(J)J", (void*)RuntimeXfermode_create},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
+         (void*)RuntimeXfermode_updateFloatArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V",
+         (void*)RuntimeXfermode_updateFloatUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V",
+         (void*)RuntimeXfermode_updateIntArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V",
+         (void*)RuntimeXfermode_updateIntUniforms},
+        {"nativeUpdateChild", "(JLjava/lang/String;J)V", (void*)RuntimeXfermode_updateChild},
+};
+
+int register_android_graphics_RuntimeXfermode(JNIEnv* env) {
+    android::RegisterMethodsOrDie(env, "android/graphics/RuntimeXfermode", gRuntimeXfermodeMethods,
+                                  NELEM(gRuntimeXfermodeMethods));
+
+    return 0;
+}