Update jumbomoji processing and downloading.

fork-5.53.8
Cody Henthorne 2022-01-21 10:31:43 -05:00 zatwierdzone przez GitHub
rodzic 2b021f5237
commit bfdedd57d1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
21 zmienionych plików z 351 dodań i 54 usunięć

Wyświetl plik

@ -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");

Wyświetl plik

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

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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?)

Wyświetl plik

@ -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();

Wyświetl plik

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

Wyświetl plik

@ -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");

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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();

Wyświetl plik

@ -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> {

Wyświetl plik

@ -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());

Wyświetl plik

@ -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();
}
}

Wyświetl plik

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

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

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

Wyświetl plik

@ -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) =

Wyświetl plik

@ -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)