Added prod animations from S3 (#1031)

diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/HappoSnapshotter.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/HappoSnapshotter.kt
index 3d2d386..669c838 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/HappoSnapshotter.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/HappoSnapshotter.kt
@@ -5,7 +5,8 @@
 import android.os.Build
 import android.util.Log
 import com.amazonaws.auth.BasicAWSCredentials
-import com.amazonaws.mobileconnectors.s3.transferutility.*
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferObserver
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
 import com.amazonaws.services.s3.AmazonS3Client
 import com.amazonaws.services.s3.model.CannedAccessControlList
 import com.google.gson.JsonArray
@@ -105,50 +106,10 @@
         }
     }
 
-    private suspend fun TransferUtility.uploadDeferred(key: String, file: File): TransferObserver = suspendCoroutine { continuation ->
-        val observer = transferUtility.upload(key, file, CannedAccessControlList.PublicRead)
-        val listener = object : TransferListener {
-            override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {}
-
-            override fun onError(id: Int, ex: Exception) {
-                Log.e(TAG, "$id failed uploading!", ex)
-                continuation.resumeWithException(ex)
-            }
-
-            override fun onStateChanged(id: Int, state: TransferState) {
-                when (state) {
-                    TransferState.COMPLETED -> {
-                        Log.d(TAG, "$id finished uploading.")
-                        continuation.resume(observer)
-                    }
-                    TransferState.CANCELED, TransferState.FAILED -> {
-                        Log.d(TAG, "$id failed uploading ($state).")
-                        continuation.resume(observer)
-                    }
-                    else -> Unit
-                }
-            }
-        }
-        observer.setTransferListener(listener)
+    private suspend fun TransferUtility.uploadDeferred(key: String, file: File): TransferObserver {
+        return transferUtility.upload(key, file, CannedAccessControlList.PublicRead).await()
     }
 
-    private val Bitmap.md5: String
-        get() {
-            val outputStream = ByteArrayOutputStream()
-            compress(Bitmap.CompressFormat.PNG, 100, outputStream)
-            val bytes = outputStream.toByteArray()
-            val digest = MessageDigest.getInstance("MD5")
-            digest.update(bytes, 0, bytes.size)
-            return BigInteger(1, digest.digest()).toString(16)
-        }
-
-
-    val String.md5: String
-        get() {
-            val md = MessageDigest.getInstance("MD5")
-            return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
-        }
-
     private suspend fun OkHttpClient.executeDeferred(request: Request): Response = suspendCoroutine { continuation ->
         newCall(request).enqueue(object: Callback {
             override fun onFailure(call: Call, e: IOException) {
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
index 712a90d..5a0d254 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
@@ -9,15 +9,20 @@
 import android.util.Log
 import android.view.ViewGroup
 import android.widget.ImageView
-import androidx.core.view.doOnNextLayout
 import androidx.core.view.updateLayoutParams
 import androidx.test.filters.LargeTest
 import androidx.test.rule.ActivityTestRule
 import androidx.test.rule.GrantPermissionRule
 import androidx.test.runner.AndroidJUnit4
 import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.model.LottieCompositionCache
+import com.airbnb.lottie.samples.BuildConfig
 import com.airbnb.lottie.samples.SnapshotTestActivity
 import com.airbnb.lottie.value.*
+import com.amazonaws.auth.BasicAWSCredentials
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
+import com.amazonaws.services.s3.AmazonS3Client
+import com.amazonaws.services.s3.model.S3ObjectSummary
 import kotlinx.coroutines.*
 
 import org.junit.Before
@@ -26,8 +31,10 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileInputStream
 import java.util.concurrent.TimeUnit
-import kotlin.coroutines.CoroutineContext
+import java.util.zip.ZipInputStream
 import kotlin.coroutines.resume
 import kotlin.coroutines.resumeWithException
 import kotlin.coroutines.suspendCoroutine
@@ -53,17 +60,26 @@
             Manifest.permission.READ_EXTERNAL_STORAGE
     )
 
+    private lateinit var prodAnimationsTransferUtility: TransferUtility
+
     private lateinit var snapshotter: HappoSnapshotter
 
     @Before
     fun setup() {
         snapshotter = HappoSnapshotter(activity)
+        prodAnimationsTransferUtility = TransferUtility.builder()
+                .context(activity)
+                .s3Client(AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey)))
+                .defaultBucket("lottie-prod-animations")
+                .build()
+
     }
 
     @Test
     fun testAll() {
         runBlocking {
             withTimeout(TimeUnit.MINUTES.toMillis(15)) {
+                snapshotProdAnimations()
                 snapshotAssets()
                 snapshotFrameBoundaries()
                 snapshotScaleTypes()
@@ -73,6 +89,26 @@
         }
     }
 
+    private suspend fun snapshotProdAnimations() {
+        Log.d(L.TAG, "Downloading prod animations from S3.")
+        val s3Client = AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey))
+        val objectListing = s3Client.listObjects("lottie-prod-animations")
+        objectListing.objectSummaries.forEach { snapshotProdAnimation(it) }
+    }
+
+    private suspend fun snapshotProdAnimation(objectSummary: S3ObjectSummary) {
+        val (fileName, extension) = objectSummary.key.split(".")
+        val file = File(activity.cacheDir, fileName.md5 + ".$extension")
+        prodAnimationsTransferUtility.download(objectSummary.key, file).await()
+        Log.d(L.TAG, "Downloaded ${objectSummary.key}")
+
+        val composition = parseComposition(file)
+        val bitmap = activity.snapshotFilmstrip(composition)
+        snapshotter.record(bitmap, "prod-" + objectSummary.key, "default")
+        file.delete()
+        LottieCompositionCache.getInstance().clear()
+    }
+
     private suspend fun snapshotAssets(pathPrefix: String = "") {
         activity.getAssets().list(pathPrefix)?.forEach { animation ->
             if (!animation.contains('.')) {
@@ -83,6 +119,7 @@
             val composition = parseComposition(if (pathPrefix.isEmpty()) animation else "$pathPrefix/$animation")
             val bitmap = activity.snapshotFilmstrip(composition)
             snapshotter.record(bitmap, animation, "default")
+            LottieCompositionCache.getInstance().clear()
         }
     }
 
@@ -472,6 +509,7 @@
             animationView.requestLayout()
             animationView.scale = 1f
             animationView.scaleType = ImageView.ScaleType.FIT_CENTER
+            LottieCompositionCache.getInstance().clear()
         }
     }
 
@@ -490,5 +528,22 @@
                 }
     }
 
+    private suspend fun parseComposition(file: File) = suspendCoroutine<LottieComposition> { continuation ->
+        var isResumed = false
+        val task = if (file.name.endsWith("zip")) LottieCompositionFactory.fromZipStream(ZipInputStream(FileInputStream(file)), file.name)
+                else LottieCompositionFactory.fromJsonInputStream(FileInputStream(file), file.name)
+        task
+                .addFailureListener {
+                    if (isResumed) return@addFailureListener
+                    continuation.resumeWithException(it)
+                    isResumed = true
+                }
+                .addListener {
+                    if (isResumed) return@addListener
+                    continuation.resume(it)
+                    isResumed = true
+                }
+    }
+
     private val Number.dp get() = this.toFloat() / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
 }
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/Utils.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/Utils.kt
new file mode 100644
index 0000000..72ae19a
--- /dev/null
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/Utils.kt
@@ -0,0 +1,57 @@
+package com.airbnb.lottie
+
+import android.graphics.Bitmap
+import android.util.Log
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferObserver
+import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
+import java.io.ByteArrayOutputStream
+import java.lang.Exception
+import java.math.BigInteger
+import java.security.MessageDigest
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+val Bitmap.md5: String
+    get() {
+        val outputStream = ByteArrayOutputStream()
+        compress(Bitmap.CompressFormat.PNG, 100, outputStream)
+        val bytes = outputStream.toByteArray()
+        val digest = MessageDigest.getInstance("MD5")
+        digest.update(bytes, 0, bytes.size)
+        return BigInteger(1, digest.digest()).toString(16)
+    }
+
+
+val String.md5: String
+    get() {
+        val md = MessageDigest.getInstance("MD5")
+        return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0')
+    }
+
+suspend fun TransferObserver.await() = suspendCoroutine<TransferObserver> { continuation ->
+    val listener = object : TransferListener {
+        override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {}
+
+        override fun onError(id: Int, ex: Exception) {
+            Log.e(L.TAG, "$id failed uploading!", ex)
+            continuation.resumeWithException(ex)
+        }
+
+        override fun onStateChanged(id: Int, state: TransferState) {
+            when (state) {
+                TransferState.COMPLETED -> {
+                    Log.d(L.TAG, "$id finished uploading.")
+                    continuation.resume(this@await)
+                }
+                TransferState.CANCELED, TransferState.FAILED -> {
+                    Log.d(L.TAG, "$id failed uploading ($state).")
+                    continuation.resume(this@await)
+                }
+                else -> Unit
+            }
+        }
+    }
+    setTransferListener(listener)
+}
\ No newline at end of file
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 7ef4b8f..9903fbd 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/SnapshotTestActivity.kt
@@ -43,7 +43,7 @@
         filmStripView.post {
             animationView.isVisible = false
             filmStripView.isVisible = true
-            val bitmap = Bitmap.createBitmap(filmStripView.width, filmStripView.height, Bitmap.Config.ARGB_8888)
+            val bitmap = bitmapPool.acquire(filmStripView.width, filmStripView.height)
             val canvas = Canvas(bitmap)
             filmStripView.setComposition(composition)
             filmStripView.draw(canvas)
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 068f4c9..e7370f3 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
@@ -24,6 +24,10 @@
 
     init {
         inflate(R.layout.film_strip_view)
+        animationViews.forEach {
+            @Suppress("DEPRECATION")
+            it.isDrawingCacheEnabled = false
+        }
     }
 
     fun setComposition(composition: LottieComposition) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieTask.java b/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
index 785cbc1..bc418da 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
@@ -145,7 +145,7 @@
     });
   }
 
-  private void notifySuccessListeners(T value) {
+  private synchronized void notifySuccessListeners(T value) {
     // Allows listeners to remove themselves in onResult.
     // Otherwise we risk ConcurrentModificationException.
     List<LottieListener<T>> listenersCopy = new ArrayList<>(successListeners);
@@ -197,7 +197,6 @@
       }
     };
     taskObserver.start();
-    L.debug("Starting TaskObserver thread");
   }
 
   /**
@@ -210,7 +209,6 @@
     if (successListeners.isEmpty() || result != null) {
       taskObserver.interrupt();
       taskObserver = null;
-      L.debug("Stopping TaskObserver thread");
     }
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/LottieCompositionCache.java b/lottie/src/main/java/com/airbnb/lottie/model/LottieCompositionCache.java
index c8ea8f7..3b17c81 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/LottieCompositionCache.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/LottieCompositionCache.java
@@ -37,4 +37,8 @@
     }
     cache.put(cacheKey, composition);
   }
+
+  public void clear() {
+    cache.evictAll();
+  }
 }