• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.emoji2.bundled;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotEquals;
21 import static org.junit.Assert.assertNull;
22 import static org.junit.Assert.assertThat;
23 import static org.junit.Assert.assertTrue;
24 import static org.mockito.ArgumentMatchers.anyFloat;
25 import static org.mockito.ArgumentMatchers.anyInt;
26 import static org.mockito.Mockito.any;
27 import static org.mockito.Mockito.doAnswer;
28 import static org.mockito.Mockito.mock;
29 
30 import android.content.Context;
31 import android.content.res.AssetManager;
32 import android.graphics.Canvas;
33 import android.graphics.Paint;
34 import android.graphics.text.PositionedGlyphs;
35 import android.graphics.text.TextRunShaper;
36 import android.text.Spanned;
37 
38 import androidx.annotation.GuardedBy;
39 import androidx.annotation.NonNull;
40 import androidx.emoji.text.EmojiCompat;
41 import androidx.emoji.text.EmojiSpan;
42 import androidx.emoji.text.MetadataRepo;
43 import androidx.emoji2.bundled.util.EmojiMatcher;
44 import androidx.emoji2.bundled.util.Emoji;
45 import androidx.emoji2.bundled.util.TestString;
46 import androidx.test.core.app.ApplicationProvider;
47 import androidx.test.filters.LargeTest;
48 import androidx.test.filters.SdkSuppress;
49 
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 import org.junit.runners.Parameterized;
53 import org.mockito.invocation.InvocationOnMock;
54 import org.mockito.stubbing.Answer;
55 
56 import java.io.BufferedReader;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.InputStreamReader;
60 import java.util.ArrayList;
61 import java.util.Collection;
62 import java.util.List;
63 
64 /**
65  * Reads raw/allemojis.txt which includes all the emojis known to human kind and tests that
66  * EmojiCompat creates EmojiSpans for each one of them.
67  */
68 @LargeTest
69 @RunWith(Parameterized.class)
70 @SdkSuppress(minSdkVersion = 19)
71 public class AllEmojisTest {
72     /**
73      * String representation for a single emoji
74      */
75     private final String mString;
76 
77     /**
78      * Codepoints of emoji for better assert error message.
79      */
80     private final String mCodepoints;
81 
82     private static class TestConfig extends EmojiCompat.Config {
TestConfig(String fontPath)83         TestConfig(String fontPath) {
84             super(new TestEmojiDataLoader(fontPath));
85             setReplaceAll(true);
86         }
87     }
88 
89     private static class TestEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
90         static final Object S_METADATA_REPO_LOCK = new Object();
91         // keep a static instance to in order not to slow down the tests
92         @GuardedBy("sMetadataRepoLock")
93         static volatile MetadataRepo sMetadataRepo;
94 
95         private final String mFontPath;
96 
TestEmojiDataLoader(String fontPath)97         TestEmojiDataLoader(String fontPath) {
98             mFontPath = fontPath;
99         }
100 
101         @Override
load(@onNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback)102         public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
103             if (sMetadataRepo == null) {
104                 synchronized (S_METADATA_REPO_LOCK) {
105                     if (sMetadataRepo == null) {
106                         try {
107                             final Context context = ApplicationProvider.getApplicationContext();
108                             final AssetManager assetManager = context.getAssets();
109                             sMetadataRepo = MetadataRepo.create(assetManager, mFontPath);
110                         } catch (Throwable e) {
111                             loaderCallback.onFailed(e);
112                             throw new RuntimeException(e);
113                         }
114                     }
115                 }
116             }
117 
118             loaderCallback.onLoaded(sMetadataRepo);
119         }
120     }
121 
122     @Parameterized.Parameters(name = "Emoji Render Test: {1}")
data()123     public static Collection<Object[]> data() throws IOException {
124         final Context context = ApplicationProvider.getApplicationContext();
125         try (InputStream inputStream = context.getAssets().open("emojis.txt")) {
126             final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
127             final Collection<Object[]> data = new ArrayList<>();
128             final StringBuilder stringBuilder = new StringBuilder();
129             final StringBuilder codePointsBuilder = new StringBuilder();
130 
131             String s;
132             while ((s = reader.readLine()) != null) {
133                 s = s.trim();
134                 // pass comments
135                 if (s.isEmpty() || s.startsWith("#")) continue;
136 
137                 stringBuilder.setLength(0);
138                 codePointsBuilder.setLength(0);
139 
140                 // emoji codepoints are space separated: i.e. 0x1f1e6 0x1f1e8
141                 final String[] split = s.split(" ");
142 
143                 for (int index = 0; index < split.length; index++) {
144                     final String part = split[index].trim();
145                     codePointsBuilder.append(part);
146                     codePointsBuilder.append(",");
147                     stringBuilder.append(Character.toChars(Integer.parseInt(part, 16)));
148                 }
149 
150                 String string = stringBuilder.toString();
151                 String codePoints = codePointsBuilder.toString();
152 
153                 // TODO(nona): Enable test case for flags.
154                 if (!isFlagEmoji(string)) {
155                     data.add(new Object[]{string, codePoints});
156                 }
157             }
158 
159             return data;
160         }
161 
162     }
163 
AllEmojisTest(String string, String codepoints)164     public AllEmojisTest(String string, String codepoints) {
165         mString = string;
166         mCodepoints = codepoints;
167 
168         // TODO(nona): Add full covered EmojiCompat font.
169         EmojiCompat.reset(new TestConfig("NotoColorEmojiCompat.ttf"));
170     }
171 
172     @Test
testEmoji()173     public void testEmoji() {
174         assertTrue("EmojiCompat should have emoji: " + mCodepoints,
175                 EmojiCompat.get().hasEmojiGlyph(mString));
176         assertEmojiCompatAddsEmoji(mString);
177         assertSpanCanRenderEmoji(mString);
178     }
179 
assertSpanCanRenderEmoji(final String str)180     private void assertSpanCanRenderEmoji(final String str) {
181         final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString());
182         final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class);
183 
184         Canvas canvas = mock(Canvas.class);
185 
186         List<PositionedGlyphs> result = new ArrayList<>();
187 
188         doAnswer(new Answer() {
189             @Override
190             public Object answer(InvocationOnMock invocation) throws Throwable {
191                 char[] text = invocation.getArgument(0);
192                 int index = invocation.getArgument(1);
193                 int count = invocation.getArgument(2);
194                 Paint paint = invocation.getArgument(5);
195 
196                 PositionedGlyphs glyphs = TextRunShaper.shapeTextRun(
197                         text, index, count, index, count, 0, 0, false, paint
198                 );
199                 result.add(glyphs);
200                 return null;
201             };
202         }).when(canvas)
203                 .drawText(any(char[].class), anyInt(), anyInt(), anyFloat(), anyFloat(),
204                         any(Paint.class));
205         spans[0].draw(canvas, spanned, 0, spanned.length(), 0, 0, 0, 0, new Paint());
206 
207         assertEquals(1, result.size());
208         PositionedGlyphs glyphs = result.get(0);
209 
210         // All inputs are single emojis. Thus if multiple glyphs are generated, likely the emoji
211         // sequence is decomposed.
212         assertEquals(mCodepoints, 1, glyphs.glyphCount());
213         assertNotEquals(mCodepoints, 0, glyphs.getGlyphId(0));
214         // null file path means the glyph is NOT came from system font.
215         assertNull(mCodepoints, glyphs.getFont(0).getFile());
216     }
217 
isFlagEmoji(String str)218     private static boolean isFlagEmoji(String str) {
219         return str.codePoints().allMatch(cp -> 0x1F1E6 <= cp && cp <= 0x1F1FF);
220     }
221 
assertEmojiCompatAddsEmoji(final String str)222     private void assertEmojiCompatAddsEmoji(final String str) {
223         TestString string = new TestString(str);
224         CharSequence sequence = EmojiCompat.get().process(string.toString());
225         assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
226         assertThat(sequence,
227                 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
228 
229         // case where Emoji is in the middle of string
230         string = new TestString(str).withPrefix().withSuffix();
231         sequence = EmojiCompat.get().process(string.toString());
232         assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
233         assertThat(sequence,
234                 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
235 
236         // case where Emoji is at the end of string
237         string = new TestString(str).withSuffix();
238         sequence = EmojiCompat.get().process(string.toString());
239         assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
240         assertThat(sequence,
241                 EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
242     }
243 
244 }
245