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();
+ }
}