kopia lustrzana https://github.com/ryukoposting/Signal-Android
Update jumbomoji processing and downloading.
rodzic
2b021f5237
commit
bfdedd57d1
|
@ -35,6 +35,7 @@ import org.signal.core.util.logging.AndroidLogger;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
|
@ -192,6 +193,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Emoji {
|
||||
|
||||
private final List<String> variations;
|
||||
private final List<String> rawVariations;
|
||||
|
||||
public Emoji(String... variations) {
|
||||
this.variations = Arrays.asList(variations);
|
||||
this(Arrays.asList(variations), Collections.emptyList());
|
||||
}
|
||||
|
||||
public Emoji(List<String> variations) {
|
||||
this(variations, Collections.emptyList());
|
||||
}
|
||||
|
||||
public Emoji(List<String> variations, List<String> rawVariations) {
|
||||
this.variations = variations;
|
||||
this.rawVariations = rawVariations;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
|
@ -26,4 +35,11 @@ public class Emoji {
|
|||
public boolean hasMultipleVariations() {
|
||||
return variations.size() > 1;
|
||||
}
|
||||
|
||||
public @Nullable String getRawVariation(int variationIndex) {
|
||||
if (rawVariations != null && variationIndex >= 0 && variationIndex < rawVariations.size()) {
|
||||
return rawVariations.get(variationIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,8 +149,8 @@ public class EmojiProvider {
|
|||
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
|
||||
}
|
||||
|
||||
if (jumboEmoji) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
|
||||
if (jumboEmoji && drawInfo.getJumboSheet() != null) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo);
|
||||
if (result instanceof JumboEmoji.LoadResult.Immediate) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
jumboLoaded.set(true);
|
||||
|
@ -171,7 +171,11 @@ public class EmojiProvider {
|
|||
|
||||
@Override
|
||||
public void onFailure(ExecutionException exception) {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
if (exception.getCause() instanceof JumboEmoji.CannotAutoDownload) {
|
||||
Log.i(TAG, "Download restrictions are preventing jumbomoji use");
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -200,15 +204,19 @@ public class EmojiProvider {
|
|||
|
||||
Bitmap bitmap = null;
|
||||
|
||||
if (jumboEmoji) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
|
||||
if (jumboEmoji && drawInfo.getJumboSheet() != null) {
|
||||
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo);
|
||||
if (result instanceof JumboEmoji.LoadResult.Immediate) {
|
||||
bitmap = ((JumboEmoji.LoadResult.Immediate) result).getBitmap();
|
||||
} else if (result instanceof JumboEmoji.LoadResult.Async) {
|
||||
try {
|
||||
bitmap = ((JumboEmoji.LoadResult.Async) result).getTask().get(10, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
if (exception.getCause() instanceof JumboEmoji.CannotAutoDownload) {
|
||||
Log.i(TAG, "Download restrictions are preventing jumbomoji use");
|
||||
} else {
|
||||
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -43,7 +44,8 @@ public class EmojiTextView extends AppCompatTextView {
|
|||
|
||||
private final boolean scaleEmojis;
|
||||
|
||||
private static final char ELLIPSIS = '…';
|
||||
private static final char ELLIPSIS = '…';
|
||||
private static final float JUMBOMOJI_SCALE = 0.8f;
|
||||
|
||||
private boolean forceCustom;
|
||||
private CharSequence previousText;
|
||||
|
@ -113,13 +115,13 @@ public class EmojiTextView extends AppCompatTextView {
|
|||
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis) {
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis && (candidates.hasJumboForAll() || JumboEmoji.canDownloadJumbo(getContext()))) {
|
||||
int emojis = candidates.size();
|
||||
float scale = 1.0f;
|
||||
|
||||
if (emojis <= 5) scale += 0.9f;
|
||||
if (emojis <= 4) scale += 0.9f;
|
||||
if (emojis <= 2) scale += 0.9f;
|
||||
if (emojis <= 5) scale += JUMBOMOJI_SCALE;
|
||||
if (emojis <= 4) scale += JUMBOMOJI_SCALE;
|
||||
if (emojis <= 2) scale += JUMBOMOJI_SCALE;
|
||||
|
||||
isJumbomoji = scale > 1.0f;
|
||||
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
package org.thoughtcrime.securesms.components.emoji.parsing
|
||||
|
||||
import org.thoughtcrime.securesms.emoji.EmojiPage
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import java.nio.charset.Charset
|
||||
|
||||
data class EmojiDrawInfo(val page: EmojiPage, val index: Int, private val emoji: String) {
|
||||
val rawEmoji: String
|
||||
get() {
|
||||
val emojiBytes: ByteArray = emoji.toByteArray(Charset.forName("UTF-16"))
|
||||
return Hex.toStringCondensed(emojiBytes.slice(2 until emojiBytes.size).toByteArray())
|
||||
}
|
||||
}
|
||||
data class EmojiDrawInfo(val page: EmojiPage, val index: Int, private val emoji: String, val rawEmoji: String?, val jumboSheet: String?)
|
||||
|
|
|
@ -24,6 +24,8 @@ package org.thoughtcrime.securesms.components.emoji.parsing;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -127,6 +129,15 @@ public class EmojiParser {
|
|||
return list.size();
|
||||
}
|
||||
|
||||
public boolean hasJumboForAll() {
|
||||
for (Candidate candidate : list) {
|
||||
if (!JumboEmoji.hasJumboEmoji(candidate.drawInfo)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Iterator<Candidate> iterator() {
|
||||
return list.iterator();
|
||||
|
|
|
@ -612,7 +612,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
if (isJumboji == null) {
|
||||
if (getBody().length() <= EmojiSource.getLatest().getMaxEmojiLength() * JumboEmoji.MAX_JUMBOJI_COUNT) {
|
||||
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(getDisplayBody(context));
|
||||
isJumboji = candidates != null && candidates.allEmojis && candidates.size() <= JumboEmoji.MAX_JUMBOJI_COUNT;
|
||||
isJumboji = candidates != null && candidates.allEmojis && candidates.size() <= JumboEmoji.MAX_JUMBOJI_COUNT && (candidates.hasJumboForAll() || JumboEmoji.canDownloadJumbo(context));
|
||||
} else {
|
||||
isJumboji = false;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,11 +54,15 @@ public class AutoDownloadEmojiConstraint implements Constraint {
|
|||
}
|
||||
|
||||
public static boolean canAutoDownloadEmoji(@NonNull Context context) {
|
||||
return getAllowedAutoDownloadTypes(context).contains(IMAGE_TYPE);
|
||||
return getAllowedAutoDownloadTypes(context, true).contains(IMAGE_TYPE);
|
||||
}
|
||||
|
||||
private static @NonNull Set<String> getAllowedAutoDownloadTypes(@NonNull Context context) {
|
||||
if (NetworkUtil.isConnectedWifi(context)) return Collections.singleton(IMAGE_TYPE);
|
||||
public static boolean canAutoDownloadJumboEmoji(@NonNull Context context) {
|
||||
return getAllowedAutoDownloadTypes(context, false).contains(IMAGE_TYPE);
|
||||
}
|
||||
|
||||
private static @NonNull Set<String> getAllowedAutoDownloadTypes(@NonNull Context context, boolean forceWifi) {
|
||||
if (NetworkUtil.isConnectedWifi(context)) return forceWifi ? Collections.singleton(IMAGE_TYPE) : TextSecurePreferences.getWifiMediaDownloadAllowed(context);
|
||||
else if (NetworkUtil.isConnectedRoaming(context)) return TextSecurePreferences.getRoamingMediaDownloadAllowed(context);
|
||||
else if (NetworkUtil.isConnectedMobile(context)) return TextSecurePreferences.getMobileMediaDownloadAllowed(context);
|
||||
else return Collections.emptySet();
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.emoji.EmojiJsonRequest;
|
|||
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiRemote;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint;
|
||||
|
@ -158,6 +159,7 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
|
|||
clearOldEmojiData(context, targetVersion);
|
||||
markComplete(targetVersion);
|
||||
EmojiSource.refresh();
|
||||
JumboEmoji.updateCurrentVersion(context);
|
||||
} else {
|
||||
Log.d(TAG, "Server has an older version than we do. Skipping.");
|
||||
}
|
||||
|
@ -359,6 +361,10 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
|
|||
.forEach(FileUtils::deleteDirectory);
|
||||
|
||||
EmojiPageCache.INSTANCE.clear();
|
||||
|
||||
if (version != null) {
|
||||
SignalStore.emojiValues().clearJumboEmojiSheets(version.getVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<DownloadLatestEmojiDataJob> {
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
|
|||
import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.DeleteDeprecatedLogsMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.EmojiDownloadMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.KbsEnclaveMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
|
||||
|
@ -187,6 +188,7 @@ public final class JobManagerFactories {
|
|||
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
|
||||
put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory());
|
||||
put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory());
|
||||
put(EmojiDownloadMigrationJob.KEY, new EmojiDownloadMigrationJob.Factory());
|
||||
put(KbsEnclaveMigrationJob.KEY, new KbsEnclaveMigrationJob.Factory());
|
||||
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
|
||||
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
|
||||
|
|
|
@ -11,7 +11,9 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class EmojiValues extends SignalStoreValues {
|
||||
|
||||
|
@ -28,6 +30,7 @@ public class EmojiValues extends SignalStoreValues {
|
|||
private static final String SEARCH_VERSION = PREFIX + "search_version";
|
||||
private static final String SEARCH_LANGUAGE = PREFIX + "search_language";
|
||||
private static final String LAST_SEARCH_CHECK = PREFIX + "last_search_check";
|
||||
private static final String JUMBO_EMOJI_DOWNLOAD = PREFIX + "jumbo_emoji_v";
|
||||
|
||||
public static final String NO_LANGUAGE = "NO_LANGUAGE";
|
||||
|
||||
|
@ -131,4 +134,22 @@ public class EmojiValues extends SignalStoreValues {
|
|||
public void setLastSearchIndexCheck(long time) {
|
||||
putLong(LAST_SEARCH_CHECK, time);
|
||||
}
|
||||
|
||||
public void addJumboEmojiSheet(int version, String sheet) {
|
||||
Set<String> sheets = getJumboEmojiSheets(version);
|
||||
sheets.add(sheet);
|
||||
getStore().beginWrite()
|
||||
.putString(JUMBO_EMOJI_DOWNLOAD + version, Util.join(sheets, ","))
|
||||
.apply();
|
||||
}
|
||||
|
||||
public HashSet<String> getJumboEmojiSheets(int version) {
|
||||
return new HashSet<>(Arrays.asList(getStore().getString(JUMBO_EMOJI_DOWNLOAD + version, "").split(",")));
|
||||
}
|
||||
|
||||
public void clearJumboEmojiSheets(int version) {
|
||||
getStore().beginWrite()
|
||||
.remove(JUMBO_EMOJI_DOWNLOAD + version)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,9 +93,10 @@ public class ApplicationMigrations {
|
|||
//static final int CHANGE_NUMBER_CAPABILITY_3 = 49;
|
||||
static final int PNI = 50;
|
||||
static final int FIX_DEPRECATION = 51; // Only used to trigger clearing the 'client deprecated' flag
|
||||
static final int JUMBOMOJI_DOWNLOAD = 52;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 51;
|
||||
public static final int CURRENT_VERSION = 52;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
|
@ -401,6 +402,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.PNI, new PniMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.JUMBOMOJI_DOWNLOAD) {
|
||||
jobs.put(Version.JUMBOMOJI_DOWNLOAD, new EmojiDownloadMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package org.thoughtcrime.securesms.migrations;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
|
||||
/**
|
||||
* Schedules a emoji download job to get the latest version.
|
||||
*/
|
||||
public final class EmojiDownloadMigrationJob extends MigrationJob {
|
||||
|
||||
public static final String KEY = "EmojiDownloadMigrationJob";
|
||||
|
||||
EmojiDownloadMigrationJob() {
|
||||
this(new Parameters.Builder().build());
|
||||
}
|
||||
|
||||
private EmojiDownloadMigrationJob(@NonNull Parameters parameters) {
|
||||
super(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUiBlocking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performMigration() {
|
||||
ApplicationDependencies.getJobManager().add(new DownloadLatestEmojiDataJob(false));
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean shouldRetry(@NonNull Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<EmojiDownloadMigrationJob> {
|
||||
@Override
|
||||
public @NonNull EmojiDownloadMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new EmojiDownloadMigrationJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package signal;
|
||||
|
||||
option java_package = "org.thoughtcrime.securesms.emoji.protos";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message JumbomojiPack {
|
||||
repeated JumbomojiItem items = 1;
|
||||
}
|
||||
|
||||
message JumbomojiItem {
|
||||
string name = 1;
|
||||
bytes image = 2;
|
||||
}
|
|
@ -51,6 +51,30 @@ private const val SAMPLE_JSON_WITH_OBSOLETE = """
|
|||
}
|
||||
"""
|
||||
|
||||
private const val SAMPLE_JSON_WITH_JUMBOS = """
|
||||
{
|
||||
"emoji": {
|
||||
"Places_1": [["0002"], ["0003", "0004", "0005"]],
|
||||
"Places_2": [["0003"], ["0008", "0009", "0000"]],
|
||||
"Foods": [["0001"], ["0002", "0003", "0004"]]
|
||||
},
|
||||
"jumbomoji": {
|
||||
"People_0": ["d83dde00","d83dde03","d83dde04", "d83dde01"],
|
||||
"People_1": ["ad83dde00","ad83dde03","ad83dde04", "ad83dde01"]
|
||||
},
|
||||
"obsolete": [
|
||||
{"obsoleted": "0012", "replace_with": "0023"}
|
||||
],
|
||||
"metrics": {
|
||||
"raw_height": 64,
|
||||
"raw_width": 64,
|
||||
"per_row": 16
|
||||
},
|
||||
"densities": [ "xhdpi" ],
|
||||
"format": "png"
|
||||
}
|
||||
"""
|
||||
|
||||
private val SAMPLE_JSON_WITHOUT_OBSOLETE_EXPECTED = listOf(
|
||||
StaticEmojiPageModel(EmojiCategory.FOODS, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")),
|
||||
StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\ud83c\udf0d"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places"))
|
||||
|
@ -128,6 +152,23 @@ class EmojiJsonParserTest {
|
|||
Assert.assertEquals(result.format, "png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Given sample with jumbo, when I parse, then I expect source with jumbo map`() {
|
||||
val result: ParsedEmojiData = EmojiJsonParser.parse(SAMPLE_JSON_WITH_JUMBOS.byteInputStream(), this::uriFactory).getOrThrow()
|
||||
|
||||
val jumboPages = result.jumboPages
|
||||
|
||||
Assert.assertEquals("People_0", jumboPages["d83dde00"])
|
||||
Assert.assertEquals("People_0", jumboPages["d83dde03"])
|
||||
Assert.assertEquals("People_0", jumboPages["d83dde04"])
|
||||
Assert.assertEquals("People_0", jumboPages["d83dde01"])
|
||||
|
||||
Assert.assertEquals("People_1", jumboPages["ad83dde00"])
|
||||
Assert.assertEquals("People_1", jumboPages["ad83dde03"])
|
||||
Assert.assertEquals("People_1", jumboPages["ad83dde04"])
|
||||
Assert.assertEquals("People_1", jumboPages["ad83dde01"])
|
||||
}
|
||||
|
||||
private fun uriFactory(sprite: String, format: String) = Uri.parse("file:///$sprite")
|
||||
|
||||
private fun EmojiPageModel.isSameAs(other: EmojiPageModel) =
|
||||
|
|
|
@ -10,7 +10,7 @@ class EmojiSourceTest {
|
|||
|
||||
@Test
|
||||
fun `Given a bunch of data pages with max value 100100, when I get the maxEmojiLength, then I expect 6`() {
|
||||
val emojiDataFake = ParsedEmojiData(EmojiMetrics(-1, -1, -1), listOf(), "png", listOf(), dataPages = generatePages(), listOf())
|
||||
val emojiDataFake = ParsedEmojiData(EmojiMetrics(-1, -1, -1), listOf(), "png", listOf(), dataPages = generatePages(), emptyMap(), listOf())
|
||||
val testSubject = EmojiSource(0f, emojiDataFake) { uri -> EmojiPage.Disk(uri) }
|
||||
|
||||
Assert.assertEquals(6, testSubject.maxEmojiLength)
|
||||
|
|
Ładowanie…
Reference in New Issue