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