Update jumbomoji processing and downloading.

This commit is contained in:
Cody Henthorne
2022-01-21 10:31:43 -05:00
committed by GitHub
parent 2b021f5237
commit bfdedd57d1
21 changed files with 351 additions and 54 deletions

View File

@@ -4,10 +4,12 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
import com.mobilecoin.lib.util.Hex;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.UUID;
@@ -44,6 +46,16 @@ public class EmojiDownloader {
() -> new EmojiFiles.Name(imagePath, UUID.randomUUID()));
}
public static void streamFileFromRemote(@NonNull EmojiFiles.Version version,
@NonNull String bucket,
@NonNull String path,
@NonNull Consumer<InputStream> streamConsumer)
throws IOException
{
streamFromRemote(() -> EmojiRemote.getObject(new EmojiFileRequest(version.getVersion(), bucket, path)),
streamConsumer);
}
private static @NonNull EmojiFiles.Name downloadAndVerifyFromRemote(@NonNull Context context,
@NonNull EmojiFiles.Version version,
@NonNull Producer<Response> responseProducer,
@@ -90,6 +102,23 @@ public class EmojiDownloader {
}
}
private static void streamFromRemote(@NonNull Producer<Response> responseProducer,
@NonNull Consumer<InputStream> streamConsumer) throws IOException
{
try (Response response = responseProducer.produce()) {
if (!response.isSuccessful()) {
throw new IOException("Unsuccessful response " + response.code());
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
throw new IOException("No response body");
}
streamConsumer.accept(Okio.buffer(responseBody.source()).inputStream());
}
}
private static @Nullable String getMD5FromResponse(@NonNull Response response) {
Pattern pattern = Pattern.compile(".*([a-f0-9]{32}).*");
String header = response.header("etag");

View File

@@ -18,6 +18,7 @@ typealias UriFactory = (sprite: String, format: String) -> Uri
*/
object EmojiJsonParser {
private val OBJECT_MAPPER = ObjectMapper()
private const val ESTIMATED_EMOJI_COUNT = 3500
@JvmStatic
fun verify(body: InputStream) {
@@ -36,11 +37,12 @@ object EmojiJsonParser {
val format: String = node["format"].textValue()
val obsolete: List<ObsoleteEmoji> = node["obsolete"].toObseleteList()
val dataPages: List<EmojiPageModel> = getDataPages(format, node["emoji"], uriFactory)
val jumboPages: Map<String, String> = getJumboPages(node["jumbomoji"])
val displayPages: List<EmojiPageModel> = mergeToDisplayPages(dataPages)
val metrics: EmojiMetrics = node["metrics"].toEmojiMetrics()
val densities: List<String> = node["densities"].toDensityList()
return ParsedEmojiData(metrics, densities, format, displayPages, dataPages, obsolete)
return ParsedEmojiData(metrics, densities, format, displayPages, dataPages, jumboPages, obsolete)
}
private fun getDataPages(format: String, emoji: JsonNode, uriFactory: UriFactory): List<EmojiPageModel> {
@@ -64,13 +66,33 @@ object EmojiJsonParser {
.toList()
}
private fun getJumboPages(jumbo: JsonNode?): Map<String, String> {
if (jumbo != null) {
return jumbo.fields()
.asSequence()
.map { (page: String, node: JsonNode) ->
node.associate { it.textValue() to page }
}
.flatMap { it.entries }
.associateTo(HashMap(ESTIMATED_EMOJI_COUNT)) { it.key to it.value }
}
return emptyMap()
}
private fun createPage(pageName: String, format: String, page: JsonNode, uriFactory: UriFactory): EmojiPageModel {
val category = EmojiCategory.forKey(pageName.asCategoryKey())
val pageList = page.mapIndexed { i, data ->
if (data.size() == 0) {
throw IllegalStateException("Page index $pageName.$i had no data")
} else {
Emoji(data.map { it.textValue().encodeAsUtf16() })
val variations: MutableList<String> = mutableListOf()
val rawVariations: MutableList<String> = mutableListOf()
data.forEach {
variations += it.textValue().encodeAsUtf16()
rawVariations += it.textValue()
}
Emoji(variations, rawVariations)
}
}
@@ -111,5 +133,6 @@ data class ParsedEmojiData(
override val format: String,
override val displayPages: List<EmojiPageModel>,
override val dataPages: List<EmojiPageModel>,
override val jumboPages: Map<String, String>,
override val obsolete: List<ObsoleteEmoji>
) : EmojiData

View File

@@ -6,7 +6,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.io.IOException
private const val VERSION_URL = "https://updates.signal.org/dynamic/android/emoji/version_v2.txt"
private const val VERSION_URL = "https://updates.signal.org/dynamic/android/emoji/version_v3.txt"
private const val BASE_STATIC_BUCKET_URL = "https://updates.signal.org/static/android/emoji"
/**
@@ -93,3 +93,11 @@ class EmojiImageRequest(
) : EmojiRequest {
override val url: String = "$BASE_STATIC_BUCKET_URL/$version/$density/$name.$format"
}
class EmojiFileRequest(
version: Int,
density: String,
name: String,
) : EmojiRequest {
override val url: String = "$BASE_STATIC_BUCKET_URL/$version/$density/$name"
}

View File

@@ -62,8 +62,13 @@ class EmojiSource(
.filter { it.spriteUri != null }
.forEach { page ->
val emojiPage = emojiPageFactory(page.spriteUri!!)
page.emoji.forEachIndexed { idx, emoji ->
tree.add(emoji, EmojiDrawInfo(emojiPage, idx, emoji))
var overallIndex = 0
page.displayEmoji.forEach { emoji: Emoji ->
emoji.variations.forEachIndexed { variationIndex, variation ->
val raw = emoji.getRawVariation(variationIndex)
tree.add(variation, EmojiDrawInfo(emojiPage, overallIndex++, variation, raw, jumboPages[raw]))
}
}
}
@@ -142,6 +147,7 @@ interface EmojiData {
val format: String
val displayPages: List<EmojiPageModel>
val dataPages: List<EmojiPageModel>
val jumboPages: Map<String, String>
val obsolete: List<ObsoleteEmoji>
}

View File

@@ -3,16 +3,22 @@ package org.thoughtcrime.securesms.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.SystemClock
import androidx.annotation.MainThread
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo
import org.thoughtcrime.securesms.emoji.protos.JumbomojiPack
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.ListenableFutureTask
import org.thoughtcrime.securesms.util.SoftHashMap
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
import java.io.IOException
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(JumboEmoji::class.java)
@@ -21,30 +27,67 @@ private val TAG = Log.tag(JumboEmoji::class.java)
*/
object JumboEmoji {
private val executor = ThreadUtil.trace(SignalExecutors.newCachedSingleThreadExecutor("jumbo-emoji"))
const val MAX_JUMBOJI_COUNT = 5
private const val JUMBOMOJI_SUPPORTED_VERSION = 5
private val cache: MutableMap<String, Bitmap> = SoftHashMap(16)
private val tasks: MutableMap<String, ListenableFutureTask<Bitmap>> = hashMapOf()
private val versionToFormat: MutableMap<UUID, String?> = hashMapOf()
private val downloadedJumbos: MutableSet<String> = mutableSetOf()
private val networkCheckThrottle: Long = TimeUnit.MINUTES.toMillis(1)
private var lastNetworkCheck: Long = 0
private var canDownload: Boolean = false
@Volatile
private var currentVersion: Int = -1
@JvmStatic
@MainThread
fun updateCurrentVersion(context: Context) {
SignalExecutors.BOUNDED.execute {
val version: EmojiFiles.Version = EmojiFiles.Version.readVersion(context, true) ?: return@execute
if (EmojiFiles.getLatestEmojiData(context, version)?.format != null) {
currentVersion = version.version
ThreadUtil.runOnMain { downloadedJumbos.addAll(SignalStore.emojiValues().getJumboEmojiSheets(version.version)) }
}
}
}
@JvmStatic
@MainThread
fun canDownloadJumbo(context: Context): Boolean {
val now = SystemClock.elapsedRealtime()
if (now - networkCheckThrottle > lastNetworkCheck) {
canDownload = AutoDownloadEmojiConstraint.canAutoDownloadJumboEmoji(context)
lastNetworkCheck = now
}
return canDownload && currentVersion >= JUMBOMOJI_SUPPORTED_VERSION
}
@JvmStatic
@MainThread
fun hasJumboEmoji(drawInfo: EmojiDrawInfo): Boolean {
return downloadedJumbos.contains(drawInfo.jumboSheet)
}
@Suppress("FoldInitializerAndIfToElvis")
@JvmStatic
@MainThread
fun loadJumboEmoji(context: Context, rawEmoji: String): LoadResult {
fun loadJumboEmoji(context: Context, drawInfo: EmojiDrawInfo): LoadResult {
val applicationContext: Context = context.applicationContext
val name: String = "jumbo/$rawEmoji"
val bitmap: Bitmap? = cache[name]
val task: ListenableFutureTask<Bitmap>? = tasks[name]
val archiveName = "jumbo/${drawInfo.jumboSheet}.proto"
val emojiName: String = drawInfo.rawEmoji!!
val bitmap: Bitmap? = cache[emojiName]
if (bitmap != null) {
return LoadResult.Immediate(bitmap)
}
if (task != null) {
return LoadResult.Async(task)
}
val newTask = ListenableFutureTask<Bitmap> {
val version: EmojiFiles.Version? = EmojiFiles.Version.readVersion(applicationContext, true)
if (version == null) {
@@ -59,38 +102,50 @@ object JumboEmoji {
throw NoVersionData()
}
currentVersion = version.version
var jumbos: EmojiFiles.JumboCollection = EmojiFiles.JumboCollection.read(applicationContext, version)
val uuid = jumbos.getUUIDForName(name)
val uuid = jumbos.getUUIDForName(emojiName)
if (uuid == null) {
if (!AutoDownloadEmojiConstraint.canAutoDownloadEmoji(applicationContext)) {
if (!AutoDownloadEmojiConstraint.canAutoDownloadJumboEmoji(applicationContext)) {
throw CannotAutoDownload()
}
Log.i(TAG, "No file for emoji, downloading jumbo")
val emojiFilesName: EmojiFiles.Name = EmojiDownloader.downloadAndVerifyImageFromRemote(applicationContext, version, version.density, name, format)
jumbos = EmojiFiles.JumboCollection.append(applicationContext, jumbos, emojiFilesName)
EmojiDownloader.streamFileFromRemote(version, version.density, archiveName) { stream ->
stream.use { remote ->
val jumbomojiPack = JumbomojiPack.parseFrom(remote)
jumbomojiPack.itemsList.forEach { jumbo ->
val emojiNameEntry = EmojiFiles.Name(jumbo.name, UUID.randomUUID())
val outputStream = EmojiFiles.openForWriting(applicationContext, version, emojiNameEntry.uuid)
outputStream.use { jumbo.image.writeTo(it) }
jumbos = EmojiFiles.JumboCollection.append(applicationContext, jumbos, emojiNameEntry)
}
}
}
SignalStore.emojiValues().addJumboEmojiSheet(version.version, drawInfo.jumboSheet)
}
val inputStream = EmojiFiles.openForReadingJumbo(applicationContext, version, jumbos, name)
inputStream.use { BitmapFactory.decodeStream(it, null, BitmapFactory.Options()) }
EmojiFiles.openForReadingJumbo(applicationContext, version, jumbos, emojiName).use { BitmapFactory.decodeStream(it, null, BitmapFactory.Options()) }
}
tasks[name] = newTask
SimpleTask.run(SignalExecutors.SERIAL, newTask::run) {
SimpleTask.run(executor, newTask::run) {
try {
val newBitmap: Bitmap? = newTask.get()
if (newBitmap == null) {
Log.w(TAG, "Failed to load jumbo emoji")
} else {
cache[name] = newBitmap
cache[emojiName] = newBitmap
downloadedJumbos.add(drawInfo.jumboSheet!!)
}
} catch (e: ExecutionException) {
Log.d(TAG, "Failed to load jumbo emoji", e.cause)
} finally {
tasks.remove(name)
// do nothing, emoji provider will log the exception
}
}