1 /* 2 * Copyright 2018 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 package androidx.emoji.text; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20 import android.os.Build; 21 import android.text.Editable; 22 import android.text.Selection; 23 import android.text.Spannable; 24 import android.text.SpannableString; 25 import android.text.Spanned; 26 import android.text.TextPaint; 27 import android.text.method.KeyListener; 28 import android.text.method.MetaKeyKeyListener; 29 import android.view.KeyEvent; 30 import android.view.inputmethod.InputConnection; 31 32 import androidx.annotation.AnyThread; 33 import androidx.annotation.IntDef; 34 import androidx.annotation.IntRange; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.RequiresApi; 38 import androidx.annotation.RestrictTo; 39 import androidx.core.graphics.PaintCompat; 40 import androidx.core.util.Preconditions; 41 import androidx.emoji.widget.SpannableBuilder; 42 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.Arrays; 46 import java.util.List; 47 48 /** 49 * Processes the CharSequence and adds the emojis. 50 * 51 * @hide 52 */ 53 @AnyThread 54 @RestrictTo(LIBRARY_GROUP) 55 @RequiresApi(19) 56 final class EmojiProcessor { 57 58 /** 59 * State transition commands. 60 */ 61 @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH}) 62 @Retention(RetentionPolicy.SOURCE) 63 private @interface Action { 64 } 65 66 /** 67 * Advance the end pointer in CharSequence and reset the start to be the end. 68 */ 69 private static final int ACTION_ADVANCE_BOTH = 1; 70 71 /** 72 * Advance end pointer in CharSequence. 73 */ 74 private static final int ACTION_ADVANCE_END = 2; 75 76 /** 77 * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end 78 * pointer in CharSequence and reset the start to be the end. 79 */ 80 private static final int ACTION_FLUSH = 3; 81 82 /** 83 * Factory used to create EmojiSpans. 84 */ 85 private final EmojiCompat.SpanFactory mSpanFactory; 86 87 /** 88 * Emoji metadata repository. 89 */ 90 private final MetadataRepo mMetadataRepo; 91 92 /** 93 * Utility class that checks if the system can render a given glyph. 94 */ 95 private GlyphChecker mGlyphChecker = new GlyphChecker(); 96 97 /** 98 * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean) 99 */ 100 private final boolean mUseEmojiAsDefaultStyle; 101 102 /** 103 * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List) 104 */ 105 private final int[] mEmojiAsDefaultStyleExceptions; 106 EmojiProcessor(@onNull final MetadataRepo metadataRepo, @NonNull final EmojiCompat.SpanFactory spanFactory, final boolean useEmojiAsDefaultStyle, @Nullable final int[] emojiAsDefaultStyleExceptions)107 EmojiProcessor(@NonNull final MetadataRepo metadataRepo, 108 @NonNull final EmojiCompat.SpanFactory spanFactory, 109 final boolean useEmojiAsDefaultStyle, 110 @Nullable final int[] emojiAsDefaultStyleExceptions) { 111 mSpanFactory = spanFactory; 112 mMetadataRepo = metadataRepo; 113 mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle; 114 mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions; 115 } 116 getEmojiMetadata(@onNull final CharSequence charSequence)117 EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) { 118 final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(), 119 mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions); 120 final int end = charSequence.length(); 121 int currentOffset = 0; 122 123 while (currentOffset < end) { 124 final int codePoint = Character.codePointAt(charSequence, currentOffset); 125 final int action = sm.check(codePoint); 126 if (action != ACTION_ADVANCE_END) { 127 return null; 128 } 129 currentOffset += Character.charCount(codePoint); 130 } 131 132 if (sm.isInFlushableState()) { 133 return sm.getCurrentMetadata(); 134 } 135 136 return null; 137 } 138 139 /** 140 * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. 141 * <p> 142 * <ul> 143 * <li>If no emojis are found, {@code charSequence} given as the input is returned without 144 * any changes. i.e. charSequence is a String, and no emojis are found, the same String is 145 * returned.</li> 146 * <li>If the given input is not a Spannable (such as String), and at least one emoji is found 147 * a new {@link android.text.Spannable} instance is returned. </li> 148 * <li>If the given input is a Spannable, the same instance is returned. </li> 149 * </ul> 150 * 151 * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null} 152 * @param start start index in the charSequence to look for emojis, should be greater than or 153 * equal to {@code 0}, also less than {@code charSequence.length()} 154 * @param end end index in the charSequence to look for emojis, should be greater than or 155 * equal to {@code start} parameter, also less than {@code charSequence.length()} 156 * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater 157 * than or equal to {@code 0} 158 * @param replaceAll whether to replace all emoji with {@link EmojiSpan}s 159 */ process(@onNull final CharSequence charSequence, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount, final boolean replaceAll)160 CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start, 161 @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount, 162 final boolean replaceAll) { 163 final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder; 164 if (isSpannableBuilder) { 165 ((SpannableBuilder) charSequence).beginBatchEdit(); 166 } 167 168 try { 169 Spannable spannable = null; 170 // if it is a spannable already, use the same instance to add/remove EmojiSpans. 171 // otherwise wait until the the first EmojiSpan found in order to change the result 172 // into a Spannable. 173 if (isSpannableBuilder || charSequence instanceof Spannable) { 174 spannable = (Spannable) charSequence; 175 } else if (charSequence instanceof Spanned) { 176 // check if there are any EmojiSpans as cheap as possible 177 // start-1, end+1 will return emoji span that starts/ends at start/end indices 178 final int nextSpanTransition = ((Spanned) charSequence).nextSpanTransition( 179 start - 1, end + 1, EmojiSpan.class); 180 181 if (nextSpanTransition <= end) { 182 spannable = new SpannableString(charSequence); 183 } 184 } 185 186 if (spannable != null) { 187 final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class); 188 if (spans != null && spans.length > 0) { 189 // remove existing spans, and realign the start, end according to spans 190 // if start or end is in the middle of an emoji they should be aligned 191 final int length = spans.length; 192 for (int index = 0; index < length; index++) { 193 final EmojiSpan span = spans[index]; 194 final int spanStart = spannable.getSpanStart(span); 195 final int spanEnd = spannable.getSpanEnd(span); 196 // Remove span only when its spanStart is NOT equal to current end. 197 // During add operation an emoji at index 0 is added with 0-1 as start and 198 // end indices. Therefore if there are emoji spans at [0-1] and [1-2] 199 // and end is 1, the span between 0-1 should be deleted, not 1-2. 200 if (spanStart != end) { 201 spannable.removeSpan(span); 202 } 203 start = Math.min(spanStart, start); 204 end = Math.max(spanEnd, end); 205 } 206 } 207 } 208 209 if (start == end || start >= charSequence.length()) { 210 return charSequence; 211 } 212 213 // calculate max number of emojis that can be added. since getSpans call is a relatively 214 // expensive operation, do it only when maxEmojiCount is not unlimited. 215 if (maxEmojiCount != EmojiCompat.EMOJI_COUNT_UNLIMITED && spannable != null) { 216 maxEmojiCount -= spannable.getSpans(0, spannable.length(), EmojiSpan.class).length; 217 } 218 // add new ones 219 int addedCount = 0; 220 final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(), 221 mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions); 222 223 int currentOffset = start; 224 int codePoint = Character.codePointAt(charSequence, currentOffset); 225 226 while (currentOffset < end && addedCount < maxEmojiCount) { 227 final int action = sm.check(codePoint); 228 229 switch (action) { 230 case ACTION_ADVANCE_BOTH: 231 start += Character.charCount(Character.codePointAt(charSequence, start)); 232 currentOffset = start; 233 if (currentOffset < end) { 234 codePoint = Character.codePointAt(charSequence, currentOffset); 235 } 236 break; 237 case ACTION_ADVANCE_END: 238 currentOffset += Character.charCount(codePoint); 239 if (currentOffset < end) { 240 codePoint = Character.codePointAt(charSequence, currentOffset); 241 } 242 break; 243 case ACTION_FLUSH: 244 if (replaceAll || !hasGlyph(charSequence, start, currentOffset, 245 sm.getFlushMetadata())) { 246 if (spannable == null) { 247 spannable = new SpannableString(charSequence); 248 } 249 addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset); 250 addedCount++; 251 } 252 start = currentOffset; 253 break; 254 } 255 } 256 257 // After the last codepoint is consumed the state machine might be in a state where it 258 // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed 259 // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ). 260 // Need to check if it is in such a state. 261 if (sm.isInFlushableState() && addedCount < maxEmojiCount) { 262 if (replaceAll || !hasGlyph(charSequence, start, currentOffset, 263 sm.getCurrentMetadata())) { 264 if (spannable == null) { 265 spannable = new SpannableString(charSequence); 266 } 267 addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset); 268 addedCount++; 269 } 270 } 271 return spannable == null ? charSequence : spannable; 272 } finally { 273 if (isSpannableBuilder) { 274 ((SpannableBuilder) charSequence).endBatchEdit(); 275 } 276 } 277 } 278 279 /** 280 * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of 281 * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an 282 * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 283 * deleted with the characters it covers. 284 * <p/> 285 * If there is a selection where selection start is not equal to selection end, does not 286 * delete. 287 * 288 * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View, 289 * Editable, int, KeyEvent)} 290 * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 291 * int, KeyEvent)} 292 * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 293 * int, KeyEvent)} 294 * 295 * @return {@code true} if an {@link EmojiSpan} is deleted 296 */ handleOnKeyDown(@onNull final Editable editable, final int keyCode, final KeyEvent event)297 static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode, 298 final KeyEvent event) { 299 final boolean handled; 300 switch (keyCode) { 301 case KeyEvent.KEYCODE_DEL: 302 handled = delete(editable, event, false /*forwardDelete*/); 303 break; 304 case KeyEvent.KEYCODE_FORWARD_DEL: 305 handled = delete(editable, event, true /*forwardDelete*/); 306 break; 307 default: 308 handled = false; 309 break; 310 } 311 312 if (handled) { 313 MetaKeyKeyListener.adjustMetaAfterKeypress(editable); 314 return true; 315 } 316 317 return false; 318 } 319 delete(final Editable content, final KeyEvent event, final boolean forwardDelete)320 private static boolean delete(final Editable content, final KeyEvent event, 321 final boolean forwardDelete) { 322 if (hasModifiers(event)) { 323 return false; 324 } 325 326 final int start = Selection.getSelectionStart(content); 327 final int end = Selection.getSelectionEnd(content); 328 if (hasInvalidSelection(start, end)) { 329 return false; 330 } 331 332 final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class); 333 if (spans != null && spans.length > 0) { 334 final int length = spans.length; 335 for (int index = 0; index < length; index++) { 336 final EmojiSpan span = spans[index]; 337 final int spanStart = content.getSpanStart(span); 338 final int spanEnd = content.getSpanEnd(span); 339 if ((forwardDelete && spanStart == start) 340 || (!forwardDelete && spanEnd == start) 341 || (start > spanStart && start < spanEnd)) { 342 content.delete(spanStart, spanEnd); 343 return true; 344 } 345 } 346 } 347 348 return false; 349 } 350 351 /** 352 * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an 353 * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 354 * deleted. 355 * <p/> 356 * If there is a selection where selection start is not equal to selection end, does not 357 * delete. 358 * 359 * @param inputConnection InputConnection instance 360 * @param editable TextView.Editable instance 361 * @param beforeLength the number of characters before the cursor to be deleted 362 * @param afterLength the number of characters after the cursor to be deleted 363 * @param inCodePoints {@code true} if length parameters are in codepoints 364 * 365 * @return {@code true} if an {@link EmojiSpan} is deleted 366 */ handleDeleteSurroundingText(@onNull final InputConnection inputConnection, @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength, @IntRange(from = 0) final int afterLength, final boolean inCodePoints)367 static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection, 368 @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength, 369 @IntRange(from = 0) final int afterLength, final boolean inCodePoints) { 370 //noinspection ConstantConditions 371 if (editable == null || inputConnection == null) { 372 return false; 373 } 374 375 if (beforeLength < 0 || afterLength < 0) { 376 return false; 377 } 378 379 final int selectionStart = Selection.getSelectionStart(editable); 380 final int selectionEnd = Selection.getSelectionEnd(editable); 381 382 if (hasInvalidSelection(selectionStart, selectionEnd)) { 383 return false; 384 } 385 386 int start; 387 int end; 388 if (inCodePoints) { 389 // go backwards in terms of codepoints 390 start = CodepointIndexFinder.findIndexBackward(editable, selectionStart, 391 Math.max(beforeLength, 0)); 392 end = CodepointIndexFinder.findIndexForward(editable, selectionEnd, 393 Math.max(afterLength, 0)); 394 395 if (start == CodepointIndexFinder.INVALID_INDEX 396 || end == CodepointIndexFinder.INVALID_INDEX) { 397 return false; 398 } 399 } else { 400 start = Math.max(selectionStart - beforeLength, 0); 401 end = Math.min(selectionEnd + afterLength, editable.length()); 402 } 403 404 final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class); 405 if (spans != null && spans.length > 0) { 406 final int length = spans.length; 407 for (int index = 0; index < length; index++) { 408 final EmojiSpan span = spans[index]; 409 int spanStart = editable.getSpanStart(span); 410 int spanEnd = editable.getSpanEnd(span); 411 start = Math.min(spanStart, start); 412 end = Math.max(spanEnd, end); 413 } 414 415 start = Math.max(start, 0); 416 end = Math.min(end, editable.length()); 417 418 inputConnection.beginBatchEdit(); 419 editable.delete(start, end); 420 inputConnection.endBatchEdit(); 421 return true; 422 } 423 424 return false; 425 } 426 hasInvalidSelection(final int start, final int end)427 private static boolean hasInvalidSelection(final int start, final int end) { 428 return start == -1 || end == -1 || start != end; 429 } 430 hasModifiers(KeyEvent event)431 private static boolean hasModifiers(KeyEvent event) { 432 return !KeyEvent.metaStateHasNoModifiers(event.getMetaState()); 433 } 434 addEmoji(@onNull final Spannable spannable, final EmojiMetadata metadata, final int start, final int end)435 private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata, 436 final int start, final int end) { 437 final EmojiSpan span = mSpanFactory.createSpan(metadata); 438 spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 439 } 440 441 /** 442 * Checks whether the current OS can render a given emoji. Used by the system to decide if an 443 * emoji span should be added. If the system cannot render it, an emoji span will be added. 444 * Used only for the case where replaceAll is set to {@code false}. 445 * 446 * @param charSequence the CharSequence that the emoji is in 447 * @param start start index of the emoji in the CharSequence 448 * @param end end index of the emoji in the CharSequence 449 * @param metadata EmojiMetadata instance for the emoji 450 * 451 * @return {@code true} if the OS can render emoji, {@code false} otherwise 452 */ hasGlyph(final CharSequence charSequence, int start, final int end, final EmojiMetadata metadata)453 private boolean hasGlyph(final CharSequence charSequence, int start, final int end, 454 final EmojiMetadata metadata) { 455 // For pre M devices, heuristic in PaintCompat can result in false positives. we are 456 // adding another heuristic using the sdkAdded field. if the emoji was added to OS 457 // at a later version we assume that the system probably cannot render it. 458 if (Build.VERSION.SDK_INT < 23 && metadata.getSdkAdded() > Build.VERSION.SDK_INT) { 459 return false; 460 } 461 462 // if the existence is not calculated yet 463 if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) { 464 final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end); 465 metadata.setHasGlyph(hasGlyph); 466 } 467 468 return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS; 469 } 470 471 /** 472 * Set the GlyphChecker instance used by EmojiProcessor. Used for testing. 473 */ setGlyphChecker(@onNull final GlyphChecker glyphChecker)474 void setGlyphChecker(@NonNull final GlyphChecker glyphChecker) { 475 Preconditions.checkNotNull(glyphChecker); 476 mGlyphChecker = glyphChecker; 477 } 478 479 /** 480 * State machine for walking over the metadata trie. 481 */ 482 static final class ProcessorSm { 483 484 private static final int STATE_DEFAULT = 1; 485 private static final int STATE_WALKING = 2; 486 487 private int mState = STATE_DEFAULT; 488 489 /** 490 * Root of the trie 491 */ 492 private final MetadataRepo.Node mRootNode; 493 494 /** 495 * Pointer to the node after last codepoint. 496 */ 497 private MetadataRepo.Node mCurrentNode; 498 499 /** 500 * The node where ACTION_FLUSH is called. Required since after flush action is 501 * returned mCurrentNode is reset to be the root. 502 */ 503 private MetadataRepo.Node mFlushNode; 504 505 /** 506 * The code point that was checked. 507 */ 508 private int mLastCodepoint; 509 510 /** 511 * Level for mCurrentNode. Root is 0. 512 */ 513 private int mCurrentDepth; 514 515 /** 516 * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean) 517 */ 518 private final boolean mUseEmojiAsDefaultStyle; 519 520 /** 521 * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List) 522 */ 523 private final int[] mEmojiAsDefaultStyleExceptions; 524 ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle, int[] emojiAsDefaultStyleExceptions)525 ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle, 526 int[] emojiAsDefaultStyleExceptions) { 527 mRootNode = rootNode; 528 mCurrentNode = rootNode; 529 mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle; 530 mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions; 531 } 532 533 @Action check(final int codePoint)534 int check(final int codePoint) { 535 final int action; 536 MetadataRepo.Node node = mCurrentNode.get(codePoint); 537 switch (mState) { 538 case STATE_WALKING: 539 if (node != null) { 540 mCurrentNode = node; 541 mCurrentDepth += 1; 542 action = ACTION_ADVANCE_END; 543 } else { 544 if (isTextStyle(codePoint)) { 545 action = reset(); 546 } else if (isEmojiStyle(codePoint)) { 547 action = ACTION_ADVANCE_END; 548 } else if (mCurrentNode.getData() != null) { 549 if (mCurrentDepth == 1) { 550 if (shouldUseEmojiPresentationStyleForSingleCodepoint()) { 551 mFlushNode = mCurrentNode; 552 action = ACTION_FLUSH; 553 reset(); 554 } else { 555 action = reset(); 556 } 557 } else { 558 mFlushNode = mCurrentNode; 559 action = ACTION_FLUSH; 560 reset(); 561 } 562 } else { 563 action = reset(); 564 } 565 } 566 break; 567 case STATE_DEFAULT: 568 default: 569 if (node == null) { 570 action = reset(); 571 } else { 572 mState = STATE_WALKING; 573 mCurrentNode = node; 574 mCurrentDepth = 1; 575 action = ACTION_ADVANCE_END; 576 } 577 break; 578 } 579 580 mLastCodepoint = codePoint; 581 return action; 582 } 583 584 @Action reset()585 private int reset() { 586 mState = STATE_DEFAULT; 587 mCurrentNode = mRootNode; 588 mCurrentDepth = 0; 589 return ACTION_ADVANCE_BOTH; 590 } 591 592 /** 593 * @return the metadata node when ACTION_FLUSH is returned 594 */ getFlushMetadata()595 EmojiMetadata getFlushMetadata() { 596 return mFlushNode.getData(); 597 } 598 599 /** 600 * @return current pointer to the metadata node in the trie 601 */ getCurrentMetadata()602 EmojiMetadata getCurrentMetadata() { 603 return mCurrentNode.getData(); 604 } 605 606 /** 607 * Need for the case where input is consumed, but action_flush was not called. For example 608 * when the char sequence has single codepoint character which is a default emoji. State 609 * machine will wait for the next. 610 * 611 * @return whether the current state requires an emoji to be added 612 */ isInFlushableState()613 boolean isInFlushableState() { 614 return mState == STATE_WALKING && mCurrentNode.getData() != null 615 && (mCurrentDepth > 1 || shouldUseEmojiPresentationStyleForSingleCodepoint()); 616 } 617 shouldUseEmojiPresentationStyleForSingleCodepoint()618 private boolean shouldUseEmojiPresentationStyleForSingleCodepoint() { 619 if (mCurrentNode.getData().isDefaultEmoji()) { 620 // The codepoint is emoji style by default. 621 return true; 622 } 623 if (isEmojiStyle(mLastCodepoint)) { 624 // The codepoint was followed by the emoji style variation selector. 625 return true; 626 } 627 if (mUseEmojiAsDefaultStyle) { 628 // Emoji presentation style for text style default emojis is enabled. We have 629 // to check that the current codepoint is not an exception. 630 if (mEmojiAsDefaultStyleExceptions == null) { 631 return true; 632 } 633 final int codepoint = mCurrentNode.getData().getCodepointAt(0); 634 final int index = Arrays.binarySearch(mEmojiAsDefaultStyleExceptions, codepoint); 635 if (index < 0) { 636 // Index is negative, so the codepoint was not found in the array of exceptions. 637 return true; 638 } 639 } 640 return false; 641 } 642 643 /** 644 * @param codePoint CodePoint to check 645 * 646 * @return {@code true} if the codepoint is a emoji style standardized variation selector 647 */ isEmojiStyle(int codePoint)648 private static boolean isEmojiStyle(int codePoint) { 649 return codePoint == 0xFE0F; 650 } 651 652 /** 653 * @param codePoint CodePoint to check 654 * 655 * @return {@code true} if the codepoint is a text style standardized variation selector 656 */ isTextStyle(int codePoint)657 private static boolean isTextStyle(int codePoint) { 658 return codePoint == 0xFE0E; 659 } 660 } 661 662 /** 663 * Copy of BaseInputConnection findIndexBackward and findIndexForward functions. 664 */ 665 private static final class CodepointIndexFinder { 666 private static final int INVALID_INDEX = -1; 667 668 /** 669 * Find start index of the character in {@code cs} that is {@code numCodePoints} behind 670 * starting from {@code from}. 671 * 672 * @param cs CharSequence to work on 673 * @param from the index to start going backwards 674 * @param numCodePoints the number of codepoints 675 * 676 * @return start index of the character 677 */ findIndexBackward(final CharSequence cs, final int from, final int numCodePoints)678 private static int findIndexBackward(final CharSequence cs, final int from, 679 final int numCodePoints) { 680 int currentIndex = from; 681 boolean waitingHighSurrogate = false; 682 final int length = cs.length(); 683 if (currentIndex < 0 || length < currentIndex) { 684 return INVALID_INDEX; // The starting point is out of range. 685 } 686 if (numCodePoints < 0) { 687 return INVALID_INDEX; // Basically this should not happen. 688 } 689 int remainingCodePoints = numCodePoints; 690 while (true) { 691 if (remainingCodePoints == 0) { 692 return currentIndex; // Reached to the requested length in code points. 693 } 694 695 --currentIndex; 696 if (currentIndex < 0) { 697 if (waitingHighSurrogate) { 698 return INVALID_INDEX; // An invalid surrogate pair is found. 699 } 700 return 0; // Reached to the beginning of the text w/o any invalid surrogate 701 // pair. 702 } 703 final char c = cs.charAt(currentIndex); 704 if (waitingHighSurrogate) { 705 if (!Character.isHighSurrogate(c)) { 706 return INVALID_INDEX; // An invalid surrogate pair is found. 707 } 708 waitingHighSurrogate = false; 709 --remainingCodePoints; 710 continue; 711 } 712 if (!Character.isSurrogate(c)) { 713 --remainingCodePoints; 714 continue; 715 } 716 if (Character.isHighSurrogate(c)) { 717 return INVALID_INDEX; // A invalid surrogate pair is found. 718 } 719 waitingHighSurrogate = true; 720 } 721 } 722 723 /** 724 * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead 725 * starting from {@code from}. 726 * 727 * @param cs CharSequence to work on 728 * @param from the index to start going forward 729 * @param numCodePoints the number of codepoints 730 * 731 * @return start index of the character 732 */ findIndexForward(final CharSequence cs, final int from, final int numCodePoints)733 private static int findIndexForward(final CharSequence cs, final int from, 734 final int numCodePoints) { 735 int currentIndex = from; 736 boolean waitingLowSurrogate = false; 737 final int length = cs.length(); 738 if (currentIndex < 0 || length < currentIndex) { 739 return INVALID_INDEX; // The starting point is out of range. 740 } 741 if (numCodePoints < 0) { 742 return INVALID_INDEX; // Basically this should not happen. 743 } 744 int remainingCodePoints = numCodePoints; 745 746 while (true) { 747 if (remainingCodePoints == 0) { 748 return currentIndex; // Reached to the requested length in code points. 749 } 750 751 if (currentIndex >= length) { 752 if (waitingLowSurrogate) { 753 return INVALID_INDEX; // An invalid surrogate pair is found. 754 } 755 return length; // Reached to the end of the text w/o any invalid surrogate 756 // pair. 757 } 758 final char c = cs.charAt(currentIndex); 759 if (waitingLowSurrogate) { 760 if (!Character.isLowSurrogate(c)) { 761 return INVALID_INDEX; // An invalid surrogate pair is found. 762 } 763 --remainingCodePoints; 764 waitingLowSurrogate = false; 765 ++currentIndex; 766 continue; 767 } 768 if (!Character.isSurrogate(c)) { 769 --remainingCodePoints; 770 ++currentIndex; 771 continue; 772 } 773 if (Character.isLowSurrogate(c)) { 774 return INVALID_INDEX; // A invalid surrogate pair is found. 775 } 776 waitingLowSurrogate = true; 777 ++currentIndex; 778 } 779 } 780 } 781 782 /** 783 * Utility class that checks if the system can render a given glyph. 784 * 785 * @hide 786 */ 787 @AnyThread 788 @RestrictTo(LIBRARY_GROUP) 789 public static class GlyphChecker { 790 /** 791 * Default text size for {@link #mTextPaint}. 792 */ 793 private static final int PAINT_TEXT_SIZE = 10; 794 795 /** 796 * Used to create strings required by 797 * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}. 798 */ 799 private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>(); 800 801 /** 802 * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check. 803 */ 804 private final TextPaint mTextPaint; 805 GlyphChecker()806 GlyphChecker() { 807 mTextPaint = new TextPaint(); 808 mTextPaint.setTextSize(PAINT_TEXT_SIZE); 809 } 810 811 /** 812 * Returns whether the system can render an emoji. 813 * 814 * @param charSequence the CharSequence that the emoji is in 815 * @param start start index of the emoji in the CharSequence 816 * @param end end index of the emoji in the CharSequence 817 * 818 * @return {@code true} if the OS can render emoji, {@code false} otherwise 819 */ hasGlyph(final CharSequence charSequence, int start, final int end)820 public boolean hasGlyph(final CharSequence charSequence, int start, final int end) { 821 final StringBuilder builder = getStringBuilder(); 822 builder.setLength(0); 823 824 while (start < end) { 825 builder.append(charSequence.charAt(start)); 826 start++; 827 } 828 829 return PaintCompat.hasGlyph(mTextPaint, builder.toString()); 830 } 831 getStringBuilder()832 private static StringBuilder getStringBuilder() { 833 if (sStringBuilder.get() == null) { 834 sStringBuilder.set(new StringBuilder()); 835 } 836 return sStringBuilder.get(); 837 } 838 839 } 840 } 841