android_jni: Make threads configurable

Add a variant of AvifDecoder.create that lets the users pass number
of threads explicitly.

GOOGLE_INTERNAL_CL: 525519742
diff --git a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AnimatedImageTest.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AnimatedImageTest.java
index 5851452..a368bf3 100644
--- a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AnimatedImageTest.java
+++ b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AnimatedImageTest.java
@@ -32,6 +32,7 @@
     public final int frameCount;
     public final int repetitionCount;
     public final double frameDuration;
+    public final int threads;
 
     public Image(
         String filename,
@@ -41,7 +42,8 @@
         boolean alphaPresent,
         int frameCount,
         int repetitionCount,
-        double frameDuration) {
+        double frameDuration,
+        int threads) {
       this.filename = filename;
       this.width = width;
       this.height = height;
@@ -50,14 +52,15 @@
       this.frameCount = frameCount;
       this.repetitionCount = repetitionCount;
       this.frameDuration = frameDuration;
+      this.threads = threads;
     }
   }
 
   private static final Image[] IMAGES = {
     // Parameter ordering: filename, width, height, depth, alphaPresent, frameCount,
-    // repetitionCount, frameDuration.
-    new Image("alpha_video.avif", 640, 480, 8, true, 48, -2, 0.04),
-    new Image("Chimera-AV1-10bit-480x270.avif", 480, 270, 10, false, 95, -2, 0.04),
+    // repetitionCount, frameDuration, threads.
+    new Image("alpha_video.avif", 640, 480, 8, true, 48, -2, 0.04, 1),
+    new Image("Chimera-AV1-10bit-480x270.avif", 480, 270, 10, false, 95, -2, 0.04, 2),
   };
 
   private static final String ASSET_DIRECTORY = "animated_avif";
@@ -88,7 +91,7 @@
   public void testAnimatedAvifDecode() throws IOException {
     ByteBuffer buffer = getBuffer();
     assertThat(buffer).isNotNull();
-    AvifDecoder decoder = AvifDecoder.create(buffer);
+    AvifDecoder decoder = AvifDecoder.create(buffer, image.threads);
     assertThat(decoder).isNotNull();
     assertThat(decoder.getWidth()).isEqualTo(image.width);
     assertThat(decoder.getHeight()).isEqualTo(image.height);
diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java
index 55633fe..8ada59c 100644
--- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java
+++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java
@@ -53,8 +53,8 @@
   private int repetitionCount;
   private double[] frameDurations;
 
-  private AvifDecoder(ByteBuffer encoded) {
-    decoder = createDecoder(encoded, encoded.remaining());
+  private AvifDecoder(ByteBuffer encoded, int threads) {
+    decoder = createDecoder(encoded, encoded.remaining(), threads);
   }
 
   /** Contains information about the AVIF Image. This class is only used for getInfo(). */
@@ -171,7 +171,23 @@
    */
   @Nullable
   public static AvifDecoder create(ByteBuffer encoded) {
-    AvifDecoder decoder = new AvifDecoder(encoded);
+    return create(encoded, 1);
+  }
+
+  /**
+   * Create and return an AvifDecoder with the specified number of threads.
+   *
+   * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this
+   *     ByteBuffer must be kept alive until release() is called.
+   * @param threads Number of threads to be used by the decoder. Zero means use number of CPU cores
+   *     as the thread count. Negative values are invalid. When this value is > 0, it is simply
+   *     mapped to the maxThreads parameter in libavif. For more details, see the documentation for
+   *     maxThreads variable in avif.h.
+   * @return null on failure. AvifDecoder object on success.
+   */
+  @Nullable
+  public static AvifDecoder create(ByteBuffer encoded, int threads) {
+    AvifDecoder decoder = new AvifDecoder(encoded, threads);
     return (decoder.decoder == 0) ? null : decoder;
   }
 
@@ -205,7 +221,7 @@
 
   private native boolean nthFrame(long decoder, int n, Bitmap bitmap);
 
-  private native long createDecoder(ByteBuffer encoded, int length);
+  private native long createDecoder(ByteBuffer encoded, int length, int threads);
 
   private native void destroyDecoder(long decoder);
 }
diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
index a3f887e..173ad39 100644
--- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
+++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
@@ -152,6 +152,10 @@
   return AvifImageToBitmap(env, decoder, bitmap);
 }
 
+int getThreadCount(int threads) {
+  return (threads == 0) ? android_getCpuCount() : threads;
+}
+
 }  // namespace
 
 jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {
@@ -199,15 +203,18 @@
   const uint8_t* const buffer =
       static_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
   AvifDecoderWrapper decoder;
-  if (!CreateDecoderAndParse(
-          &decoder, buffer, length,
-          (threads == 0) ? android_getCpuCount() : threads)) {
+  if (!CreateDecoderAndParse(&decoder, buffer, length,
+                             getThreadCount(threads))) {
     return false;
   }
   return DecodeNextImage(env, &decoder, bitmap);
 }
 
-FUNC(jlong, createDecoder, jobject encoded, int length) {
+FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) {
+  if (threads < 0) {
+    LOGE("Invalid value for threads (%d).", threads);
+    return 0;
+  }
   const uint8_t* const buffer =
       static_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
   std::unique_ptr<AvifDecoderWrapper> decoder(new (std::nothrow)
@@ -215,8 +222,8 @@
   if (decoder == nullptr) {
     return 0;
   }
-  // TODO(b/272577342): Make threads configurable.
-  if (!CreateDecoderAndParse(decoder.get(), buffer, length, /*threads=*/1)) {
+  if (!CreateDecoderAndParse(decoder.get(), buffer, length,
+                             getThreadCount(threads))) {
     return 0;
   }
   const jclass avif_decoder_class =