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