android_jni: Add API for frame durations

Expose frame durations for animated images in the JNI API.

GOOGLE_INTERNAL_CL: 524968419
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 2ad879d..d322c70 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
@@ -30,22 +30,31 @@
     public final int depth;
     public final int frameCount;
     public final int repetitionCount;
+    public final double frameDuration;
 
     public Image(
-        String filename, int width, int height, int depth, int frameCount, int repetitionCount) {
+        String filename,
+        int width,
+        int height,
+        int depth,
+        int frameCount,
+        int repetitionCount,
+        double frameDuration) {
       this.filename = filename;
       this.width = width;
       this.height = height;
       this.depth = depth;
       this.frameCount = frameCount;
       this.repetitionCount = repetitionCount;
+      this.frameDuration = frameDuration;
     }
   }
 
   private static final Image[] IMAGES = {
-    // Parameter ordering: filename, width, height, depth, frameCount, repetitionCount.
-    new Image("alpha_video.avif", 640, 480, 8, 48, -2),
-    new Image("Chimera-AV1-10bit-480x270.avif", 480, 270, 10, 95, -2),
+    // Parameter ordering: filename, width, height, depth, frameCount, repetitionCount,
+    // frameDuration.
+    new Image("alpha_video.avif", 640, 480, 8, 48, -2, 0.04),
+    new Image("Chimera-AV1-10bit-480x270.avif", 480, 270, 10, 95, -2, 0.04),
   };
 
   private static final String ASSET_DIRECTORY = "animated_avif";
@@ -82,10 +91,14 @@
     assertThat(decoder.getDepth()).isEqualTo(image.depth);
     assertThat(decoder.getFrameCount()).isEqualTo(image.frameCount);
     assertThat(decoder.getRepetitionCount()).isEqualTo(image.repetitionCount);
+    double[] frameDurations = decoder.getFrameDurations();
+    assertThat(frameDurations).isNotNull();
+    assertThat(frameDurations).hasLength(image.frameCount);
     Bitmap bitmap = Bitmap.createBitmap(image.width, image.height, config);
     assertThat(bitmap).isNotNull();
     for (int i = 0; i < image.frameCount; i++) {
       assertThat(decoder.nextFrame(bitmap)).isTrue();
+      assertThat(frameDurations[i]).isWithin(1.0e-2).of(image.frameDuration);
     }
     decoder.release();
   }
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 39285d8..2bb4450 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
@@ -50,6 +50,7 @@
   private int depth;
   private int frameCount;
   private int repetitionCount;
+  private double[] frameDurations;
 
   private AvifDecoder(ByteBuffer encoded) {
     decoder = createDecoder(encoded, encoded.remaining());
@@ -142,6 +143,11 @@
     return repetitionCount;
   }
 
+  /** Get the duration for each frame in the image. */
+  public double[] getFrameDurations() {
+    return frameDurations;
+  }
+
   /** Releases the underlying decoder object. */
   public void release() {
     if (decoder != 0) {
diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
index 31d6f6f..ef9d69d 100644
--- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
+++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
@@ -6,6 +6,7 @@
 #include <cpu-features.h>
 #include <jni.h>
 
+#include <memory>
 #include <new>
 
 #include "avif/avif.h"
@@ -33,6 +34,7 @@
 jfieldID global_depth;
 jfieldID global_frame_count;
 jfieldID global_repetition_count;
+jfieldID global_frame_durations;
 
 // RAII wrapper class that properly frees the decoder related objects on
 // destruction.
@@ -167,6 +169,8 @@
   global_frame_count = env->GetFieldID(avif_decoder_class, "frameCount", "I");
   global_repetition_count =
       env->GetFieldID(avif_decoder_class, "repetitionCount", "I");
+  global_frame_durations =
+      env->GetFieldID(avif_decoder_class, "frameDurations", "[D");
   return JNI_VERSION_1_6;
 }
 
@@ -187,7 +191,8 @@
   env->SetIntField(info, global_info_width, decoder.decoder->image->width);
   env->SetIntField(info, global_info_height, decoder.decoder->image->height);
   env->SetIntField(info, global_info_depth, decoder.decoder->image->depth);
-  env->SetBooleanField(info, global_info_alpha_present, decoder.decoder->alphaPresent);
+  env->SetBooleanField(info, global_info_alpha_present,
+                       decoder.decoder->alphaPresent);
   return true;
 }
 
@@ -211,21 +216,45 @@
 FUNC(jlong, createDecoder, jobject encoded, int length) {
   const uint8_t* const buffer =
       static_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
-  AvifDecoderWrapper* decoder = new (std::nothrow) AvifDecoderWrapper();
+  std::unique_ptr<AvifDecoderWrapper> decoder(new (std::nothrow)
+                                                  AvifDecoderWrapper());
   if (decoder == nullptr) {
     return 0;
   }
   // TODO(b/272577342): Make threads configurable.
-  if (!CreateDecoderAndParse(decoder, buffer, length, /*threads=*/1)) {
+  if (!CreateDecoderAndParse(decoder.get(), buffer, length, /*threads=*/1)) {
     return 0;
   }
   env->SetIntField(thiz, global_width, decoder->decoder->image->width);
   env->SetIntField(thiz, global_height, decoder->decoder->image->height);
   env->SetIntField(thiz, global_depth, decoder->decoder->image->depth);
-  env->SetIntField(thiz, global_frame_count, decoder->decoder->imageCount);
   env->SetIntField(thiz, global_repetition_count,
                    decoder->decoder->repetitionCount);
-  return reinterpret_cast<jlong>(decoder);
+  const int frameCount = decoder->decoder->imageCount;
+  env->SetIntField(thiz, global_frame_count, frameCount);
+  // This native array is needed because setting one element at a time to a Java
+  // array from the JNI layer is inefficient.
+  std::unique_ptr<double[]> native_durations(
+      new (std::nothrow) double[frameCount]);
+  if (native_durations == nullptr) {
+    return 0;
+  }
+  for (int i = 0; i < frameCount; ++i) {
+    avifImageTiming timing;
+    if (avifDecoderNthImageTiming(decoder->decoder, i, &timing) !=
+        AVIF_RESULT_OK) {
+      return 0;
+    }
+    native_durations[i] = timing.duration;
+  }
+  jdoubleArray durations = env->NewDoubleArray(frameCount);
+  if (durations == nullptr) {
+    return 0;
+  }
+  env->SetDoubleArrayRegion(durations, /*start=*/0, frameCount,
+                            native_durations.get());
+  env->SetObjectField(thiz, global_frame_durations, durations);
+  return reinterpret_cast<jlong>(decoder.release());
 }
 
 FUNC(jboolean, nextFrame, jlong jdecoder, jobject bitmap) {