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