• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.airbnb.lottie.samples
2 
3 import android.content.Context
4 import android.graphics.Bitmap
5 import android.os.Build
6 import android.util.Log
7 import com.airbnb.lottie.BuildConfig
8 import com.airbnb.lottie.L
9 import com.amazonaws.auth.BasicAWSCredentials
10 import com.amazonaws.mobileconnectors.s3.transferutility.TransferObserver
11 import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
12 import com.amazonaws.services.s3.AmazonS3Client
13 import com.amazonaws.services.s3.model.CannedAccessControlList
14 import com.google.gson.JsonArray
15 import com.google.gson.JsonElement
16 import com.google.gson.JsonObject
17 import kotlinx.coroutines.CoroutineScope
18 import kotlinx.coroutines.Dispatchers
19 import kotlinx.coroutines.Job
20 import kotlinx.coroutines.async
21 import kotlinx.coroutines.delay
22 import kotlinx.coroutines.withContext
23 import okhttp3.Call
24 import okhttp3.Callback
25 import okhttp3.Credentials
26 import okhttp3.MediaType
27 import okhttp3.OkHttpClient
28 import okhttp3.Request
29 import okhttp3.RequestBody
30 import okhttp3.Response
31 import java.io.File
32 import java.io.FileOutputStream
33 import java.io.IOException
34 import java.net.URLEncoder
35 import java.nio.charset.Charset
36 import kotlin.coroutines.CoroutineContext
37 import kotlin.coroutines.resume
38 import kotlin.coroutines.resumeWithException
39 import kotlin.coroutines.suspendCoroutine
40 import com.airbnb.lottie.samples.BuildConfig as BC
41 
42 private const val TAG = "HappotSnapshotter"
43 
44 /**
45  * Use this class to record Bitmap snapshots and upload them to happo.
46  *
47  * To use it:
48  *    1) Call record with each bitmap you want to save
49  *    2) Call finalizeAndUpload
50  */
51 class HappoSnapshotter(
52         private val context: Context
53 ) {
54     private val recordJob = Job()
55     val recordContext: CoroutineContext
56         get() = Dispatchers.IO + recordJob
57     val recordScope = CoroutineScope(recordContext)
58 
59     private val bucket = "lottie-happo"
60     private val happoApiKey = BC.HappoApiKey
61     private val happoSecretKey = BC.HappoSecretKey
62     private val gitBranch = URLEncoder.encode((if (BC.BITRISE_GIT_BRANCH == "null") BC.GIT_BRANCH else BC.BITRISE_GIT_BRANCH).replace("/", "_"), "UTF-8")
63     private val androidVersion = "android${Build.VERSION.SDK_INT}"
64     private val reportNamePrefixes = listOf(BC.GIT_SHA, gitBranch, BuildConfig.VERSION_NAME).filter { it.isNotBlank() }
65     private val reportNames = reportNamePrefixes.map { "$it-$androidVersion" }
66 
67     private val okhttp = OkHttpClient()
68 
69     private val transferUtility = TransferUtility.builder()
70             .context(context)
71             .s3Client(AmazonS3Client(BasicAWSCredentials(BC.S3AccessKey, BC.S3SecretKey)))
72             .defaultBucket(bucket)
73             .build()
74     private val snapshots = mutableListOf<Snapshot>()
75 
76     suspend fun record(bitmap: Bitmap, animationName: String, variant: String) = withContext(Dispatchers.IO) {
77         val md5 = bitmap.md5
78         val key = "snapshots/$md5.png"
79         val file = File(context.cacheDir, "$md5.png")
80         val outputStream = FileOutputStream(file)
81         bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
82         recordScope.async { transferUtility.uploadDeferred(key, file) }
83         snapshots += Snapshot(bucket, key, bitmap.width, bitmap.height, animationName, variant)
84     }
85 
86     suspend fun finalizeReportAndUpload() {
87         val recordJobStart = System.currentTimeMillis()
88         fun Job.activeJobs() = children.filter { it.isActive }.count()
89         var activeJobs = recordJob.activeJobs()
90         while (activeJobs > 0) {
91             activeJobs = recordJob.activeJobs()
92             Log.d(L.TAG, "Waiting for record $activeJobs jobs to finish.")
93             delay(1000)
94         }
95         recordJob.children.forEach { it.join() }
96         Log.d(L.TAG, "Waited ${System.currentTimeMillis() - recordJobStart}ms for recordings to finish saving.")
97         val json = JsonObject()
98         val snaps = JsonArray()
99         json.add("snaps", snaps)
100         snapshots.forEach {
101             snaps.add(it.toJson())
102         }
103         reportNames.forEach { upload(it, json) }
104     }
105 
106     private suspend fun upload(reportName: String, json: JsonElement) {
107         val body = RequestBody.create(MediaType.get("application/json"), json.toString())
108         val request = Request.Builder()
109                 .addHeader("Authorization", Credentials.basic(happoApiKey, happoSecretKey, Charset.forName("UTF-8")))
110                 .url("https://happo.io/api/reports/$reportName")
111                 .post(body)
112                 .build()
113 
114         val response = okhttp.executeDeferred(request)
115         if (response.isSuccessful) {
116             Log.d(TAG, "Uploaded $reportName to happo")
117         } else {
118             throw IllegalStateException("Failed to upload $reportName to Happo. Failed with code ${response.code()}. " + response.body()?.string())
119         }
120     }
121 
122     private suspend fun TransferUtility.uploadDeferred(key: String, file: File): TransferObserver {
123         return transferUtility.upload(key, file, CannedAccessControlList.PublicRead).await()
124     }
125 
126     private suspend fun OkHttpClient.executeDeferred(request: Request): Response = suspendCoroutine { continuation ->
127         newCall(request).enqueue(object : Callback {
128             override fun onFailure(call: Call, e: IOException) {
129                 continuation.resumeWithException(e)
130             }
131 
132             override fun onResponse(call: Call, response: Response) {
133                 continuation.resume(response)
134             }
135         })
136     }
137 }