<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 }