Add support for OTA emoji download.

This commit is contained in:
Alex Hart
2021-04-19 10:36:33 -03:00
committed by Cody Henthorne
parent 7fa200401c
commit 85e0e74bc6
55 changed files with 1653 additions and 621 deletions

View File

@@ -5,42 +5,59 @@ import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@RunWith(ParameterizedRobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "androidx.*" })
@PrepareForTest({ApplicationDependencies.class, AttachmentSecretProvider.class})
public class EmojiUtilTest_isEmoji {
public @Rule PowerMockRule rule = new PowerMockRule();
private final String input;
private final boolean output;
@ParameterizedRobolectricTestRunner.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{ null, false },
{ "", false },
{ "cat", false },
{ "ᑢᗩᖶ", false },
{ "♍︎♋︎⧫︎", false },
{ "", false },
{ "¯\\_(ツ)_/¯", false},
{ "\uD83D\uDE0D", true }, // Smiling face with heart-shaped eyes
{ "\uD83D\uDD77", true }, // Spider
{ "\uD83E\uDD37", true }, // Person shrugging
{ "\uD83E\uDD37\uD83C\uDFFF\u200D♂", true }, // Man shrugging dark skin tone
{ "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", true }, // Family: Man, Woman, Girl, Boy
{ "\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDC67\uD83C\uDFFB\u200D\uD83D\uDC66\uD83C\uDFFB", true }, // Family - Man: Light Skin Tone, Woman: Light Skin Tone, Girl: Light Skin Tone, Boy: Light Skin Tone (NOTE: Not widely supported, good stretch test)
{ "\uD83D\uDE0Dhi", false }, // Smiling face with heart-shaped eyes, text afterwards
{ "\uD83D\uDE0D ", false }, // Smiling face with heart-shaped eyes, space afterwards
{ "\uD83D\uDE0D\uD83D\uDE0D", false }, // Smiling face with heart-shaped eyes, twice
{null, false},
{"", false},
{"cat", false},
{"ᑢᗩᖶ", false},
{"♍︎♋︎⧫︎", false},
{"", false},
{"¯\\_(ツ)_/¯", false},
{"\uD83D\uDE0D", true}, // Smiling face with heart-shaped eyes
{"\uD83D\uDD77", true}, // Spider
{"\uD83E\uDD37", true}, // Person shrugging
{"\uD83E\uDD37\uD83C\uDFFF\u200D♂", true}, // Man shrugging dark skin tone
{"\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66", true}, // Family: Man, Woman, Girl, Boy
{"\uD83D\uDC68\uD83C\uDFFB\u200D\uD83D\uDC69\uD83C\uDFFB\u200D\uD83D\uDC67\uD83C\uDFFB\u200D\uD83D\uDC66\uD83C\uDFFB", true}, // Family - Man: Light Skin Tone, Woman: Light Skin Tone, Girl: Light Skin Tone, Boy: Light Skin Tone (NOTE: Not widely supported, good stretch test)
{"\uD83D\uDE0Dhi", false}, // Smiling face with heart-shaped eyes, text afterwards
{"\uD83D\uDE0D ", false}, // Smiling face with heart-shaped eyes, space afterwards
{"\uD83D\uDE0D\uD83D\uDE0D", false}, // Smiling face with heart-shaped eyes, twice
});
}
@@ -54,6 +71,12 @@ public class EmojiUtilTest_isEmoji {
public void isEmoji() {
Context context = ApplicationProvider.getApplicationContext();
PowerMockito.mockStatic(ApplicationDependencies.class);
PowerMockito.when(ApplicationDependencies.getApplication()).thenReturn((Application) context);
PowerMockito.mockStatic(AttachmentSecretProvider.class);
PowerMockito.when(AttachmentSecretProvider.getInstance(any())).thenThrow(IOException.class);
EmojiSource.refresh();
assertEquals(output, EmojiUtil.isEmoji(context, input));
}
}

View File

@@ -0,0 +1,138 @@
package org.thoughtcrime.securesms.emoji
import android.app.Application
import android.net.Uri
import com.fasterxml.jackson.core.JsonParseException
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.components.emoji.CompositeEmojiPageModel
import org.thoughtcrime.securesms.components.emoji.Emoji
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel
private const val INVALID_JSON = "{{}"
private const val EMPTY_JSON = "{}"
private const val SAMPLE_JSON_WITHOUT_OBSOLETE = """
{
"emoji": {
"Places": [["d83cdf0d"], ["0003", "0004", "0005"]],
"Foods": [["0001"], ["0002", "0003", "0004"]]
},
"metrics": {
"raw_height": 64,
"raw_width": 64,
"per_row": 16
},
"densities": [ "xhdpi" ],
"format": "png"
}
"""
private const val SAMPLE_JSON_WITH_OBSOLETE = """
{
"emoji": {
"Places_1": [["0002"], ["0003", "0004", "0005"]],
"Places_2": [["0003"], ["0008", "0009", "0000"]],
"Foods": [["0001"], ["0002", "0003", "0004"]]
},
"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.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")),
StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\ud83c\udf0d"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places"))
)
private val SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DISPLAY = listOf(
StaticEmojiPageModel(EmojiCategory.FOODS.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")),
CompositeEmojiPageModel(
EmojiCategory.PLACES.icon,
listOf(
StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")),
StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2"))
)
)
)
private val SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DATA = listOf(
StaticEmojiPageModel(EmojiCategory.FOODS.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")),
StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")),
StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2"))
)
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class EmojiJsonParserTest {
@Test(expected = NullPointerException::class)
fun `Given empty json, when I parse, then I expect a NullPointerException`() {
val result = EmojiJsonParser.parse(EMPTY_JSON.byteInputStream(), this::uriFactory)
result.getOrThrow()
}
@Test(expected = JsonParseException::class)
fun `Given invalid json, when I parse, then I expect a JsonParseException`() {
val result = EmojiJsonParser.parse(INVALID_JSON.byteInputStream(), this::uriFactory)
result.getOrThrow()
}
@Test
fun `Given sample without obselete, when I parse, then I expect source without obsolete`() {
val result: ParsedEmojiData = EmojiJsonParser.parse(SAMPLE_JSON_WITHOUT_OBSOLETE.byteInputStream(), this::uriFactory).getOrThrow()
Assert.assertTrue(result.obsolete.isEmpty())
Assert.assertTrue(result.displayPages == result.dataPages)
Assert.assertEquals(SAMPLE_JSON_WITHOUT_OBSOLETE_EXPECTED.size, result.dataPages.size)
result.dataPages.zip(SAMPLE_JSON_WITHOUT_OBSOLETE_EXPECTED).forEach { (actual, expected) ->
Assert.assertTrue(actual.isSameAs(expected))
}
}
@Test
fun `Given sample with obsolete, when I parse, then I expect source with obsolete`() {
val result: ParsedEmojiData = EmojiJsonParser.parse(SAMPLE_JSON_WITH_OBSOLETE.byteInputStream(), this::uriFactory).getOrThrow()
Assert.assertTrue(result.obsolete.size == 1)
Assert.assertEquals("\u0012", result.obsolete[0].obsolete)
Assert.assertEquals("\u0023", result.obsolete[0].replaceWith)
Assert.assertFalse(result.displayPages == result.dataPages)
Assert.assertEquals(SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DISPLAY.size, result.displayPages.size)
result.displayPages.zip(SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DISPLAY).forEach { (actual, expected) ->
Assert.assertTrue(actual.isSameAs(expected))
}
Assert.assertEquals(SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DATA.size, result.dataPages.size)
result.dataPages.zip(SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DATA).forEach { (actual, expected) ->
Assert.assertTrue(actual.isSameAs(expected))
}
Assert.assertEquals(result.densities, listOf("xhdpi"))
Assert.assertEquals(result.format, "png")
}
private fun uriFactory(sprite: String, format: String) = Uri.parse("file:///$sprite")
private fun EmojiPageModel.isSameAs(other: EmojiPageModel) =
this.javaClass == other.javaClass &&
this.emoji == other.emoji &&
this.iconAttr == other.iconAttr &&
this.spriteUri == other.spriteUri
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.emoji
import android.net.Uri
import org.junit.Assert
import org.junit.Test
import org.thoughtcrime.securesms.components.emoji.Emoji
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
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 testSubject = EmojiSource(0f, emojiDataFake, ::EmojiPageReference)
Assert.assertEquals(6, testSubject.maxEmojiLength)
}
private fun generatePages() = (1..10).map { EmojiPageModelFake((1..100).shuffled().map { Emoji("$it$it") }) }
private class EmojiPageModelFake(private val displayE: List<Emoji>) : EmojiPageModel {
override fun getEmoji(): List<String> = displayE.map { it.variations }.flatten()
override fun getDisplayEmoji(): List<Emoji> = displayE
override fun getIconAttr(): Int = TODO("Not yet implemented")
override fun getSpriteUri(): Uri = TODO("Not yet implemented")
override fun isDynamic(): Boolean = TODO("Not yet implemented")
}
}