Updated testing (#1064)

diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
index 5a0d254..31a4bf9 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
@@ -22,6 +22,7 @@
 import com.amazonaws.auth.BasicAWSCredentials
 import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
 import com.amazonaws.services.s3.AmazonS3Client
+import com.amazonaws.services.s3.model.ListObjectsV2Request
 import com.amazonaws.services.s3.model.S3ObjectSummary
 import kotlinx.coroutines.*
 
@@ -78,7 +79,7 @@
     @Test
     fun testAll() {
         runBlocking {
-            withTimeout(TimeUnit.MINUTES.toMillis(15)) {
+            withTimeout(TimeUnit.MINUTES.toMillis(45)) {
                 snapshotProdAnimations()
                 snapshotAssets()
                 snapshotFrameBoundaries()
@@ -91,9 +92,25 @@
 
     private suspend fun snapshotProdAnimations() {
         Log.d(L.TAG, "Downloading prod animations from S3.")
+        val allObjects = mutableListOf<S3ObjectSummary>()
         val s3Client = AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey))
-        val objectListing = s3Client.listObjects("lottie-prod-animations")
-        objectListing.objectSummaries.forEach { snapshotProdAnimation(it) }
+        var request = ListObjectsV2Request().apply {
+            bucketName = "lottie-prod-animations"
+        }
+        var result = s3Client.listObjectsV2(request)
+        allObjects.addAll(result.objectSummaries)
+        var startAfter = result.objectSummaries.lastOrNull()?.key
+        while (startAfter != null) {
+            request = ListObjectsV2Request().apply {
+                bucketName = "lottie-prod-animations"
+                this.startAfter = startAfter
+            }
+            result = s3Client.listObjectsV2(request)
+            allObjects.addAll(result.objectSummaries)
+            startAfter = result.objectSummaries.lastOrNull()?.key
+        }
+
+        allObjects.forEach { snapshotProdAnimation(it) }
     }
 
     private suspend fun snapshotProdAnimation(objectSummary: S3ObjectSummary) {
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
index 9903fbd..c044da2 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
@@ -3,9 +3,11 @@
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Canvas
+import android.graphics.Typeface
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
+import com.airbnb.lottie.FontAssetDelegate
 import com.airbnb.lottie.ImageAssetDelegate
 import com.airbnb.lottie.LottieComposition
 import kotlinx.android.synthetic.main.activity_snapshot_tests.*
@@ -24,6 +26,11 @@
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_snapshot_tests)
         filmStripView.setImageAssetDelegate(ImageAssetDelegate { dummyBitmap })
+        filmStripView.setFontAssetDelegate(object : FontAssetDelegate() {
+            override fun getFontPath(fontFamily: String?): String {
+                return "fonts/Roboto.ttf"
+            }
+        })
         @Suppress("DEPRECATION")
         animationView.isDrawingCacheEnabled = false
     }
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
index e7370f3..dd6b356 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
@@ -6,6 +6,7 @@
 import android.widget.FrameLayout
 import androidx.annotation.FloatRange
 import androidx.core.view.children
+import com.airbnb.lottie.FontAssetDelegate
 import com.airbnb.lottie.ImageAssetDelegate
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieComposition
@@ -40,4 +41,8 @@
     fun setImageAssetDelegate(delegate: ImageAssetDelegate) {
         animationViews.forEach { it.setImageAssetDelegate(delegate) }
     }
+
+    fun setFontAssetDelegate(delegate: FontAssetDelegate) {
+        animationViews.forEach { it.setFontAssetDelegate(delegate) }
+    }
 }
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/GradientFillContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/GradientFillContent.java
index effa0fe..80e7e81 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/GradientFillContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/GradientFillContent.java
@@ -183,6 +183,9 @@
     float x1 = endPoint.x;
     float y1 = endPoint.y;
     float r = (float) Math.hypot(x1 - x0, y1 - y0);
+    if (r <= 0) {
+      r = 0.001f;
+    }
     gradient = new RadialGradient(x0, y0, r, colors, positions, Shader.TileMode.CLAMP);
     radialGradientCache.put(gradientHash, gradient);
     return gradient;
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/KeyPath.java b/lottie/src/main/java/com/airbnb/lottie/model/KeyPath.java
index 18720e9..31a50cf 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/KeyPath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/KeyPath.java
@@ -185,7 +185,7 @@
   @SuppressWarnings("SimplifiableIfStatement")
   @RestrictTo(RestrictTo.Scope.LIBRARY)
   public boolean propagateToChildren(String key, int depth) {
-    if (key.equals("__container")) {
+    if ("__container".equals(key)) {
       return true;
     }
     return depth < keys.size() - 1 || keys.get(depth).equals("**");
@@ -196,7 +196,7 @@
    * and for the contents of a ShapeLayer).
    */
   private boolean isContainer(String key) {
-    return key.equals("__container");
+    return "__container".equals(key);
   }
 
   private boolean endsWithGlobstar() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
index da0ca56..20c0ccf 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/AnimatableTransform.java
@@ -51,7 +51,8 @@
             !(position instanceof AnimatableSplitDimensionPathValue) &&
             position.isStatic() && position.getKeyframes().get(0).startValue.equals(0f, 0f) &&
             scale.isStatic() && scale.getKeyframes().get(0).startValue.equals(1f, 1f) &&
-            rotation.isStatic() && rotation.getKeyframes().get(0).startValue == 0f;
+            (rotation.isStatic() && rotation.getKeyframes().get(0).startValue == 0f ||
+                    rotation.keyframes.isEmpty());
   }
 
   public AnimatablePathValue getAnchorPoint() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/animatable/BaseAnimatableValue.java b/lottie/src/main/java/com/airbnb/lottie/model/animatable/BaseAnimatableValue.java
index 39cfa35..4f930ae 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/animatable/BaseAnimatableValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/animatable/BaseAnimatableValue.java
@@ -28,7 +28,7 @@
 
   @Override
   public boolean isStatic() {
-    return keyframes.size() == 1 && keyframes.get(0).isStatic();
+    return keyframes.isEmpty() || (keyframes.size() == 1 && keyframes.get(0).isStatic());
   }
 
   @Override public String toString() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
index 4018fb7..4824a01 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/content/ShapeData.java
@@ -63,7 +63,7 @@
         curves.add(new CubicCurveData());
       }
     } else if (curves.size() > points) {
-      for (int i = points; i < curves.size(); i++) {
+      for (int i = curves.size() - 1; i >= points; i--) {
         curves.remove(curves.size() - 1);
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
index dc5e234..7a6d750 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/AnimatableTransformParser.java
@@ -13,6 +13,7 @@
 import com.airbnb.lottie.model.animatable.AnimatableScaleValue;
 import com.airbnb.lottie.model.animatable.AnimatableTransform;
 import com.airbnb.lottie.model.animatable.AnimatableValue;
+import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.value.ScaleXY;
 
 import java.io.IOException;
@@ -58,7 +59,20 @@
         case "rz":
           composition.addWarning("Lottie doesn't support 3D layers.");
         case "r":
+          /**
+           * Sometimes split path rotation gets exported like:
+           *         "rz": {
+           *           "a": 1,
+           *           "k": [
+           *             {}
+           *           ]
+           *         },
+           * which doesn't parse to a real keyframe.
+           */
           rotation = AnimatableValueParser.parseFloat(reader, composition, false);
+          if (rotation.getKeyframes().isEmpty()) {
+            rotation.getKeyframes().add(new Keyframe(composition, 0f, 0f, null, 0f, composition.getEndFrame()));
+          }
           break;
         case "o":
           opacity = AnimatableValueParser.parseInteger(reader, composition);
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
index b0bdd9d..91d6f87 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/ScaleXYParser.java
@@ -28,4 +28,5 @@
     }
     return new ScaleXY(sx / 100f * scale, sy / 100f * scale);
   }
+
 }