1 /*
2  * Copyright (C) 2013 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 android.text.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertTrue;
22 
23 import android.Manifest;
24 import android.content.Context;
25 import android.graphics.Bitmap;
26 import android.graphics.Canvas;
27 import android.graphics.Paint;
28 import android.graphics.Picture;
29 import android.util.TypedValue;
30 import android.view.KeyEvent;
31 import android.view.View;
32 import android.webkit.WebView.VisualStateCallback;
33 import android.webkit.cts.WebViewOnUiThread;
34 import android.widget.EditText;
35 import android.widget.TextView;
36 
37 import androidx.test.annotation.UiThreadTest;
38 import androidx.test.ext.junit.runners.AndroidJUnit4;
39 import androidx.test.filters.LargeTest;
40 import androidx.test.filters.MediumTest;
41 import androidx.test.platform.app.InstrumentationRegistry;
42 import androidx.test.rule.ActivityTestRule;
43 
44 import com.android.compatibility.common.util.AdoptShellPermissionsRule;
45 import com.android.compatibility.common.util.NullWebViewUtils;
46 import com.android.compatibility.common.util.WindowUtil;
47 
48 import com.google.common.util.concurrent.SettableFuture;
49 
50 import org.junit.Before;
51 import org.junit.Rule;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.util.concurrent.TimeUnit;
56 
57 @MediumTest
58 @RunWith(AndroidJUnit4.class)
59 public class EmojiTest {
60     private static final long TEST_TIMEOUT_MS = 20000L; // 20s
61     private EmojiCtsActivity mActivity;
62     private EditText mEditText;
63 
64     @Rule(order = 0)
65     public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
66             InstrumentationRegistry.getInstrumentation().getUiAutomation(),
67             Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX);
68 
69     @Rule(order = 1)
70     public ActivityTestRule<EmojiCtsActivity> mActivityRule =
71             new ActivityTestRule<>(EmojiCtsActivity.class);
72 
73     @Before
setup()74     public void setup() {
75         mActivity = mActivityRule.getActivity();
76         WindowUtil.waitForFocus(mActivity);
77     }
78 
79     /**
80      * Tests all Emoji are defined in Character class
81      */
82     @Test
testEmojiCodePoints()83     public void testEmojiCodePoints() {
84         for (int i = 0; i < EmojiConstants.ALL_EMOJI.length; i++) {
85             assertTrue(Character.isDefined(EmojiConstants.ALL_EMOJI[i]));
86         }
87     }
88 
describeBitmap(final Bitmap bmp)89     private String describeBitmap(final Bitmap bmp) {
90         StringBuilder sb = new StringBuilder();
91         sb.append("[ID:0x" + Integer.toHexString(System.identityHashCode(bmp)));
92         sb.append(" " + Integer.toString(bmp.getWidth()) + "x" + Integer.toString(bmp.getHeight()));
93         sb.append(" Config:");
94         if (bmp.getConfig() == Bitmap.Config.ALPHA_8) {
95             sb.append("ALPHA_8");
96         } else if (bmp.getConfig() == Bitmap.Config.RGB_565) {
97             sb.append("RGB_565");
98         } else if (bmp.getConfig() == Bitmap.Config.ARGB_4444) {
99             sb.append("ARGB_4444");
100         } else if (bmp.getConfig() == Bitmap.Config.ARGB_8888) {
101             sb.append("ARGB_8888");
102         } else {
103             sb.append("UNKNOWN");
104         }
105         sb.append("]");
106         return sb.toString();
107     }
108 
109     // These emojis should have different characters
110     private static final int sComparedCodePoints[][] = {
111         {0x1F436, 0x1F435},      // Dog(U+1F436) and Monkey(U+1F435)
112         {0x26BD, 0x26BE},        // Soccer ball(U+26BD) and Baseball(U+26BE)
113         {0x1F47B, 0x1F381},      // Ghost(U+1F47B) and wrapped present(U+1F381)
114         {0x2764, 0x1F494},       // Heavy black heart(U+2764) and broken heart(U+1F494)
115         {0x1F603, 0x1F33B}       // Smiling face with open mouth(U+1F603) and sunflower(U+1F33B)
116     };
117 
118     /**
119      * Tests Emoji has different glyph for different meaning characters.
120      * Test on Canvas, TextView and EditText
121      */
122     @UiThreadTest
123     @Test
testEmojiGlyph()124     public void testEmojiGlyph() {
125         CaptureCanvas ccanvas = new CaptureCanvas(mActivity);
126 
127         Bitmap bitmapA, bitmapB;  // Emoji displayed Bitmaps to compare
128 
129         for (int i = 0; i < sComparedCodePoints.length; i++) {
130             String baseMessage = "Glyph for U+" + Integer.toHexString(sComparedCodePoints[i][0])
131                     + " should be different from glyph for U+"
132                     + Integer.toHexString(sComparedCodePoints[i][1]) + ". ";
133 
134             bitmapA = ccanvas.capture(Character.toChars(sComparedCodePoints[i][0]));
135             bitmapB = ccanvas.capture(Character.toChars(sComparedCodePoints[i][1]));
136 
137             String bmpDiffMessage = describeBitmap(bitmapA) + "vs" + describeBitmap(bitmapB);
138             assertFalse(baseMessage + bmpDiffMessage, bitmapA.sameAs(bitmapB));
139 
140             // cannot reuse CaptureTextView as 2nd setText call throws NullPointerException
141             CaptureTextView cviewA = new CaptureTextView(mActivity);
142             bitmapA = cviewA.capture(Character.toChars(sComparedCodePoints[i][0]));
143             CaptureTextView cviewB = new CaptureTextView(mActivity);
144             bitmapB = cviewB.capture(Character.toChars(sComparedCodePoints[i][1]));
145 
146             bmpDiffMessage = describeBitmap(bitmapA) + "vs" + describeBitmap(bitmapB);
147             assertFalse(baseMessage + bmpDiffMessage, bitmapA.sameAs(bitmapB));
148 
149             CaptureEditText cedittextA = new CaptureEditText(mActivity);
150             bitmapA = cedittextA.capture(Character.toChars(sComparedCodePoints[i][0]));
151             CaptureEditText cedittextB = new CaptureEditText(mActivity);
152             bitmapB = cedittextB.capture(Character.toChars(sComparedCodePoints[i][1]));
153 
154             bmpDiffMessage = describeBitmap(bitmapA) + "vs" + describeBitmap(bitmapB);
155             assertFalse(baseMessage + bmpDiffMessage, bitmapA.sameAs(bitmapB));
156         }
157     }
158 
159     /**
160      * Tests Emoji has different glyph for different meaning characters.
161      * Test on WebView
162      */
163     @Test
testEmojiGlyphWebView()164     public void testEmojiGlyphWebView() {
165         if (!NullWebViewUtils.isWebViewAvailable()) {
166             return;
167         }
168 
169         Bitmap bitmapA, bitmapB;  // Emoji displayed Bitmaps to compare
170 
171         CaptureWebView cwebview = new CaptureWebView();
172         for (int i = 0; i < sComparedCodePoints.length; i++) {
173             String baseMessage = "Glyph for U+" + Integer.toHexString(sComparedCodePoints[i][0])
174                     + " should be different from glyph for U+"
175                     + Integer.toHexString(sComparedCodePoints[i][1]) + ". ";
176 
177             bitmapA = cwebview.capture(Character.toChars(sComparedCodePoints[i][0]));
178             bitmapB = cwebview.capture(Character.toChars(sComparedCodePoints[i][1]));
179 
180             String bmpDiffMessage = describeBitmap(bitmapA) + "vs" + describeBitmap(bitmapB);
181             assertFalse(baseMessage + bmpDiffMessage, bitmapA.sameAs(bitmapB));
182         }
183     }
184 
185     /**
186      * Tests EditText handles Emoji
187      */
188     @LargeTest
189     @Test
testEmojiEditable()190     public void testEmojiEditable() throws Throwable {
191         int testedCodePoints[] = {
192             0xAE,    // registered mark
193             0x2764,    // heavy black heart
194             0x1F353    // strawberry - surrogate pair sample. Count as two characters.
195         };
196 
197         String origStr, newStr;
198 
199         // delete Emoji by sending KEYCODE_DEL
200         for (int i = 0; i < testedCodePoints.length; i++) {
201             origStr = "Test character  ";
202             // cannot reuse CaptureTextView as 2nd setText call throws NullPointerException
203             mActivityRule.runOnUiThread(() -> mEditText = new EditText(mActivity));
204             mEditText.setText(origStr + String.valueOf(Character.toChars(testedCodePoints[i])));
205 
206             // confirm the emoji is added.
207             newStr = mEditText.getText().toString();
208             assertEquals(newStr.codePointCount(0, newStr.length()), origStr.length() + 1);
209 
210             // Delete added character by sending KEYCODE_DEL event
211             mActivityRule.runOnUiThread(() -> mEditText.dispatchKeyEvent(
212                     new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)));
213             InstrumentationRegistry.getInstrumentation().waitForIdleSync();
214 
215             newStr = mEditText.getText().toString();
216             assertEquals(newStr.codePointCount(0, newStr.length()), origStr.length() + 1);
217         }
218     }
219 
220     private static class CaptureCanvas extends View {
221 
222         String mTestStr;
223         Paint paint = new Paint();
224 
CaptureCanvas(Context context)225         CaptureCanvas(Context context) {
226             super(context);
227         }
228 
onDraw(Canvas canvas)229         public void onDraw(Canvas canvas) {
230             if (mTestStr != null) {
231                 canvas.drawText(mTestStr, 50, 50, paint);
232             }
233             return;
234         }
235 
capture(char c[])236         Bitmap capture(char c[]) {
237             mTestStr = String.valueOf(c);
238             invalidate();
239 
240             setDrawingCacheEnabled(true);
241             measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
242                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
243             layout(0, 0, 200,200);
244 
245             Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
246             setDrawingCacheEnabled(false);
247             return bitmap;
248         }
249 
250     }
251 
252     private static class CaptureTextView extends TextView {
253 
CaptureTextView(Context context)254         CaptureTextView(Context context) {
255             super(context);
256             setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
257         }
258 
capture(char c[])259         Bitmap capture(char c[]) {
260             setText(String.valueOf(c));
261 
262             invalidate();
263 
264             setDrawingCacheEnabled(true);
265             measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
266                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
267             layout(0, 0, 200,200);
268 
269             Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
270             setDrawingCacheEnabled(false);
271             return bitmap;
272         }
273 
274     }
275 
276     private static class CaptureEditText extends EditText {
277 
CaptureEditText(Context context)278         CaptureEditText(Context context) {
279             super(context);
280             setTextSize(TypedValue.COMPLEX_UNIT_SP, 10);
281         }
282 
capture(char c[])283         Bitmap capture(char c[]) {
284             setText(String.valueOf(c));
285 
286             invalidate();
287 
288             setDrawingCacheEnabled(true);
289             measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
290                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
291             layout(0, 0, 200,200);
292 
293             Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
294             setDrawingCacheEnabled(false);
295             return bitmap;
296         }
297 
298     }
299 
300 
301     private static long sRequestId = 0;
302 
303     private class CaptureWebView {
304         WebViewOnUiThread webViewOnUiThread;
305 
CaptureWebView()306         CaptureWebView() {
307             webViewOnUiThread = new WebViewOnUiThread(mActivityRule.getActivity().getWebView());
308             // Offscreen pre-raster ensures that visibile region of the WebView is not used to
309             // determine which tiles to render, and instead the full WebView size is treated
310             // as the visible region.
311             webViewOnUiThread.getSettings().setOffscreenPreRaster(true);
312         }
313 
capture(char c[])314         Bitmap capture(char c[]) {
315             webViewOnUiThread.loadDataAndWaitForCompletion(
316                     "<html><body>" + String.valueOf(c) + "</body></html>",
317                     "text/html; charset=utf-8", "utf-8");
318 
319             // Wait for the loaded DOM state to be ready to draw.
320             final SettableFuture<Void> future = SettableFuture.create();
321             webViewOnUiThread.postVisualStateCallback(sRequestId++, new VisualStateCallback() {
322                 @Override
323                 public void onComplete(long requestId) {
324                     future.set(null);
325                 }
326             });
327             try {
328                 future.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
329             } catch (Exception e) {
330                 return null;
331             }
332 
333             Picture picture = webViewOnUiThread.capturePicture();
334             if (picture == null || picture.getHeight() <= 0 || picture.getWidth() <= 0) {
335                 return null;
336             }
337             Bitmap bitmap = Bitmap.createBitmap(picture.getWidth(), picture.getHeight(),
338                     Bitmap.Config.ARGB_8888);
339             Canvas canvas = new Canvas(bitmap);
340             picture.draw(canvas);
341 
342             return bitmap;
343         }
344 
345     }
346 
347 }
348 
349