182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/*
2ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikas * Copyright 2018 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 */
16ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikaspackage androidx.emoji.text;
1782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
18ac5fe7c617c66850fff75a9fce9979c6e5674b0fAurimas Liutikasimport static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
1982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
2082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.os.Build;
2182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Editable;
2282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Selection;
2382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spannable;
2482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.SpannableString;
2582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spanned;
2682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.TextPaint;
2782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.KeyListener;
2882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.MetaKeyKeyListener;
2982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.KeyEvent;
3082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.inputmethod.InputConnection;
3182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
3238746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.AnyThread;
3338746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.IntDef;
3438746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.IntRange;
3538746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.NonNull;
3638746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.Nullable;
3738746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.RequiresApi;
3838746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.annotation.RestrictTo;
3938746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.core.graphics.PaintCompat;
4038746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.core.util.Preconditions;
4138746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikasimport androidx.emoji.widget.SpannableBuilder;
4238746a682208c764867ffe4415d0b62fb22b5b9aAurimas Liutikas
4382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.Retention;
4482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.RetentionPolicy;
45ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popaimport java.util.Arrays;
46ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popaimport java.util.List;
4782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
4882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/**
4982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Processes the CharSequence and adds the emojis.
5082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
5182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @hide
5282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */
5382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@AnyThread
5482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@RestrictTo(LIBRARY_GROUP)
5577b5c5b734f9f665577d1e3d178615db43ae1d4fSiyamed Sinir@RequiresApi(19)
5682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirfinal class EmojiProcessor {
5782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
5882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
5982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State transition commands.
6082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
6182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH})
6282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @Retention(RetentionPolicy.SOURCE)
63fb15b88b8ff336c2f8c2dff88e55eaba3491349aSiyamed Sinir    private @interface Action {
6482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
6582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
6682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
6782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance the end pointer in CharSequence and reset the start to be the end.
6882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
6982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_BOTH = 1;
7082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
7182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
7282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance end pointer in CharSequence.
7382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
7482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_END = 2;
7582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
7682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
7782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end
7882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * pointer in CharSequence and reset the start to be the end.
7982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
8082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_FLUSH = 3;
8182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
830bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     * Factory used to create EmojiSpans.
8482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
850bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir    private final EmojiCompat.SpanFactory mSpanFactory;
8682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
8882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Emoji metadata repository.
8982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
9082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final MetadataRepo mMetadataRepo;
9182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
9282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
93fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Utility class that checks if the system can render a given glyph.
9482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
95fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    private GlyphChecker mGlyphChecker = new GlyphChecker();
9682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
97ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa    /**
98ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa     * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
99ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa     */
100ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa    private final boolean mUseEmojiAsDefaultStyle;
101ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa
102ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa    /**
103ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa     * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
104ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa     */
105ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa    private final int[] mEmojiAsDefaultStyleExceptions;
106ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa
10782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiProcessor(@NonNull final MetadataRepo metadataRepo,
108ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            @NonNull final EmojiCompat.SpanFactory spanFactory,
109ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            final boolean useEmojiAsDefaultStyle,
110ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            @Nullable final int[] emojiAsDefaultStyleExceptions) {
11182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mSpanFactory = spanFactory;
11282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mMetadataRepo = metadataRepo;
113ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
114ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
11582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
11682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
11782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
118ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
119ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
12082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = charSequence.length();
12182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int currentOffset = 0;
12282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
12382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        while (currentOffset < end) {
12482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int codePoint = Character.codePointAt(charSequence, currentOffset);
12582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action = sm.check(codePoint);
12682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (action != ACTION_ADVANCE_END) {
12782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return null;
12882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
12982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            currentOffset += Character.charCount(codePoint);
13082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
13182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
13282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (sm.isInFlushableState()) {
13382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return sm.getCurrentMetadata();
13482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
13582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
13682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return null;
13782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
13882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
13982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
14082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
14182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p>
14282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <ul>
14382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If no emojis are found, {@code charSequence} given as the input is returned without
14482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
14582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * returned.</li>
14682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
14782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * a new {@link android.text.Spannable} instance is returned. </li>
14882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is a Spannable, the same instance is returned. </li>
14982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * </ul>
15082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
15182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
15282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index in the charSequence to look for emojis, should be greater than or
15382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              equal to {@code 0}, also less than {@code charSequence.length()}
15482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index in the charSequence to look for emojis, should be greater than or
15582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *            equal to {@code start} parameter, also less than {@code charSequence.length()}
1560bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
1570bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir     *                      than or equal to {@code 0}
158fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * @param replaceAll whether to replace all emoji with {@link EmojiSpan}s
15982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
16082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
161fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount,
162fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final boolean replaceAll) {
163f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder;
164f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        if (isSpannableBuilder) {
165f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            ((SpannableBuilder) charSequence).beginBatchEdit();
16682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
16782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
168f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        try {
169f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            Spannable spannable = null;
170f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // if it is a spannable already, use the same instance to add/remove EmojiSpans.
171f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // otherwise wait until the the first EmojiSpan found in order to change the result
172f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // into a Spannable.
173f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (isSpannableBuilder || charSequence instanceof Spannable) {
174f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                spannable = (Spannable) charSequence;
175c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir            } else if (charSequence instanceof Spanned) {
176c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                // check if there are any EmojiSpans as cheap as possible
177c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                // start-1, end+1 will return emoji span that starts/ends at start/end indices
178c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                final int nextSpanTransition = ((Spanned) charSequence).nextSpanTransition(
179c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                        start - 1, end + 1, EmojiSpan.class);
180c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir
181c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                if (nextSpanTransition <= end) {
182c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                    spannable = new SpannableString(charSequence);
183c59a6c5f501efc56fcfa0fbdb0a52fcb0c5186ebSiyamed Sinir                }
184f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
185f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir
186f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (spannable != null) {
187f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class);
188f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                if (spans != null && spans.length > 0) {
189f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    // remove existing spans, and realign the start, end according to spans
190f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    // if start or end is in the middle of an emoji they should be aligned
191f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    final int length = spans.length;
192f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    for (int index = 0; index < length; index++) {
193f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final EmojiSpan span = spans[index];
194f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final int spanStart = spannable.getSpanStart(span);
195f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        final int spanEnd = spannable.getSpanEnd(span);
196f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // Remove span only when its spanStart is NOT equal to current end.
197f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // During add operation an emoji at index 0 is added with 0-1 as start and
198f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // end indices. Therefore if there are emoji spans at [0-1] and [1-2]
199f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        // and end is 1, the span between 0-1 should be deleted, not 1-2.
200f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (spanStart != end) {
201f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            spannable.removeSpan(span);
202f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
203f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        start = Math.min(spanStart, start);
204f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        end = Math.max(spanEnd, end);
20582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
20682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
20782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
20882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
209f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (start == end || start >= charSequence.length()) {
210f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                return charSequence;
211f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
21282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
2130bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            // calculate max number of emojis that can be added. since getSpans call is a relatively
2140bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            // expensive operation, do it only when maxEmojiCount is not unlimited.
21536d5e1b59cef90bb72936e6641970e04b7bde18cAurimas Liutikas            if (maxEmojiCount != EmojiCompat.EMOJI_COUNT_UNLIMITED && spannable != null) {
2160bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir                maxEmojiCount -= spannable.getSpans(0, spannable.length(), EmojiSpan.class).length;
2170bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            }
218f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // add new ones
219f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int addedCount = 0;
220ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
221ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                    mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
22282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
223f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int currentOffset = start;
224f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            int codePoint = Character.codePointAt(charSequence, currentOffset);
22582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
2260bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            while (currentOffset < end && addedCount < maxEmojiCount) {
227f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                final int action = sm.check(codePoint);
22882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
229f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                switch (action) {
230f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_ADVANCE_BOTH:
2317f51b0e4448c80ef32a8c41d90f8bc91c55ed3d4Siyamed Sinir                        start += Character.charCount(Character.codePointAt(charSequence, start));
2327f51b0e4448c80ef32a8c41d90f8bc91c55ed3d4Siyamed Sinir                        currentOffset = start;
233f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (currentOffset < end) {
234f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            codePoint = Character.codePointAt(charSequence, currentOffset);
23582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
236f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
237f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_ADVANCE_END:
238f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        currentOffset += Character.charCount(codePoint);
239f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        if (currentOffset < end) {
240f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            codePoint = Character.codePointAt(charSequence, currentOffset);
241f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
242f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
243f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    case ACTION_FLUSH:
244fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                        if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
245f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                                sm.getFlushMetadata())) {
246f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            if (spannable == null) {
247f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                                spannable = new SpannableString(charSequence);
248f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            }
249f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset);
250f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                            addedCount++;
251f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        }
252f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        start = currentOffset;
253f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        break;
254f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                }
25582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
25682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
257f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // After the last codepoint is consumed the state machine might be in a state where it
258f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed
259f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ).
260f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            // Need to check if it is in such a state.
2610bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir            if (sm.isInFlushableState() && addedCount < maxEmojiCount) {
262fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
263f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        sm.getCurrentMetadata())) {
264f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    if (spannable == null) {
265f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                        spannable = new SpannableString(charSequence);
266f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    }
267f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset);
268f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                    addedCount++;
26982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
270f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            }
271f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            return spannable == null ? charSequence : spannable;
272f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir        } finally {
273f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir            if (isSpannableBuilder) {
274f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir                ((SpannableBuilder) charSequence).endBatchEdit();
27582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
27682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
27782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
27882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
27982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
28082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
28182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
28282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
28382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted with the characters it covers.
28482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
28582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
28682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
28782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
28882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
28982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                 Editable, int, KeyEvent)}
29082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
29182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                int, KeyEvent)}
29282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
29382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              int, KeyEvent)}
29482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
29582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
29682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
29782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
29882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final KeyEvent event) {
29982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final boolean handled;
30082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        switch (keyCode) {
30182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_DEL:
30282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, false /*forwardDelete*/);
30382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
30482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_FORWARD_DEL:
30582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, true /*forwardDelete*/);
30682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
30782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            default:
30882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = false;
30982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
31082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
31182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
31282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (handled) {
31382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetaKeyKeyListener.adjustMetaAfterKeypress(editable);
31482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
31582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
31682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
31782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
31882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
31982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
32082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean delete(final Editable content, final KeyEvent event,
32182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final boolean forwardDelete) {
32282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasModifiers(event)) {
32382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
32482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
32582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
32682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int start = Selection.getSelectionStart(content);
32782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = Selection.getSelectionEnd(content);
32882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(start, end)) {
32982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
33082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
33182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
33282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class);
33382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
33482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
33582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
33682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
33782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanStart = content.getSpanStart(span);
33882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanEnd = content.getSpanEnd(span);
33982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if ((forwardDelete && spanStart == start)
34082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (!forwardDelete && spanEnd == start)
34182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (start > spanStart && start < spanEnd)) {
34282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    content.delete(spanStart, spanEnd);
34382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return true;
34482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
34582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
34682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
34782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
34882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
34982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
35082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
35182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
35282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
35382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
35482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted.
35582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
35682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
35782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
35882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
35982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inputConnection InputConnection instance
36082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable TextView.Editable instance
36182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param beforeLength the number of characters before the cursor to be deleted
36282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param afterLength the number of characters after the cursor to be deleted
36382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inCodePoints {@code true} if length parameters are in codepoints
36482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
36582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
36682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
36782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection,
36882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength,
36982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @IntRange(from = 0) final int afterLength, final boolean inCodePoints) {
370fb15b88b8ff336c2f8c2dff88e55eaba3491349aSiyamed Sinir        //noinspection ConstantConditions
37182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (editable == null || inputConnection == null) {
37282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
37382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
37482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (beforeLength < 0 || afterLength < 0) {
37682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
37782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
37882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionStart = Selection.getSelectionStart(editable);
38082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionEnd = Selection.getSelectionEnd(editable);
38182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
38282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(selectionStart, selectionEnd)) {
38382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
38482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
38582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
38682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int start;
38782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int end;
38882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (inCodePoints) {
38982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            // go backwards in terms of codepoints
39082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = CodepointIndexFinder.findIndexBackward(editable, selectionStart,
39182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(beforeLength, 0));
39282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = CodepointIndexFinder.findIndexForward(editable, selectionEnd,
39382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(afterLength, 0));
39482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
39582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (start == CodepointIndexFinder.INVALID_INDEX
39682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || end == CodepointIndexFinder.INVALID_INDEX) {
39782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return false;
39882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
39982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        } else {
40082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(selectionStart - beforeLength, 0);
40182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(selectionEnd + afterLength, editable.length());
40282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
40382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
40482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class);
40582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
40682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
40782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
40882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
40982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanStart = editable.getSpanStart(span);
41082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanEnd = editable.getSpanEnd(span);
41182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                start = Math.min(spanStart, start);
41282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                end = Math.max(spanEnd, end);
41382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
41482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
41582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(start, 0);
41682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(end, editable.length());
41782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
41882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.beginBatchEdit();
41982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            editable.delete(start, end);
42082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.endBatchEdit();
42182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
42282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
42382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
42482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
42582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
42682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
42782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasInvalidSelection(final int start, final int end) {
42882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return start == -1 || end == -1 || start != end;
42982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
43082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
43182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasModifiers(KeyEvent event) {
43282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
43382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
43482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
43582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata,
43682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int start, final int end) {
43782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan span = mSpanFactory.createSpan(metadata);
43882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
43982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
44082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
44182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
44282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks whether the current OS can render a given emoji. Used by the system to decide if an
44382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * emoji span should be added. If the system cannot render it, an emoji span will be added.
44482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Used only for the case where replaceAll is set to {@code false}.
44582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
44682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence the CharSequence that the emoji is in
44782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index of the emoji in the CharSequence
44882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index of the emoji in the CharSequence
44982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param metadata EmojiMetadata instance for the emoji
45082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
45182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if the OS can render emoji, {@code false} otherwise
45282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
45382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private boolean hasGlyph(final CharSequence charSequence, int start, final int end,
45482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final EmojiMetadata metadata) {
455611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // For pre M devices, heuristic in PaintCompat can result in false positives. we are
456611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // adding another heuristic using the sdkAdded field. if the emoji was added to OS
457611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        // at a later version we assume that the system probably cannot render it.
458611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir        if (Build.VERSION.SDK_INT < 23 && metadata.getSdkAdded() > Build.VERSION.SDK_INT) {
45982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
46082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
46182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // if the existence is not calculated yet
46382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) {
464fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end);
46582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            metadata.setHasGlyph(hasGlyph);
46682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
46782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS;
46982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
47082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
471fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    /**
472fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Set the GlyphChecker instance used by EmojiProcessor. Used for testing.
473fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     */
474fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    void setGlyphChecker(@NonNull final GlyphChecker glyphChecker) {
475fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        Preconditions.checkNotNull(glyphChecker);
476fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        mGlyphChecker = glyphChecker;
47782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
47882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
47982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
48082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State machine for walking over the metadata trie.
48182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
48282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static final class ProcessorSm {
48382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_DEFAULT = 1;
48582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_WALKING = 2;
48682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mState = STATE_DEFAULT;
48882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
49082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Root of the trie
49182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
49282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private final MetadataRepo.Node mRootNode;
49382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
49482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
49582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Pointer to the node after last codepoint.
49682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
49782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mCurrentNode;
49882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
49982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
50082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The node where ACTION_FLUSH is called. Required since after flush action is
50182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * returned mCurrentNode is reset to be the root.
50282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
50382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mFlushNode;
50482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
50582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
50682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The code point that was checked.
50782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
50882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mLastCodepoint;
50982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
51082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
51182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Level for mCurrentNode. Root is 0.
51282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
51382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mCurrentDepth;
51482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
515ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        /**
516ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa         * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
517ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa         */
518ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        private final boolean mUseEmojiAsDefaultStyle;
519ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa
520ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        /**
521ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa         * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
522ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa         */
523ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        private final int[] mEmojiAsDefaultStyleExceptions;
524ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa
525ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle,
526ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                int[] emojiAsDefaultStyleExceptions) {
52782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mRootNode = rootNode;
52882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = rootNode;
529ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
530ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
53182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
53282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
53382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
53482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int check(final int codePoint) {
53582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action;
53682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetadataRepo.Node node = mCurrentNode.get(codePoint);
53782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            switch (mState) {
53882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_WALKING:
53982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node != null) {
54082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
54182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth += 1;
54282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
54382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
54482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        if (isTextStyle(codePoint)) {
54582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
54682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (isEmojiStyle(codePoint)) {
54782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = ACTION_ADVANCE_END;
54882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (mCurrentNode.getData() != null) {
54982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            if (mCurrentDepth == 1) {
550ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                                if (shouldUseEmojiPresentationStyleForSingleCodepoint()) {
55182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    mFlushNode = mCurrentNode;
55282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = ACTION_FLUSH;
55382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    reset();
55482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                } else {
55582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = reset();
55682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                }
55782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            } else {
55882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                mFlushNode = mCurrentNode;
55982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                action = ACTION_FLUSH;
56082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                reset();
56182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            }
56282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else {
56382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
56482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
56582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
56682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
56782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_DEFAULT:
56882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                default:
56982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node == null) {
57082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = reset();
57182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
57282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mState = STATE_WALKING;
57382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
57482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth = 1;
57582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
57682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
57782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
57882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
57982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
58082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mLastCodepoint = codePoint;
58182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return action;
58282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
58382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
58482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
58582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int reset() {
58682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mState = STATE_DEFAULT;
58782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = mRootNode;
58882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentDepth = 0;
58982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return ACTION_ADVANCE_BOTH;
59082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
59182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
59282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
59382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return the metadata node when ACTION_FLUSH is returned
59482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
59582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getFlushMetadata() {
59682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mFlushNode.getData();
59782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
59882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
59982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
60082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return current pointer to the metadata node in the trie
60182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
60282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getCurrentMetadata() {
60382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mCurrentNode.getData();
60482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
60582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
60682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
60782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Need for the case where input is consumed, but action_flush was not called. For example
60882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * when the char sequence has single codepoint character which is a default emoji. State
60982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * machine will wait for the next.
61082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
61182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return whether the current state requires an emoji to be added
61282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
61382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        boolean isInFlushableState() {
61482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mState == STATE_WALKING && mCurrentNode.getData() != null
615ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                    && (mCurrentDepth > 1 || shouldUseEmojiPresentationStyleForSingleCodepoint());
616ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        }
617ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa
618ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa        private boolean shouldUseEmojiPresentationStyleForSingleCodepoint() {
619ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            if (mCurrentNode.getData().isDefaultEmoji()) {
620ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                // The codepoint is emoji style by default.
621ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                return true;
622ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            }
623ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            if (isEmojiStyle(mLastCodepoint)) {
624ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                // The codepoint was followed by the emoji style variation selector.
625ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                return true;
626ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            }
627ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            if (mUseEmojiAsDefaultStyle) {
628ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                // Emoji presentation style for text style default emojis is enabled. We have
629ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                // to check that the current codepoint is not an exception.
630ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                if (mEmojiAsDefaultStyleExceptions == null) {
631ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                    return true;
632ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                }
633ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                final int codepoint = mCurrentNode.getData().getCodepointAt(0);
634ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                final int index = Arrays.binarySearch(mEmojiAsDefaultStyleExceptions, codepoint);
635ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                if (index < 0) {
636ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                    // Index is negative, so the codepoint was not found in the array of exceptions.
637ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                    return true;
638ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa                }
639ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            }
640ca429f4bd64320e424b2e984c2e9d615679fa9cdMihai Popa            return false;
64182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
64282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
64382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
64482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
64582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
64682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a emoji style standardized variation selector
64782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
64882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isEmojiStyle(int codePoint) {
64982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0F;
65082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
65182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
65282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
65382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
65482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
65582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a text style standardized variation selector
65682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
65782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isTextStyle(int codePoint) {
65882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0E;
65982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
66082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
66182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
66282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
66382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
66482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
66582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final class CodepointIndexFinder {
66682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int INVALID_INDEX = -1;
66782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
66882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
66982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} behind
67082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
67182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
67282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
67382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going backwards
67482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
67582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
67682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
67782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
67882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexBackward(final CharSequence cs, final int from,
67982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
68082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
68182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingHighSurrogate = false;
68282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
68382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
68482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
68582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
68682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
68782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
68882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
68982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
69082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
69182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
69282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
69382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
69482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
69582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                --currentIndex;
69682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex < 0) {
69782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingHighSurrogate) {
69882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
69982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
70082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return 0;  // Reached to the beginning of the text w/o any invalid surrogate
70182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
70282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
70382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
70482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingHighSurrogate) {
70582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isHighSurrogate(c)) {
70682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
70782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
70882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingHighSurrogate = false;
70982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
71082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
71182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
71282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
71382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
71482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
71582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
71682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isHighSurrogate(c)) {
71782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
71882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
71982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingHighSurrogate = true;
72082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
72182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
72282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
72382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
72482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead
72582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
72682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
72782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
72882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going forward
72982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
73082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
73182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
73282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
73382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexForward(final CharSequence cs, final int from,
73482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
73582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
73682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingLowSurrogate = false;
73782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
73882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
73982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
74082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
74182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
74282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
74382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
74482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
74582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
74682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
74782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
74882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
74982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
75082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
75182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex >= length) {
75282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingLowSurrogate) {
75382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
75482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
75582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return length;  // Reached to the end of the text w/o any invalid surrogate
75682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
75782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
75882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
75982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingLowSurrogate) {
76082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isLowSurrogate(c)) {
76182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
76282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
76382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
76482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingLowSurrogate = false;
76582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
76682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
76782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
76882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
76982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
77082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
77182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
77282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
77382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isLowSurrogate(c)) {
77482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
77582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
77682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingLowSurrogate = true;
77782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                ++currentIndex;
77882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
77982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
78082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
781fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
782fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    /**
783fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * Utility class that checks if the system can render a given glyph.
784fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     *
785fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     * @hide
786fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir     */
787fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    @AnyThread
788fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    @RestrictTo(LIBRARY_GROUP)
789fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    public static class GlyphChecker {
790fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
791fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Default text size for {@link #mTextPaint}.
792fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
793fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static final int PAINT_TEXT_SIZE = 10;
794fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
795fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
796fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Used to create strings required by
797fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
798fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
799fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
800fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
801fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
802fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
803fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
804fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private final TextPaint mTextPaint;
805fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
806fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        GlyphChecker() {
807fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            mTextPaint = new TextPaint();
808fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            mTextPaint.setTextSize(PAINT_TEXT_SIZE);
809fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
810fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
811fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        /**
812fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * Returns whether the system can render an emoji.
813fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         *
814fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param charSequence the CharSequence that the emoji is in
815fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param start start index of the emoji in the CharSequence
816fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @param end end index of the emoji in the CharSequence
817fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         *
818fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         * @return {@code true} if the OS can render emoji, {@code false} otherwise
819fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir         */
820fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        public boolean hasGlyph(final CharSequence charSequence, int start, final int end) {
821fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            final StringBuilder builder = getStringBuilder();
822fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            builder.setLength(0);
823fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
824fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            while (start < end) {
825fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                builder.append(charSequence.charAt(start));
826fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                start++;
827fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            }
828fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
829fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            return PaintCompat.hasGlyph(mTextPaint, builder.toString());
830fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
831fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
832fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        private static StringBuilder getStringBuilder() {
833fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            if (sStringBuilder.get() == null) {
834fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir                sStringBuilder.set(new StringBuilder());
835fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            }
836fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir            return sStringBuilder.get();
837fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir        }
838fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir
839fdc5f7bbb0e22b5d6e400327d9d4b732b07dec75Siyamed Sinir    }
84082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir}
841