mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 13:13:43 +00:00
Add support for OTA emoji download.
This commit is contained in:
committed by
Cody Henthorne
parent
7fa200401c
commit
85e0e74bc6
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user