EmojiProcessor.java revision 7f51b0e4448c80ef32a8c41d90f8bc91c55ed3d4
182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/*
282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Copyright (C) 2017 The Android Open Source Project
382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Licensed under the Apache License, Version 2.0 (the "License");
582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * you may not use this file except in compliance with the License.
682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * You may obtain a copy of the License at
782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *      http://www.apache.org/licenses/LICENSE-2.0
982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
1082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Unless required by applicable law or agreed to in writing, software
1182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * distributed under the License is distributed on an "AS IS" BASIS,
1282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * See the License for the specific language governing permissions and
1482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * limitations under the License.
1582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */
1682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirpackage android.support.text.emoji;
1782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
1882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
1982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
2082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.os.Build;
2182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.AnyThread;
2282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.IntDef;
2382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.IntRange;
2482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.NonNull;
2577b5c5b734f9f665577d1e3d178615db43ae1d4fSiyamed Sinirimport android.support.annotation.RequiresApi;
2682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.RestrictTo;
27f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinirimport android.support.text.emoji.widget.SpannableBuilder;
2882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.v4.graphics.PaintCompat;
29fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinirimport android.support.v4.util.Preconditions;
3082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Editable;
3182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Selection;
3282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spannable;
3382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.SpannableString;
3482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spanned;
3582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.TextPaint;
3682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.KeyListener;
3782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.MetaKeyKeyListener;
3882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.KeyEvent;
3982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.inputmethod.InputConnection;
4082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
4182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.Retention;
4282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.RetentionPolicy;
4382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
4482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/**
4582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Processes the CharSequence and adds the emojis.
4682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
4782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @hide
4882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */
4982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@AnyThread
5082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@RestrictTo(LIBRARY_GROUP)
5177b5c5b734f9f665577d1e3d178615db43ae1d4fSiyamed Sinir@RequiresApi(19)
5282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirfinal class EmojiProcessor {
5382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
5482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
5582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State transition commands.
5682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
5782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH})
5882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @Retention(RetentionPolicy.SOURCE)
59fb15b88b8ff336c2f8c2dff88e55eaba3491349aSiyamed Sinir    private @interface Action {
6082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
6182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
6282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
6382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance the end pointer in CharSequence and reset the start to be the end.
6482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
6582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_BOTH = 1;
6682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
6782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
6882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance end pointer in CharSequence.
6982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
7082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_END = 2;
7182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
7282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
7382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end
7482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * pointer in CharSequence and reset the start to be the end.
7582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
7682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_FLUSH = 3;
7782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
7882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
790bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     * Factory used to create EmojiSpans.
8082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
810bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir    private final EmojiCompat.SpanFactory mSpanFactory;
8282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
8482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Emoji metadata repository.
8582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
8682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final MetadataRepo mMetadataRepo;
8782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
89fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Utility class that checks if the system can render a given glyph.
9082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
91fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    private GlyphChecker mGlyphChecker = new GlyphChecker();
9282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
9382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiProcessor(@NonNull final MetadataRepo metadataRepo,
94fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            @NonNull final EmojiCompat.SpanFactory spanFactory) {
9582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mSpanFactory = spanFactory;
9682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mMetadataRepo = metadataRepo;
9782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
9882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
9982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
10082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
10182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = charSequence.length();
10282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int currentOffset = 0;
10382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
10482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        while (currentOffset < end) {
10582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int codePoint = Character.codePointAt(charSequence, currentOffset);
10682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action = sm.check(codePoint);
10782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (action != ACTION_ADVANCE_END) {
10882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return null;
10982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
11082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            currentOffset += Character.charCount(codePoint);
11182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
11282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
11382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (sm.isInFlushableState()) {
11482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return sm.getCurrentMetadata();
11582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
11682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
11782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return null;
11882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
11982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
12082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
12182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
12282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p>
12382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <ul>
12482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If no emojis are found, {@code charSequence} given as the input is returned without
12582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
12682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * returned.</li>
12782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
12882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * a new {@link android.text.Spannable} instance is returned. </li>
12982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is a Spannable, the same instance is returned. </li>
13082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * </ul>
13182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
13282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
13382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index in the charSequence to look for emojis, should be greater than or
13482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              equal to {@code 0}, also less than {@code charSequence.length()}
13582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index in the charSequence to look for emojis, should be greater than or
13682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *            equal to {@code start} parameter, also less than {@code charSequence.length()}
1370bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
1380bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     *                      than or equal to {@code 0}
139fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * @param replaceAll whether to replace all emoji with {@link EmojiSpan}s
14082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
14182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
142fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount,
143fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final boolean replaceAll) {
144f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder;
145f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        if (isSpannableBuilder) {
146f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            ((SpannableBuilder) charSequence).beginBatchEdit();
14782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
14882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
149f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        try {
150f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            Spannable spannable = null;
151f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // if it is a spannable already, use the same instance to add/remove EmojiSpans.
152f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // otherwise wait until the the first EmojiSpan found in order to change the result
153f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // into a Spannable.
154f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (isSpannableBuilder || charSequence instanceof Spannable) {
155f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                spannable = (Spannable) charSequence;
156f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
157f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir
158f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (spannable != null) {
159f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class);
160f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                if (spans != null && spans.length > 0) {
161f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    // remove existing spans, and realign the start, end according to spans
162f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    // if start or end is in the middle of an emoji they should be aligned
163f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    final int length = spans.length;
164f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    for (int index = 0; index < length; index++) {
165f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final EmojiSpan span = spans[index];
166f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final int spanStart = spannable.getSpanStart(span);
167f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final int spanEnd = spannable.getSpanEnd(span);
168f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // Remove span only when its spanStart is NOT equal to current end.
169f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // During add operation an emoji at index 0 is added with 0-1 as start and
170f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // end indices. Therefore if there are emoji spans at [0-1] and [1-2]
171f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // and end is 1, the span between 0-1 should be deleted, not 1-2.
172f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (spanStart != end) {
173f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            spannable.removeSpan(span);
174f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
175f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        start = Math.min(spanStart, start);
176f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        end = Math.max(spanEnd, end);
17782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
17882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
17982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
18082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
181f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (start == end || start >= charSequence.length()) {
182f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                return charSequence;
183f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
18482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
1850bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            // calculate max number of emojis that can be added. since getSpans call is a relatively
1860bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            // expensive operation, do it only when maxEmojiCount is not unlimited.
18736d5e1b59cef90bb72936e6641970e04b7bde18cAurimas Liutikas            if (maxEmojiCount != EmojiCompat.EMOJI_COUNT_UNLIMITED && spannable != null) {
1880bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir                maxEmojiCount -= spannable.getSpans(0, spannable.length(), EmojiSpan.class).length;
1890bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            }
190f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // add new ones
191f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int addedCount = 0;
192f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
19382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
194f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int currentOffset = start;
195f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int codePoint = Character.codePointAt(charSequence, currentOffset);
19682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
1970bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            while (currentOffset < end && addedCount < maxEmojiCount) {
198f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                final int action = sm.check(codePoint);
19982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
200f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                switch (action) {
201f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_ADVANCE_BOTH:
2027f51b0e4448c80ef32a8c41d90f8bc91c55ed3d4Siyamed Sinir                        start += Character.charCount(Character.codePointAt(charSequence, start));
2037f51b0e4448c80ef32a8c41d90f8bc91c55ed3d4Siyamed Sinir                        currentOffset = start;
204f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (currentOffset < end) {
205f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            codePoint = Character.codePointAt(charSequence, currentOffset);
20682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
207f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
208f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_ADVANCE_END:
209f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        currentOffset += Character.charCount(codePoint);
210f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (currentOffset < end) {
211f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            codePoint = Character.codePointAt(charSequence, currentOffset);
212f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
213f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
214f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_FLUSH:
215fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                        if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
216f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                                sm.getFlushMetadata())) {
217f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            if (spannable == null) {
218f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                                spannable = new SpannableString(charSequence);
219f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            }
220f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset);
221f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            addedCount++;
222f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
223f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        start = currentOffset;
224f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
225f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                }
22682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
22782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
228f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // After the last codepoint is consumed the state machine might be in a state where it
229f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed
230f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ).
231f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // Need to check if it is in such a state.
2320bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            if (sm.isInFlushableState() && addedCount < maxEmojiCount) {
233fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
234f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        sm.getCurrentMetadata())) {
235f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    if (spannable == null) {
236f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        spannable = new SpannableString(charSequence);
237f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    }
238f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset);
239f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    addedCount++;
24082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
241f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
242f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            return spannable == null ? charSequence : spannable;
243f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        } finally {
244f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (isSpannableBuilder) {
245f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                ((SpannableBuilder) charSequence).endBatchEdit();
24682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
24782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
24882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
24982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
25082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
25182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
25282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
25382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
25482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted with the characters it covers.
25582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
25682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
25782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
25882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
25982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
26082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                 Editable, int, KeyEvent)}
26182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
26282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                int, KeyEvent)}
26382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
26482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              int, KeyEvent)}
26582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
26682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
26782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
26882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
26982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final KeyEvent event) {
27082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final boolean handled;
27182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        switch (keyCode) {
27282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_DEL:
27382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, false /*forwardDelete*/);
27482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
27582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_FORWARD_DEL:
27682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, true /*forwardDelete*/);
27782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
27882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            default:
27982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = false;
28082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
28182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
28282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
28382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (handled) {
28482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetaKeyKeyListener.adjustMetaAfterKeypress(editable);
28582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
28682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
28782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
28882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
28982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
29082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
29182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean delete(final Editable content, final KeyEvent event,
29282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final boolean forwardDelete) {
29382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasModifiers(event)) {
29482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
29582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
29682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
29782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int start = Selection.getSelectionStart(content);
29882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = Selection.getSelectionEnd(content);
29982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(start, end)) {
30082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
30182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
30282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
30382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class);
30482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
30582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
30682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
30782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
30882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanStart = content.getSpanStart(span);
30982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanEnd = content.getSpanEnd(span);
31082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if ((forwardDelete && spanStart == start)
31182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (!forwardDelete && spanEnd == start)
31282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (start > spanStart && start < spanEnd)) {
31382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    content.delete(spanStart, spanEnd);
31482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return true;
31582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
31682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
31782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
31882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
31982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
32082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
32182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
32282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
32382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
32482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
32582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted.
32682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
32782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
32882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
32982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
33082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inputConnection InputConnection instance
33182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable TextView.Editable instance
33282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param beforeLength the number of characters before the cursor to be deleted
33382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param afterLength the number of characters after the cursor to be deleted
33482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inCodePoints {@code true} if length parameters are in codepoints
33582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
33682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
33782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
33882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection,
33982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength,
34082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @IntRange(from = 0) final int afterLength, final boolean inCodePoints) {
341fb15b88b8ff336c2f8c2dff88e55eaba3491349aSiyamed Sinir        //noinspection ConstantConditions
34282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (editable == null || inputConnection == null) {
34382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
34482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
34582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
34682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (beforeLength < 0 || afterLength < 0) {
34782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
34882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
34982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
35082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionStart = Selection.getSelectionStart(editable);
35182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionEnd = Selection.getSelectionEnd(editable);
35282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
35382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(selectionStart, selectionEnd)) {
35482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
35582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
35682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
35782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int start;
35882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int end;
35982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (inCodePoints) {
36082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            // go backwards in terms of codepoints
36182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = CodepointIndexFinder.findIndexBackward(editable, selectionStart,
36282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(beforeLength, 0));
36382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = CodepointIndexFinder.findIndexForward(editable, selectionEnd,
36482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(afterLength, 0));
36582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
36682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (start == CodepointIndexFinder.INVALID_INDEX
36782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || end == CodepointIndexFinder.INVALID_INDEX) {
36882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return false;
36982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
37082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        } else {
37182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(selectionStart - beforeLength, 0);
37282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(selectionEnd + afterLength, editable.length());
37382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
37482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class);
37682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
37782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
37882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
37982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
38082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanStart = editable.getSpanStart(span);
38182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanEnd = editable.getSpanEnd(span);
38282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                start = Math.min(spanStart, start);
38382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                end = Math.max(spanEnd, end);
38482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
38582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
38682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(start, 0);
38782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(end, editable.length());
38882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
38982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.beginBatchEdit();
39082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            editable.delete(start, end);
39182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.endBatchEdit();
39282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
39382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
39482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
39582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
39682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
39782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
39882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasInvalidSelection(final int start, final int end) {
39982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return start == -1 || end == -1 || start != end;
40082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
40182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
40282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasModifiers(KeyEvent event) {
40382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
40482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
40582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
40682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata,
40782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int start, final int end) {
40882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan span = mSpanFactory.createSpan(metadata);
40982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
41082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
41182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
41282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
41382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks whether the current OS can render a given emoji. Used by the system to decide if an
41482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * emoji span should be added. If the system cannot render it, an emoji span will be added.
41582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Used only for the case where replaceAll is set to {@code false}.
41682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
41782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence the CharSequence that the emoji is in
41882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index of the emoji in the CharSequence
41982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index of the emoji in the CharSequence
42082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param metadata EmojiMetadata instance for the emoji
42182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
42282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if the OS can render emoji, {@code false} otherwise
42382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
42482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private boolean hasGlyph(final CharSequence charSequence, int start, final int end,
42582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final EmojiMetadata metadata) {
426611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // For pre M devices, heuristic in PaintCompat can result in false positives. we are
427611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // adding another heuristic using the sdkAdded field. if the emoji was added to OS
428611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // at a later version we assume that the system probably cannot render it.
429611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        if (Build.VERSION.SDK_INT < 23 && metadata.getSdkAdded() > Build.VERSION.SDK_INT) {
43082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
43182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
43282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
43382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // if the existence is not calculated yet
43482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) {
435fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end);
43682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            metadata.setHasGlyph(hasGlyph);
43782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
43882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
43982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS;
44082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
44182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
442fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    /**
443fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Set the GlyphChecker instance used by EmojiProcessor. Used for testing.
444fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     */
445fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    void setGlyphChecker(@NonNull final GlyphChecker glyphChecker) {
446fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        Preconditions.checkNotNull(glyphChecker);
447fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        mGlyphChecker = glyphChecker;
44882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
44982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
45082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
45182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State machine for walking over the metadata trie.
45282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
45382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static final class ProcessorSm {
45482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
45582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_DEFAULT = 1;
45682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_WALKING = 2;
45782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
45882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mState = STATE_DEFAULT;
45982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
46182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Root of the trie
46282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
46382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private final MetadataRepo.Node mRootNode;
46482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
46682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Pointer to the node after last codepoint.
46782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
46882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mCurrentNode;
46982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
47082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
47182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The node where ACTION_FLUSH is called. Required since after flush action is
47282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * returned mCurrentNode is reset to be the root.
47382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
47482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mFlushNode;
47582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
47682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
47782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The code point that was checked.
47882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
47982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mLastCodepoint;
48082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
48282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Level for mCurrentNode. Root is 0.
48382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
48482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mCurrentDepth;
48582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        ProcessorSm(MetadataRepo.Node rootNode) {
48782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mRootNode = rootNode;
48882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = rootNode;
48982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
49082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
49182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
49282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int check(final int codePoint) {
49382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action;
49482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetadataRepo.Node node = mCurrentNode.get(codePoint);
49582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            switch (mState) {
49682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_WALKING:
49782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node != null) {
49882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
49982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth += 1;
50082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
50182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
50282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        if (isTextStyle(codePoint)) {
50382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
50482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (isEmojiStyle(codePoint)) {
50582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = ACTION_ADVANCE_END;
50682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (mCurrentNode.getData() != null) {
50782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            if (mCurrentDepth == 1) {
50882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                if (mCurrentNode.getData().isDefaultEmoji()
50982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                        || isEmojiStyle(mLastCodepoint)) {
51082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    mFlushNode = mCurrentNode;
51182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = ACTION_FLUSH;
51282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    reset();
51382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                } else {
51482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = reset();
51582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                }
51682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            } else {
51782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                mFlushNode = mCurrentNode;
51882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                action = ACTION_FLUSH;
51982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                reset();
52082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            }
52182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else {
52282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
52382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
52482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
52582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
52682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_DEFAULT:
52782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                default:
52882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node == null) {
52982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = reset();
53082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
53182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mState = STATE_WALKING;
53282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
53382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth = 1;
53482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
53582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
53682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
53782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
53882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
53982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mLastCodepoint = codePoint;
54082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return action;
54182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
54282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
54382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
54482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int reset() {
54582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mState = STATE_DEFAULT;
54682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = mRootNode;
54782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentDepth = 0;
54882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return ACTION_ADVANCE_BOTH;
54982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
55082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
55182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
55282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return the metadata node when ACTION_FLUSH is returned
55382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
55482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getFlushMetadata() {
55582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mFlushNode.getData();
55682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
55782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
55882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
55982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return current pointer to the metadata node in the trie
56082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
56182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getCurrentMetadata() {
56282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mCurrentNode.getData();
56382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
56482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
56582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
56682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Need for the case where input is consumed, but action_flush was not called. For example
56782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * when the char sequence has single codepoint character which is a default emoji. State
56882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * machine will wait for the next.
56982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
57082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return whether the current state requires an emoji to be added
57182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
57282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        boolean isInFlushableState() {
57382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mState == STATE_WALKING && mCurrentNode.getData() != null
57482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    && (mCurrentNode.getData().isDefaultEmoji()
57582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || isEmojiStyle(mLastCodepoint)
57682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || mCurrentDepth > 1);
57782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
57882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
57982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
58082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
58182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
58282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a emoji style standardized variation selector
58382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
58482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isEmojiStyle(int codePoint) {
58582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0F;
58682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
58782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
58882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
58982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
59082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
59182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a text style standardized variation selector
59282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
59382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isTextStyle(int codePoint) {
59482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0E;
59582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
59682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
59782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
59882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
59982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
60082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
60182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final class CodepointIndexFinder {
60282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int INVALID_INDEX = -1;
60382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
60482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
60582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} behind
60682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
60782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
60882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
60982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going backwards
61082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
61182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
61282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
61382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
61482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexBackward(final CharSequence cs, final int from,
61582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
61682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
61782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingHighSurrogate = false;
61882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
61982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
62082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
62182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
62282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
62382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
62482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
62582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
62682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
62782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
62882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
62982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
63082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
63182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                --currentIndex;
63282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex < 0) {
63382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingHighSurrogate) {
63482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
63582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
63682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return 0;  // Reached to the beginning of the text w/o any invalid surrogate
63782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
63882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
63982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
64082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingHighSurrogate) {
64182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isHighSurrogate(c)) {
64282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
64382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
64482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingHighSurrogate = false;
64582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
64682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
64782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
64882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
64982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
65082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
65182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
65282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isHighSurrogate(c)) {
65382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
65482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
65582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingHighSurrogate = true;
65682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
65782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
65882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
65982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
66082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead
66182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
66282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
66382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
66482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going forward
66582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
66682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
66782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
66882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
66982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexForward(final CharSequence cs, final int from,
67082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
67182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
67282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingLowSurrogate = false;
67382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
67482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
67582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
67682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
67782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
67882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
67982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
68082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
68182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
68282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
68382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
68482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
68582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
68682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
68782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex >= length) {
68882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingLowSurrogate) {
68982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
69082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
69182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return length;  // Reached to the end of the text w/o any invalid surrogate
69282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
69382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
69482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
69582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingLowSurrogate) {
69682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isLowSurrogate(c)) {
69782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
69882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
69982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
70082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingLowSurrogate = false;
70182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
70282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
70382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
70482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
70582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
70682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
70782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
70882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
70982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isLowSurrogate(c)) {
71082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
71182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
71282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingLowSurrogate = true;
71382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                ++currentIndex;
71482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
71582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
71682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
717fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
718fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    /**
719fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Utility class that checks if the system can render a given glyph.
720fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     *
721fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * @hide
722fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     */
723fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    @AnyThread
724fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    @RestrictTo(LIBRARY_GROUP)
725fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    public static class GlyphChecker {
726fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
727fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Default text size for {@link #mTextPaint}.
728fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
729fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static final int PAINT_TEXT_SIZE = 10;
730fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
731fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
732fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Used to create strings required by
733fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
734fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
735fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
736fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
737fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
738fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
739fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
740fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private final TextPaint mTextPaint;
741fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
742fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        GlyphChecker() {
743fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            mTextPaint = new TextPaint();
744fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            mTextPaint.setTextSize(PAINT_TEXT_SIZE);
745fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
746fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
747fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
748fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Returns whether the system can render an emoji.
749fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         *
750fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param charSequence the CharSequence that the emoji is in
751fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param start start index of the emoji in the CharSequence
752fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param end end index of the emoji in the CharSequence
753fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         *
754fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @return {@code true} if the OS can render emoji, {@code false} otherwise
755fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
756fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        public boolean hasGlyph(final CharSequence charSequence, int start, final int end) {
757fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final StringBuilder builder = getStringBuilder();
758fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            builder.setLength(0);
759fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
760fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            while (start < end) {
761fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                builder.append(charSequence.charAt(start));
762fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                start++;
763fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            }
764fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
765fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            return PaintCompat.hasGlyph(mTextPaint, builder.toString());
766fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
767fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
768fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static StringBuilder getStringBuilder() {
769fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            if (sStringBuilder.get() == null) {
770fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                sStringBuilder.set(new StringBuilder());
771fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            }
772fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            return sStringBuilder.get();
773fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
774fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
775fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    }
77682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir}
777