Switch back to cloud env (#2241)

diff --git a/.github/workflows/build_snapshot_tests.yml b/.github/workflows/build_snapshot_tests.yml
deleted file mode 100644
index e798e7f..0000000
--- a/.github/workflows/build_snapshot_tests.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: Build snapshot tests
-
-on:
-  pull_request:
-  push:
-    branches:
-      - master
-
-jobs:
-  build-snapshot-tests:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout the code
-        uses: actions/checkout@v3
-      - name: Setup JDK
-        uses: actions/setup-java@v2
-        with:
-          distribution: 'zulu'
-          java-version: 11
-          cache: 'gradle'
-      - name: Build app
-        run: ./gradlew snapshot-tests:assembleDebug snapshot-tests:assembleDebugAndroidTest --no-daemon
-      - name: Upload artifact
-        uses: actions/upload-artifact@v3
-        with:
-          name: apks
-          path: |
-            snapshot-tests/build/outputs/apk/androidTest/debug/snapshot-tests-debug-androidTest.apk
-            snapshot-tests/build/outputs/apk/debug/snapshot-tests-debug.apk
\ No newline at end of file
diff --git a/.github/workflows/snapshot_tests.yml b/.github/workflows/snapshot_tests.yml
deleted file mode 100644
index 3f2f1f2..0000000
--- a/.github/workflows/snapshot_tests.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-name: Snapshot tests
-
-on:
-  workflow_run:
-    workflows: ['Build snapshot tests']
-    types:
-      - completed
-
-jobs:
-  snapshot-tests:
-   runs-on: ubuntu-latest
-   steps:
-     - uses: haya14busa/action-workflow_run-status@v1
-     - name: Download artifact
-       # From https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run
-       uses: actions/github-script@v6
-       with:
-         script: |
-           let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              run_id: context.payload.workflow_run.id,
-           });
-           let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
-             return artifact.name == "apks"
-           })[0];
-           let download = await github.rest.actions.downloadArtifact({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              artifact_id: matchArtifact.id,
-              archive_format: 'zip',
-           });
-           let fs = require('fs');
-           fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/apks.zip`, Buffer.from(download.data));
-     - name: Unzip artifact
-       run: |
-         unzip apks.zip
-     - name: Run tests
-       uses: emulator-wtf/run-tests@master
-       with:
-         api-token: ${{ secrets.EW_API_TOKEN }}
-         app: debug/snapshot-tests-debug.apk
-         test: androidTest/debug/snapshot-tests-debug-androidTest.apk
-         devices: |
-           model=Pixel2,version=23
-           model=Pixel2,version=31
-         outputs-dir: build/test-results
-         directories-to-pull: /sdcard/Download/
-     - name: Check files
-       run: |
-         ls -ltR build/test-results/Pixel2_api31/sdcard/Download
-         ls -ltR build/test-results/Pixel2_api23/sdcard/Download
-     - name: 'Post PR comment'
-       uses: mshick/add-pr-comment@v2
-       if: github.event.workflow_run.event == 'pull_request'
-       with:
-         message-id: ${{ github.sha }}
-         message: |
-           **Snapshot Tests**
-           **API 23**: [Report](https://happo.io/a/27/report/${{ github.sha }}-android23) [Diff](https://happo.io/a/27/p/27/compare/master-android23/${{ github.sha }}-android23)
-           **API 31**: [Report](https://happo.io/a/27/report/${{ github.sha }}-android31) [Diff](https://happo.io/a/27/p/27/compare/master-android31/${{ github.sha }}-android31)
-     - name: "Deploy Snapshot"
-       env:
-         SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
-         SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
-       run: ./deploy_snapshot.sh
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index f2dedf6..bf8ee9a 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -55,4 +55,60 @@
         uses: actions/upload-artifact@v2
         with:
           name: unit_test_reports
-          path: reports.zip
\ No newline at end of file
+          path: reports.zip
+  snapshot-tests:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout the code
+        uses: actions/checkout@v3
+      - name: Setup env
+        shell: bash
+        run: |
+          curl https://us-central1-lottie-snapshots.cloudfunctions.net/snapshot-env-v1/emulator > snapshot-env
+          while read line; do
+            echo "$line" >> $GITHUB_ENV
+          done < snapshot-env
+      - name: Setup JDK
+        uses: actions/setup-java@v2
+        with:
+          distribution: 'zulu'
+          java-version: 11
+          cache: 'gradle'
+      - name: Build app
+        run: ./gradlew snapshot-tests:assembleDebug snapshot-tests:assembleDebugAndroidTest --no-daemon
+      - name: Run tests
+        uses: emulator-wtf/run-tests@master
+        with:
+          api-token: ${{ env.EW_API_TOKEN }}
+          app: snapshot-tests/build/outputs/apk/debug/snapshot-tests-debug.apk
+          test: snapshot-tests/build/outputs/apk/androidTest/debug/snapshot-tests-debug-androidTest.apk
+          devices: |
+            model=Pixel2,version=23
+            model=Pixel2,version=31
+          outputs-dir: build/test-results
+      - uses: mshick/add-pr-comment@v2
+        if: github.event_name == 'pull_request'
+        with:
+          message-id: ${{ github.sha }}
+          message: |
+            **Snapshot Tests**
+            **API 23**: [Report](https://happo.io/a/27/report/${{ github.sha }}-android23) [Diff](https://happo.io/a/27/p/27/compare/master-android23/${{ github.sha }}-android23)
+            **API 31**: [Report](https://happo.io/a/27/report/${{ github.sha }}-android31) [Diff](https://happo.io/a/27/p/27/compare/master-android31/${{ github.sha }}-android31)
+  deploy:
+    if: github.event_name == 'push' && github.repository == 'airbnb/lottie-android' && github.ref == 'refs/heads/master'
+    runs-on: ubuntu-latest
+    needs: [lint, unit-test, gradle-wrapper, snapshot-tests]
+    steps:
+      - name: Checkout the code
+        uses: actions/checkout@v3
+      - name: Setup JDK
+        uses: actions/setup-java@v2
+        with:
+          distribution: 'zulu'
+          java-version: 11
+          cache: 'gradle'
+      - name: "Deploy Snapshot"
+        env:
+          SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
+          SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+        run: ./deploy_snapshot.sh
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..4515aa3
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="1.6.10" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/snapshot-tests/build.gradle b/snapshot-tests/build.gradle
index 80fd816..d3beaef 100644
--- a/snapshot-tests/build.gradle
+++ b/snapshot-tests/build.gradle
@@ -9,7 +9,7 @@
   defaultConfig {
     applicationId "com.airbnb.lottie.snapshots"
     minSdk 21
-    targetSdk 29
+    targetSdk 30
     versionCode 1
     versionName VERSION_NAME
     testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -39,7 +39,7 @@
     freeCompilerArgs += [
         "-Xallow-jvm-ir-dependencies",
         "-Xskip-prerelease-check",
-        "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
+        "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
         "-Xopt-in=kotlin.RequiresOptIn",
     ]
   }
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
index 51a7b6a..3341722 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
@@ -6,7 +6,6 @@
 import android.content.res.Configuration
 import android.util.Log
 import android.widget.FrameLayout
-import androidx.compose.animation.core.snap
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -44,6 +43,9 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import org.json.JSONObject
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -61,11 +63,13 @@
     @get:Rule
     val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
         Manifest.permission.WRITE_EXTERNAL_STORAGE,
-        Manifest.permission.READ_EXTERNAL_STORAGE,
+        Manifest.permission.READ_EXTERNAL_STORAGE
     )
 
     lateinit var testCaseContext: SnapshotTestCaseContext
     lateinit var snapshotter: HappoSnapshotter
+    private lateinit var s3AccessKey: String
+    private lateinit var s3SecretKey: String
 
     @Before
     fun setup() {
@@ -76,12 +80,28 @@
                 .build()
         )
         val context = ApplicationProvider.getApplicationContext<Context>()
-        snapshotter = HappoSnapshotter(context) { name, variant ->
+        s3AccessKey = BuildConfig.S3AccessKey
+        s3SecretKey = BuildConfig.S3SecretKey
+        var happoApiKey = BuildConfig.HappoApiKey
+        var happoSecretKey = BuildConfig.HappoSecretKey
+        @Suppress("KotlinConstantConditions")
+        if (BuildConfig.S3AccessKey == "" || BuildConfig.S3AccessKey == "null") {
+            val client = OkHttpClient()
+            val request = Request.Builder()
+                .url("https://us-central1-lottie-snapshots.cloudfunctions.net/snapshot-env-v1/snapshots")
+                .build()
+            val response = client.newCall(request).execute()
+            val json = JSONObject(response.body?.string() ?: "{}")
+            s3AccessKey = json.getString("LOTTIE_S3_API_KEY")
+            s3SecretKey = json.getString("LOTTIE_S3_SECRET_KEY")
+            happoApiKey = json.getString("LOTTIE_HAPPO_API_KEY")
+            happoSecretKey = json.getString("LOTTIE_HAPPO_SECRET_KEY")
+        }
+        snapshotter = HappoSnapshotter(context, s3AccessKey, s3SecretKey, happoApiKey, happoSecretKey) { name, variant ->
             snapshotActivityRule.scenario.onActivity { activity ->
                 activity.updateUiForSnapshot(name, variant)
             }
         }
-        snapshotter.setupCacheDir()
         testCaseContext = object : SnapshotTestCaseContext {
             override val context: Context = context
             override val snapshotter: HappoSnapshotter = this@LottieSnapshotTest.snapshotter
@@ -120,26 +140,26 @@
     fun testAll() = runBlocking {
         val testCases = listOf(
             CustomBoundsTestCase(),
-//            ColorStateListColorFilterTestCase(),
-//            FailureTestCase(),
-//            FrameBoundariesTestCase(),
-//            ScaleTypesTestCase(),
-//            ComposeScaleTypesTestCase(),
-//            DynamicPropertiesTestCase(),
-//            MarkersTestCase(),
-//            AssetsTestCase(),
-//            TextTestCase(),
-//            PartialFrameProgressTestCase(),
-//            NightModeTestCase(),
-//            ApplyOpacityToLayerTestCase(),
-//            OutlineMasksAndMattesTestCase(),
-//            LargeCompositionSoftwareRendering(),
-//            ComposeDynamicPropertiesTestCase(),
-//            ProdAnimationsTestCase(),
-//            ClipChildrenTestCase(),
-//            SoftwareRenderingDynamicPropertiesInvalidationTestCase(),
-//            SeekBarTestCase(),
-//            CompositionFrameRate(),
+            ColorStateListColorFilterTestCase(),
+            FailureTestCase(),
+            FrameBoundariesTestCase(),
+            ScaleTypesTestCase(),
+            ComposeScaleTypesTestCase(),
+            DynamicPropertiesTestCase(),
+            MarkersTestCase(),
+            AssetsTestCase(),
+            TextTestCase(),
+            PartialFrameProgressTestCase(),
+            NightModeTestCase(),
+            ApplyOpacityToLayerTestCase(),
+            OutlineMasksAndMattesTestCase(),
+            LargeCompositionSoftwareRendering(),
+            ComposeDynamicPropertiesTestCase(),
+            ProdAnimationsTestCase(s3AccessKey, s3SecretKey),
+            ClipChildrenTestCase(),
+            SoftwareRenderingDynamicPropertiesInvalidationTestCase(),
+            SeekBarTestCase(),
+            CompositionFrameRate(),
         )
 
         withTimeout(TimeUnit.MINUTES.toMillis(45)) {
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
index 903d319..56d280f 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
@@ -25,11 +25,7 @@
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.zip.ZipInputStream
 
-/**
- * TODO:
- * prod-com.eharmony-lottie-loader-data
- */
-class ProdAnimationsTestCase : SnapshotTestCase {
+class ProdAnimationsTestCase(private val s3AccessKey: String, private val s3SecretKey: String) : SnapshotTestCase {
     private val filesChannel = Channel<File>(capacity = 2_048)
 
     override suspend fun SnapshotTestCaseContext.run() = coroutineScope {
@@ -45,8 +41,7 @@
         }
     }
 
-    @Suppress("BlockingMethodInNonBlockingContext")
-    fun CoroutineScope.parseCompositions(files: ReceiveChannel<File>) = produce(
+    private fun CoroutineScope.parseCompositions(files: ReceiveChannel<File>) = produce(
         context = Dispatchers.IO,
         capacity = 50,
     ) {
@@ -63,7 +58,7 @@
     suspend fun SnapshotTestCaseContext.downloadAnimations() = coroutineScope {
         val transferUtility = TransferUtility.builder()
             .context(context)
-            .s3Client(AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey)))
+            .s3Client(AmazonS3Client(BasicAWSCredentials(s3AccessKey, s3SecretKey)))
             .defaultBucket("lottie-prod-animations")
             .build()
 
@@ -92,7 +87,7 @@
 
     private fun fetchAllObjects(bucket: String): List<S3ObjectSummary> {
         val allObjects = mutableListOf<S3ObjectSummary>()
-        val s3Client = AmazonS3Client(BasicAWSCredentials(BuildConfig.S3AccessKey, BuildConfig.S3SecretKey))
+        val s3Client = AmazonS3Client(BasicAWSCredentials(s3AccessKey, s3SecretKey))
         var request = ListObjectsV2Request().apply {
             bucketName = bucket
         }
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/HappoSnapshotter.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/HappoSnapshotter.kt
index 4c68723..82ea63a 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/HappoSnapshotter.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/HappoSnapshotter.kt
@@ -2,24 +2,36 @@
 
 import android.content.Context
 import android.graphics.Bitmap
-import android.os.Environment
+import android.os.Build
 import android.util.Log
 import com.airbnb.lottie.L
+import com.airbnb.lottie.snapshots.BuildConfig
+import com.amazonaws.auth.BasicAWSCredentials
+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
+import com.google.gson.JsonElement
 import com.google.gson.JsonObject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.BufferedInputStream
-import java.io.BufferedOutputStream
+import kotlinx.coroutines.*
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody.Companion.toRequestBody
 import java.io.ByteArrayOutputStream
 import java.io.File
-import java.io.FileInputStream
 import java.io.FileOutputStream
+import java.io.IOException
 import java.math.BigInteger
+import java.net.URLEncoder
+import java.nio.charset.Charset
 import java.security.MessageDigest
-import java.util.UUID
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
+import java.util.*
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+private const val TAG = "HappoSnapshotter"
 
 /**
  * Use this class to record Bitmap snapshots and upload them to happo.
@@ -30,42 +42,36 @@
  */
 class HappoSnapshotter(
     private val context: Context,
+    s3AccessKey: String,
+    s3SecretKey: String,
+    private val happoApiKey: String,
+    private val happoSecretKey: String,
     private val onSnapshotRecorded: (snapshotName: String, snapshotVariant: String) -> Unit,
 ) {
-    private val bucket = "lottie-happo"
-    private val cacheDir by lazy {
-        val file = File("/sdcard/Download", "lottie")
-        if (!file.exists()) {
-            if (!file.mkdirs()) {
-                throw IllegalStateException("Unable to make cache dir.")
-            }
-        }
-        file
-    }
-    private val snapshotTempDir by lazy {
-        val file = File(cacheDir, "snapshots-temp")
-        if (!file.exists()) {
-            if (!file.mkdirs()) {
-                throw IllegalStateException("Unable to make cache dir.")
-            }
-        }
-        file
-    }
-    private val snapshotDir by lazy {
-        val file = File(cacheDir, "snapshots")
-        if (!file.exists()) {
-            if (!file.mkdirs()) {
-                throw IllegalStateException("Unable to make cache dir.")
-            }
-        }
-        file
-    }
+    private val recordJob = Job()
+    private val recordScope = CoroutineScope(Dispatchers.IO + recordJob)
 
+    private val bucket = "lottie-happo"
+    private val gitBranch = URLEncoder.encode((BuildConfig.GIT_BRANCH).replace("/", "_"), "UTF-8")
+    private val androidVersion = "android${Build.VERSION.SDK_INT}"
+    private val reportNamePrefixes = listOf(BuildConfig.GIT_SHA, gitBranch, BuildConfig.VERSION_NAME).filter { it.isNotBlank() }
+
+    // Use this when running snapshots locally.
+    // private val reportNamePrefixes = listOf(System.currentTimeMillis().toString()).filter { it.isNotBlank() }
+    private val reportNames = reportNamePrefixes.map { "$it-$androidVersion" }
+
+    private val okhttp = OkHttpClient()
+
+    private val transferUtility = TransferUtility.builder()
+        .context(context)
+        .s3Client(AmazonS3Client(BasicAWSCredentials(s3AccessKey, s3SecretKey)))
+        .defaultBucket(bucket)
+        .build()
     private val snapshots = mutableListOf<Snapshot>()
 
     suspend fun record(bitmap: Bitmap, animationName: String, variant: String) = withContext(Dispatchers.IO) {
         val tempUuid = UUID.randomUUID().toString()
-        val file = File(snapshotTempDir, "$tempUuid.png")
+        val file = File(context.cacheDir, "$tempUuid.png")
 
         val fileOutputStream = FileOutputStream(file)
         val byteOutputStream = ByteArrayOutputStream()
@@ -74,12 +80,10 @@
         bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
         val md5 = byteOutputStream.toByteArray().md5
         val key = "snapshots/$md5.png"
-        val md5File = File(snapshotDir, "$md5.png")
-        if (!file.renameTo(md5File)) {
-            throw IllegalStateException("Unable to rename ${file.absolutePath} to ${md5File.absolutePath}")
-        }
-        Log.d("Gabe", "Renamed file to ${md5File.absolutePath}")
+        val md5File = File(context.cacheDir, "$md5.png")
+        file.renameTo(md5File)
 
+        recordScope.launch { uploadDeferred(key, md5File) }
         Log.d(L.TAG, "Adding snapshot for $animationName-$variant")
         synchronized(snapshots) {
             snapshots += Snapshot(bucket, key, bitmap.width, bitmap.height, animationName, variant)
@@ -87,15 +91,16 @@
         onSnapshotRecorded(animationName, variant)
     }
 
-    fun setupCacheDir() {
-        val files = cacheDir.listFiles() ?: return
-        for (file in files) {
-            file.deleteRecursively()
-        }
-    }
-
-    fun finalizeReportAndUpload() {
+    suspend fun finalizeReportAndUpload() {
         val recordJobStart = System.currentTimeMillis()
+        fun Job.activeJobs() = children.filter { it.isActive }.count()
+        var activeJobs = recordJob.activeJobs()
+        while (activeJobs > 0) {
+            activeJobs = recordJob.activeJobs()
+            Log.d(L.TAG, "Waiting for record $activeJobs jobs to finish.")
+            delay(1000)
+        }
+        recordJob.children.forEach { it.join() }
         Log.d(L.TAG, "Waited ${System.currentTimeMillis() - recordJobStart}ms for recordings to finish saving.")
         val json = JsonObject()
         val snaps = JsonArray()
@@ -104,11 +109,44 @@
             snaps.add(s.toJson())
         }
         Log.d(L.TAG, "Finished creating snapshot report")
-        val reportFile = File(cacheDir, "report.json")
-        FileOutputStream(reportFile).use { fos ->
-            fos.write(json.toString().toByteArray())
+        reportNames.forEach { reportName ->
+            Log.d(L.TAG, "Uploading $reportName")
+            upload(reportName, json)
         }
-        createZip()
+    }
+
+    private suspend fun upload(reportName: String, json: JsonElement) {
+        val body = json.toString().toRequestBody("application/json".toMediaType())
+        val request = Request.Builder()
+            .addHeader("Authorization", Credentials.basic(happoApiKey, happoSecretKey, Charset.forName("UTF-8")))
+            .url("https://happo.io/api/reports/$reportName")
+            .post(body)
+            .build()
+
+        val response = okhttp.executeDeferred(request)
+        if (response.isSuccessful) {
+            Log.d(TAG, "Uploaded $reportName to happo")
+        } else {
+            throw IllegalStateException("Failed to upload $reportName to Happo. Failed with code ${response.code}. " + response.body?.string())
+        }
+    }
+
+    private suspend fun uploadDeferred(key: String, file: File): TransferObserver {
+        return retry { _, _ ->
+            transferUtility.upload(key, file, CannedAccessControlList.PublicRead).await()
+        }
+    }
+
+    private suspend fun OkHttpClient.executeDeferred(request: Request): Response = suspendCoroutine { continuation ->
+        newCall(request).enqueue(object : Callback {
+            override fun onFailure(call: Call, e: IOException) {
+                continuation.resumeWithException(e)
+            }
+
+            override fun onResponse(call: Call, response: Response) {
+                continuation.resume(response)
+            }
+        })
     }
 
     private val ByteArray.md5: String
@@ -117,20 +155,4 @@
             digest.update(this, 0, this.size)
             return BigInteger(1, digest.digest()).toString(16)
         }
-
-    private fun createZip() {
-        val files = (snapshotDir.listFiles() ?: emptyArray()) + File(cacheDir, "report.json")
-        ZipOutputStream(BufferedOutputStream(FileOutputStream("/sdcard/Download/snapshots.zip"))).use { out ->
-            for (file in files) {
-                FileInputStream(file).use { fi ->
-                    BufferedInputStream(fi).use { origin ->
-                        val entryName = file.absolutePath.substring(file.absolutePath.lastIndexOf("/"))
-                        val entry = ZipEntry(entryName)
-                        out.putNextEntry(entry)
-                        origin.copyTo(out, 1024)
-                    }
-                }
-            }
-        }
-    }
 }
\ No newline at end of file
diff --git a/snapshot-tests/src/main/AndroidManifest.xml b/snapshot-tests/src/main/AndroidManifest.xml
index 3eca9ac..0004b3b 100644
--- a/snapshot-tests/src/main/AndroidManifest.xml
+++ b/snapshot-tests/src/main/AndroidManifest.xml
@@ -1,26 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
     package="com.airbnb.lottie.snapshots">
-    <uses-permission
-        android:name="android.permission.READ_EXTERNAL_STORAGE"
-        android:maxSdkVersion="31"
-        tools:ignore="ScopedStorage" />
-    <uses-permission
-        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
-        android:maxSdkVersion="31"
-        tools:ignore="ScopedStorage" />
 
-    <uses-permission
-        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
-        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
         android:largeHeap="true"
-        android:requestLegacyExternalStorage="true"
         android:roundIcon="@mipmap/ic_launcher"
         android:theme="@style/Theme.LottieCompose">