Add support for jumbo emoji.

fork-5.53.8
Cody Henthorne 2022-01-06 10:24:08 -05:00 zatwierdzone przez Alex Hart
rodzic 449acaf9df
commit 34f679b10b
20 zmienionych plików z 401 dodań i 163 usunięć

Wyświetl plik

@ -34,14 +34,13 @@ class TextAvatarDrawable(
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
val width = bounds.width()
val candidates = EmojiProvider.getCandidates(avatar.text)
var hasEmoji = false
textPaint.textSize = textSize
val newText = if (candidates == null || candidates.size() == 0) {
SpannableString(avatar.text)
} else {
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous, true)
}
if (newText == null) return

Wyświetl plik

@ -50,7 +50,7 @@ public class SearchView extends androidx.appcompat.widget.SearchView {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(view);
result[0] = new EmojiFilter(view, false);
return result;
}

Wyświetl plik

@ -33,10 +33,11 @@ public class EmojiEditText extends AppCompatEditText {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
setFilters(appendEmojiFilter(this.getFilters()));
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
}
}
@ -54,7 +55,7 @@ public class EmojiEditText extends AppCompatEditText {
else super.invalidateDrawable(drawable);
}
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
InputFilter[] result;
if (originalFilters != null) {
@ -64,7 +65,7 @@ public class EmojiEditText extends AppCompatEditText {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(this);
result[0] = new EmojiFilter(this, jumboEmoji);
return result;
}

Wyświetl plik

@ -8,9 +8,11 @@ import android.widget.TextView;
public class EmojiFilter implements InputFilter {
private TextView view;
private boolean jumboEmoji;
public EmojiFilter(TextView view) {
this.view = view;
public EmojiFilter(TextView view, boolean jumboEmoji) {
this.view = view;
this.jumboEmoji = jumboEmoji;
}
@Override
@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.emojify(new String(v), view);
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);

Wyświetl plik

@ -19,15 +19,17 @@ import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.util.DeviceProperties;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
public class EmojiProvider {
@ -39,23 +41,24 @@ public class EmojiProvider {
return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text);
}
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) {
static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv, boolean jumboEmoji) {
if (tv.isInEditMode()) {
return null;
} else {
return emojify(getCandidates(text), text, tv);
return emojify(getCandidates(text), text, tv, jumboEmoji);
}
}
static @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches,
@Nullable CharSequence text,
@NonNull TextView tv)
@NonNull TextView tv,
boolean jumboEmoji)
{
if (matches == null || text == null || tv.isInEditMode()) return null;
SpannableStringBuilder builder = new SpannableStringBuilder(text);
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout);
Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout, jumboEmoji);
if (drawable != null) {
builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(),
@ -70,7 +73,8 @@ public class EmojiProvider {
@Nullable EmojiParser.CandidateList matches,
@Nullable CharSequence text,
@NonNull Paint paint,
boolean synchronous)
boolean synchronous,
boolean jumboEmoji)
{
if (matches == null || text == null) return null;
SpannableStringBuilder builder = new SpannableStringBuilder(text);
@ -78,9 +82,9 @@ public class EmojiProvider {
for (EmojiParser.Candidate candidate : matches) {
Drawable drawable;
if (synchronous) {
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo());
drawable = getEmojiDrawableSync(context, candidate.getDrawInfo(), jumboEmoji);
} else {
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null);
drawable = getEmojiDrawable(context, candidate.getDrawInfo(), null, jumboEmoji);
}
if (drawable != null) {
@ -93,8 +97,12 @@ public class EmojiProvider {
}
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) {
return getEmojiDrawable(context, emoji, false);
}
static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji, boolean jumboEmoji) {
EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length());
return getEmojiDrawable(context, drawInfo, null);
return getEmojiDrawable(context, drawInfo, null, jumboEmoji);
}
/**
@ -104,7 +112,7 @@ public class EmojiProvider {
* @param drawInfo Information about the emoji being displayed
* @param onEmojiLoaded Runnable which will trigger when an emoji is loaded from disk
*/
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded) {
private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded, boolean jumboEmoji) {
if (drawInfo == null) {
return null;
}
@ -112,6 +120,7 @@ public class EmojiProvider {
final int lowMemoryDecodeScale = DeviceProperties.isLowMemoryDevice(context) ? 2 : 1;
final EmojiSource source = EmojiSource.getLatest();
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
final AtomicBoolean jumboLoaded = new AtomicBoolean(false);
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
@ -122,9 +131,11 @@ public class EmojiProvider {
@Override
public void onSuccess(Bitmap result) {
ThreadUtil.runOnMain(() -> {
drawable.setBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
if (!jumboLoaded.get()) {
drawable.setBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
}
}
});
}
@ -138,6 +149,36 @@ public class EmojiProvider {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
if (jumboEmoji) {
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
if (result instanceof JumboEmoji.LoadResult.Immediate) {
ThreadUtil.runOnMain(() -> {
jumboLoaded.set(true);
drawable.setSingleBitmap(((JumboEmoji.LoadResult.Immediate) result).getBitmap());
});
} else if (result instanceof JumboEmoji.LoadResult.Async) {
((JumboEmoji.LoadResult.Async) result).getTask().addListener(new FutureTaskListener<Bitmap>() {
@Override
public void onSuccess(Bitmap result) {
ThreadUtil.runOnMain(() -> {
jumboLoaded.set(true);
drawable.setSingleBitmap(result);
if (onEmojiLoaded != null) {
onEmojiLoaded.run();
}
});
}
@Override
public void onFailure(ExecutionException exception) {
Log.d(TAG, "Failed to load jumbo emoji bitmap resource", exception);
}
});
}
return drawable;
}
return drawable;
}
@ -147,7 +188,7 @@ public class EmojiProvider {
* @param context Context object used in reading and writing from disk
* @param drawInfo Information about the emoji being displayed
*/
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo) {
private static @Nullable Drawable getEmojiDrawableSync(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, boolean jumboEmoji) {
ThreadUtil.assertNotMainThread();
if (drawInfo == null) {
return null;
@ -157,24 +198,45 @@ public class EmojiProvider {
final EmojiSource source = EmojiSource.getLatest();
final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale);
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
Bitmap bitmap = null;
Bitmap bitmap = null;
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
try {
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
if (jumboEmoji) {
JumboEmoji.LoadResult result = JumboEmoji.loadJumboEmoji(context, drawInfo.getRawEmoji());
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 (bitmap != null) {
drawable.setSingleBitmap(bitmap);
}
} else {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
drawable.setBitmap(bitmap);
if (!jumboEmoji || bitmap == null) {
EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale);
if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) {
Log.d(TAG, "Cached emoji page: " + drawInfo.getPage().getUri().toString());
bitmap = ((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap();
} else if (loadResult instanceof EmojiPageCache.LoadResult.Async) {
Log.d(TAG, "Loading emoji page: " + drawInfo.getPage().getUri().toString());
try {
bitmap = ((EmojiPageCache.LoadResult.Async) loadResult).getTask().get(2, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException exception) {
Log.d(TAG, "Failed to load emoji bitmap resource", exception);
}
} else {
throw new IllegalStateException("Unexpected subclass " + loadResult.getClass());
}
drawable.setBitmap(bitmap);
}
return drawable;
}
@ -183,7 +245,8 @@ public class EmojiProvider {
private final float intrinsicHeight;
private final Rect emojiBounds;
private Bitmap bmp;
private Bitmap bmp;
private boolean isSingleBitmap;
@Override
public int getIntrinsicWidth() {
@ -219,12 +282,21 @@ public class EmojiProvider {
}
canvas.drawBitmap(bmp,
emojiBounds,
isSingleBitmap ? null : emojiBounds,
getBounds(),
PAINT);
}
public void setBitmap(Bitmap bitmap) {
setBitmap(bitmap, false);
}
public void setSingleBitmap(Bitmap bitmap) {
setBitmap(bitmap, true);
}
private void setBitmap(Bitmap bitmap, boolean isSingleBitmap) {
this.isSingleBitmap = isSingleBitmap;
if (bmp == null || !bmp.sameAs(bitmap)) {
bmp = bitmap;
invalidateSelf();

Wyświetl plik

@ -57,6 +57,7 @@ public class EmojiTextView extends AppCompatTextView {
private int lastLineWidth = -1;
private TextDirectionHeuristic textDirection;
private boolean isJumbomoji;
private boolean forceJumboEmoji;
private MentionRendererDelegate mentionRendererDelegate;
@ -77,6 +78,7 @@ public class EmojiTextView extends AppCompatTextView {
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
@ -112,10 +114,10 @@ public class EmojiTextView extends AppCompatTextView {
int emojis = candidates.size();
float scale = 1.0f;
if (emojis <= 8) scale += 0.25f;
if (emojis <= 6) scale += 0.25f;
if (emojis <= 4) scale += 0.25f;
if (emojis <= 2) scale += 0.25f;
if (emojis <= 8) scale += 0.75f;
if (emojis <= 6) scale += 0.75f;
if (emojis <= 4) scale += 0.75f;
if (emojis <= 2) scale += 0.75f;
isJumbomoji = scale > 1.0f;
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalFontSize * scale);
@ -137,7 +139,7 @@ public class EmojiTextView extends AppCompatTextView {
if (useSystemEmoji || candidates == null || candidates.size() == 0) {
super.setText(new SpannableStringBuilder(Optional.fromNullable(text).or("")), BufferType.SPANNABLE);
} else {
CharSequence emojified = EmojiProvider.emojify(candidates, text, this);
CharSequence emojified = EmojiProvider.emojify(candidates, text, this, isJumbomoji || forceJumboEmoji);
super.setText(new SpannableStringBuilder(emojified), BufferType.SPANNABLE);
}
@ -261,7 +263,7 @@ public class EmojiTextView extends AppCompatTextView {
if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) {
super.setText(newContent, BufferType.SPANNABLE);
} else {
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
super.setText(emojified, BufferType.SPANNABLE);
}
}
@ -292,7 +294,7 @@ public class EmojiTextView extends AppCompatTextView {
.append(Optional.fromNullable(overflowText).or(""));
EmojiParser.CandidateList newCandidates = isInEditMode() ? null : EmojiProvider.getCandidates(newContent);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this);
CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, isJumbomoji || forceJumboEmoji);
super.setText(emojified, BufferType.SPANNABLE);
}

Wyświetl plik

@ -31,7 +31,7 @@ open class SimpleEmojiTextView @JvmOverloads constructor(
val newText = if (newCandidates == null || newCandidates.size() == 0) {
text
} else {
EmojiProvider.emojify(newCandidates, text, this)
EmojiProvider.emojify(newCandidates, text, this, false)
}
val newContent = if (width == 0 || maxLines == -1) {

Wyświetl plik

@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.components.emoji.parsing;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.emoji.EmojiPage;
public class EmojiDrawInfo {
private final EmojiPage page;
private final int index;
public EmojiDrawInfo(final @NonNull EmojiPage page, final int index) {
this.page = page;
this.index = index;
}
public @NonNull EmojiPage getPage() {
return page;
}
public int getIndex() {
return index;
}
@Override
public @NonNull String toString() {
return "DrawInfo{" +
"page=" + page +
", index=" + index +
'}';
}
}

Wyświetl plik

@ -0,0 +1,13 @@
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())
}
}

Wyświetl plik

@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.emoji;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.mobilecoin.lib.util.Hex;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Okio;
import okio.Sink;
import okio.Source;
/**
* Helper for downloading Emoji files via {@link EmojiRemote}.
*/
public class EmojiDownloader {
public static @NonNull EmojiFiles.Name downloadAndVerifyJsonFromRemote(@NonNull Context context, @NonNull EmojiFiles.Version version) throws IOException {
return downloadAndVerifyFromRemote(context,
version,
() -> EmojiRemote.getObject(new EmojiJsonRequest(version.getVersion())),
EmojiFiles.Name::forEmojiDataJson);
}
public static @NonNull EmojiFiles.Name downloadAndVerifyImageFromRemote(@NonNull Context context,
@NonNull EmojiFiles.Version version,
@NonNull String bucket,
@NonNull String imagePath,
@NonNull String format) throws IOException
{
return downloadAndVerifyFromRemote(context,
version,
() -> EmojiRemote.getObject(new EmojiImageRequest(version.getVersion(), bucket, imagePath, format)),
() -> new EmojiFiles.Name(imagePath, UUID.randomUUID()));
}
private static @NonNull EmojiFiles.Name downloadAndVerifyFromRemote(@NonNull Context context,
@NonNull EmojiFiles.Version version,
@NonNull Producer<Response> responseProducer,
@NonNull Producer<EmojiFiles.Name> nameProducer) 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");
}
String responseMD5 = getMD5FromResponse(response);
if (responseMD5 == null) {
throw new IOException("Invalid ETag on response");
}
EmojiFiles.Name name = nameProducer.produce();
byte[] savedMd5;
try (OutputStream outputStream = EmojiFiles.openForWriting(context, version, name.getUuid())) {
Source source = response.body().source();
Sink sink = Okio.sink(outputStream);
Okio.buffer(source).readAll(sink);
outputStream.flush();
source.close();
sink.close();
savedMd5 = EmojiFiles.getMd5(context, version, name.getUuid());
}
if (!Arrays.equals(savedMd5, Hex.toByteArray(responseMD5))) {
EmojiFiles.delete(context, version, name.getUuid());
throw new IOException("MD5 Mismatch.");
}
return name;
}
}
private static @Nullable String getMD5FromResponse(@NonNull Response response) {
Pattern pattern = Pattern.compile(".*([a-f0-9]{32}).*");
String header = response.header("etag");
Matcher matcher = pattern.matcher(header);
if (matcher.find()) {
return matcher.group(1);
} else {
return null;
}
}
private interface Producer<T> {
@NonNull T produce();
}
}

Wyświetl plik

@ -47,11 +47,13 @@ private const val TAG = "EmojiFiles"
private const val EMOJI_DIRECTORY = "emoji"
private const val VERSION_FILE = ".version"
private const val NAME_FILE = ".names"
private const val JUMBO_FILE = ".jumbos"
private const val EMOJI_JSON = "emoji_data.json"
private fun Context.getEmojiDirectory(): File = getDir(EMOJI_DIRECTORY, Context.MODE_PRIVATE)
private fun Context.getVersionFile(): File = File(getEmojiDirectory(), VERSION_FILE)
private fun Context.getNameFile(versionUuid: UUID): File = File(File(getEmojiDirectory(), versionUuid.toString()).apply { mkdir() }, NAME_FILE)
private fun Context.getJumboFile(versionUuid: UUID): File = File(File(getEmojiDirectory(), versionUuid.toString()).apply { mkdir() }, JUMBO_FILE)
@Suppress("UNUSED_PARAMETER")
private fun getFilesUri(name: String, format: String): Uri = PartAuthority.getEmojiUri(name)
@ -89,6 +91,13 @@ object EmojiFiles {
return getInputStream(context, file)
}
fun openForReadingJumbo(context: Context, version: Version, names: JumboCollection, name: String): InputStream {
val dataUuid: UUID = names.getUUIDForName(name) ?: throw IOException("Could not get UUID for name $name")
val file: File = version.getFile(context, dataUuid)
return getInputStream(context, file)
}
@JvmStatic
fun openForWriting(context: Context, version: Version, uuid: UUID): OutputStream {
return getOutputStream(context, version.getFile(context, uuid))
@ -137,7 +146,8 @@ object EmojiFiles {
private val objectMapper = ObjectMapper().registerKotlinModule()
@JvmStatic
fun readVersion(context: Context): Version? {
@JvmOverloads
fun readVersion(context: Context, skipValidation: Boolean = false): Version? {
val version = try {
getInputStream(context, context.getVersionFile()).use {
objectMapper.readValue(it, Version::class.java)
@ -147,7 +157,7 @@ object EmojiFiles {
null
}
return if (isVersionValid(context, version)) {
return if (skipValidation || isVersionValid(context, version)) {
version
} else {
null
@ -236,4 +246,34 @@ object EmojiFiles {
@JsonIgnore
fun getUUIDForName(name: String): UUID? = names.firstOrNull { it.name == name }?.uuid
}
class JumboCollection(@JsonProperty val versionUuid: UUID, @JsonProperty val names: List<Name>) {
companion object {
private val objectMapper = ObjectMapper().registerKotlinModule()
@JvmStatic
fun read(context: Context, version: Version): JumboCollection {
try {
getInputStream(context, context.getJumboFile(version.uuid)).use {
return objectMapper.readValue(it)
}
} catch (e: Exception) {
return JumboCollection(version.uuid, listOf())
}
}
@JvmStatic
fun append(context: Context, nameCollection: JumboCollection, name: Name): JumboCollection {
val collection = JumboCollection(nameCollection.versionUuid, nameCollection.names + name)
getOutputStream(context, context.getJumboFile(nameCollection.versionUuid)).use {
objectMapper.writeValue(it, collection)
}
return collection
}
}
@JsonIgnore
fun getUUIDForName(name: String): UUID? = names.firstOrNull { it.name == name }?.uuid
}
}

Wyświetl plik

@ -75,7 +75,7 @@ object EmojiPageCache {
val bitmapOptions = BitmapFactory.Options()
bitmapOptions.inSampleSize = emojiPageRequest.inSampleSize
return BitmapFactory.decodeStream(inputStream, null, bitmapOptions)
return inputStream.use { BitmapFactory.decodeStream(it, null, bitmapOptions) }
}
private data class EmojiPageRequest(val emojiPage: EmojiPage, val inSampleSize: Int)

Wyświetl plik

@ -63,7 +63,7 @@ class EmojiSource(
.forEach { page ->
val emojiPage = emojiPageFactory(page.spriteUri!!)
page.emoji.forEachIndexed { idx, emoji ->
tree.add(emoji, EmojiDrawInfo(emojiPage, idx))
tree.add(emoji, EmojiDrawInfo(emojiPage, idx, emoji))
}
}

Wyświetl plik

@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.emoji
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.annotation.MainThread
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint
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
private val TAG = Log.tag(JumboEmoji::class.java)
/**
* For Jumbo Emojis, will download, add to in-memory cache, and load from disk.
*/
object JumboEmoji {
private val cache: MutableMap<String, Bitmap> = SoftHashMap(16)
private val tasks: MutableMap<String, ListenableFutureTask<Bitmap>> = hashMapOf()
private val versionToFormat: MutableMap<UUID, String?> = hashMapOf()
@Suppress("FoldInitializerAndIfToElvis")
@JvmStatic
@MainThread
fun loadJumboEmoji(context: Context, rawEmoji: String): LoadResult {
val applicationContext: Context = context.applicationContext
val name: String = "jumbo/$rawEmoji"
val bitmap: Bitmap? = cache[name]
val task: ListenableFutureTask<Bitmap>? = tasks[name]
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) {
throw NoVersionData()
}
val format: String? = versionToFormat.getOrPut(version.uuid) {
EmojiFiles.getLatestEmojiData(context, version)?.format
}
if (format == null) {
throw NoVersionData()
}
var jumbos: EmojiFiles.JumboCollection = EmojiFiles.JumboCollection.read(applicationContext, version)
val uuid = jumbos.getUUIDForName(name)
if (uuid == null) {
if (!AutoDownloadEmojiConstraint.canAutoDownloadEmoji(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)
}
val inputStream = EmojiFiles.openForReadingJumbo(applicationContext, version, jumbos, name)
inputStream.use { BitmapFactory.decodeStream(it, null, BitmapFactory.Options()) }
}
tasks[name] = newTask
SimpleTask.run(SignalExecutors.SERIAL, newTask::run) {
try {
val newBitmap: Bitmap? = newTask.get()
if (newBitmap == null) {
Log.w(TAG, "Failed to load jumbo emoji")
} else {
cache[name] = newBitmap
}
} catch (e: ExecutionException) {
Log.d(TAG, "Failed to load jumbo emoji", e.cause)
} finally {
tasks.remove(name)
}
}
return LoadResult.Async(newTask)
}
class NoVersionData : Throwable()
class CannotAutoDownload : IOException()
sealed class LoadResult {
data class Immediate(val bitmap: Bitmap) : LoadResult()
data class Async(val task: ListenableFutureTask<Bitmap>) : LoadResult()
}
}

Wyświetl plik

@ -14,6 +14,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiData;
import org.thoughtcrime.securesms.emoji.EmojiDownloader;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.emoji.EmojiImageRequest;
import org.thoughtcrime.securesms.emoji.EmojiJsonRequest;
@ -260,7 +261,7 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
if (!Arrays.equals(localHash, remoteHash)) {
Log.d(TAG, "Downloading JSON from Remote");
assertRemoteDownloadConstraints(context);
EmojiFiles.Name name = downloadAndVerifyJsonFromRemote(context, version);
EmojiFiles.Name name = EmojiDownloader.downloadAndVerifyJsonFromRemote(context, version);
EmojiFiles.NameCollection.append(context, names, name);
} else {
Log.d(TAG, "Already have JSON from remote, skipping download");
@ -305,7 +306,7 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
}
assertRemoteDownloadConstraints(context);
EmojiFiles.Name name = downloadAndVerifyImageFromRemote(context, version, version.getDensity(), imagePath, format);
EmojiFiles.Name name = EmojiDownloader.downloadAndVerifyImageFromRemote(context, version, version.getDensity(), imagePath, format);
names = EmojiFiles.NameCollection.append(context, names, name);
} else {
Log.d(TAG, "Already have Image from remote, skipping download");
@ -323,83 +324,6 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
}
}
private static @NonNull EmojiFiles.Name downloadAndVerifyJsonFromRemote(@NonNull Context context, @NonNull EmojiFiles.Version version) throws IOException {
return downloadAndVerifyFromRemote(context,
version,
() -> EmojiRemote.getObject(new EmojiJsonRequest(version.getVersion())),
EmojiFiles.Name::forEmojiDataJson);
}
private static @NonNull EmojiFiles.Name downloadAndVerifyImageFromRemote(@NonNull Context context,
@NonNull EmojiFiles.Version version,
@NonNull String bucket,
@NonNull String imagePath,
@NonNull String format) throws IOException
{
return downloadAndVerifyFromRemote(context,
version,
() -> EmojiRemote.getObject(new EmojiImageRequest(version.getVersion(), bucket, imagePath, format)),
() -> new EmojiFiles.Name(imagePath, UUID.randomUUID()));
}
private static @NonNull EmojiFiles.Name downloadAndVerifyFromRemote(@NonNull Context context,
@NonNull EmojiFiles.Version version,
@NonNull Producer<Response> responseProducer,
@NonNull Producer<EmojiFiles.Name> nameProducer) 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");
}
String responseMD5 = getMD5FromResponse(response);
if (responseMD5 == null) {
throw new IOException("Invalid ETag on response");
}
EmojiFiles.Name name = nameProducer.produce();
byte[] savedMd5;
try (OutputStream outputStream = EmojiFiles.openForWriting(context, version, name.getUuid())) {
Source source = response.body().source();
Sink sink = Okio.sink(outputStream);
Okio.buffer(source).readAll(sink);
outputStream.flush();
source.close();
sink.close();
savedMd5 = EmojiFiles.getMd5(context, version, name.getUuid());
}
if (!Arrays.equals(savedMd5, Hex.toByteArray(responseMD5))) {
EmojiFiles.delete(context, version, name.getUuid());
throw new IOException("MD5 Mismatch.");
}
return name;
}
}
private static @Nullable String getMD5FromResponse(@NonNull Response response) {
Pattern pattern = Pattern.compile(".*([a-f0-9]{32}).*");
String header = response.header("etag");
Matcher matcher = pattern.matcher(header);
if (matcher.find()) {
return matcher.group(1);
} else {
return null;
}
}
private static void clearOldEmojiData(@NonNull Context context, @Nullable EmojiFiles.Version newVersion) {
EmojiFiles.Version version = EmojiFiles.Version.readVersion(context);

Wyświetl plik

@ -38,6 +38,7 @@
app:layout_constraintStart_toStartOf="@id/avatar_picker_item_image"
app:layout_constraintTop_toTopOf="@id/avatar_picker_item_image"
app:emoji_forceCustom="true"
app:emoji_forceJumbo="true"
tools:ignore="SpUsage"
tools:text="AF" />

Wyświetl plik

@ -23,6 +23,7 @@
android:fontFamily="sans-serif-medium"
android:gravity="center"
tools:text="AF"
app:emoji_forceJumbo="true"
app:emoji_forceCustom="true"/>
<View

Wyświetl plik

@ -36,6 +36,7 @@
android:gravity="center"
android:imeOptions="actionNext"
android:inputType="textCapCharacters"
app:emoji_forceJumbo="true"
app:layout_constraintBottom_toBottomOf="@id/avatar_picker_item_image"
app:layout_constraintEnd_toEndOf="@id/avatar_picker_item_image"
app:layout_constraintStart_toStartOf="@id/avatar_picker_item_image"

Wyświetl plik

@ -35,6 +35,7 @@
android:gravity="center"
android:imeOptions="actionNext"
android:inputType="textCapCharacters"
app:emoji_forceJumbo="true"
app:layout_constraintBottom_toBottomOf="@id/avatar_picker_item_image"
app:layout_constraintEnd_toEndOf="@id/avatar_picker_item_image"
app:layout_constraintStart_toStartOf="@id/avatar_picker_item_image"

Wyświetl plik

@ -121,6 +121,7 @@
<attr name="emoji_maxLength" format="integer" />
<attr name="emoji_forceCustom" format="boolean" />
<attr name="emoji_renderMentions" format="boolean" />
<attr name="emoji_forceJumbo" format="boolean" />
</declare-styleable>
<declare-styleable name="RingtonePreference">