EmojiProcessor.java revision 82d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439
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;
2582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.RestrictTo;
2682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.v4.graphics.PaintCompat;
2782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.v4.util.Preconditions;
2882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Editable;
2982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Selection;
3082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spannable;
3182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.SpannableString;
3282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spanned;
3382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.TextPaint;
3482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.KeyListener;
3582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.MetaKeyKeyListener;
3682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.KeyEvent;
3782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.inputmethod.InputConnection;
3882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
3982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.Retention;
4082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.RetentionPolicy;
4182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
4282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/**
4382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Processes the CharSequence and adds the emojis.
4482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir *
4582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @hide
4682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */
4782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@AnyThread
4882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@RestrictTo(LIBRARY_GROUP)
4982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirfinal class EmojiProcessor {
5082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
5182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
5282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State transition commands.
5382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
5482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH})
5582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @Retention(RetentionPolicy.SOURCE)
5682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    @interface Action {
5782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
5882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
5982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
6082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance the end pointer in CharSequence and reset the start to be the end.
6182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
6282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_BOTH = 1;
6382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
6482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
6582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Advance end pointer in CharSequence.
6682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
6782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_ADVANCE_END = 2;
6882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
6982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
7082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end
7182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * pointer in CharSequence and reset the start to be the end.
7282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
7382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int ACTION_FLUSH = 3;
7482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
7582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
7682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Default text size for {@link #mTextPaint}.
7782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
7882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final int PAINT_TEXT_SIZE = 10;
7982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
8182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Used to create strings required by
8282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
8382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
8482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
8582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
8682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
8782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Factory used to create EmojiSpans.
8882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
8982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final EmojiCompat.SpanFactory mSpanFactory;
9082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
9182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
9282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @see EmojiCompat.Config#setMaxEmojiPerText(int)
9382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
9482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final int mMaxEmojiPerText;
9582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
9682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
9782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @see EmojiCompat.Config#setReplaceAll(boolean)
9882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
9982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final boolean mReplaceAll;
10082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
10182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
10282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Emoji metadata repository.
10382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
10482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final MetadataRepo mMetadataRepo;
10582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
10682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
10782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
10882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
10982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private final TextPaint mTextPaint;
11082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
11182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiProcessor(@NonNull final MetadataRepo metadataRepo,
11282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @NonNull final EmojiCompat.SpanFactory spanFactory,
11382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final boolean replaceAll, @IntRange(from = 0) final int maxEmojiPerText) {
11482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mSpanFactory = spanFactory;
11582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mMaxEmojiPerText = maxEmojiPerText;
11682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mMetadataRepo = metadataRepo;
11782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mReplaceAll = replaceAll;
11882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mTextPaint = new TextPaint();
11982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        mTextPaint.setTextSize(PAINT_TEXT_SIZE);
12082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
12182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
12282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
12382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
12482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = charSequence.length();
12582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int currentOffset = 0;
12682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
12782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        while (currentOffset < end) {
12882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int codePoint = Character.codePointAt(charSequence, currentOffset);
12982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action = sm.check(codePoint);
13082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (action != ACTION_ADVANCE_END) {
13182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return null;
13282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
13382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            currentOffset += Character.charCount(codePoint);
13482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
13582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
13682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (sm.isInFlushableState()) {
13782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return sm.getCurrentMetadata();
13882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
13982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
14082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return null;
14182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
14282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
14382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
14482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
14582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p>
14682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <ul>
14782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If no emojis are found, {@code charSequence} given as the input is returned without
14882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
14982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * returned.</li>
15082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
15182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * a new {@link android.text.Spannable} instance is returned. </li>
15282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <li>If the given input is a Spannable, the same instance is returned. </li>
15382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * </ul>
15482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
15582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
15682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index in the charSequence to look for emojis, should be greater than or
15782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              equal to {@code 0}, also less than {@code charSequence.length()}
15882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index in the charSequence to look for emojis, should be greater than or
15982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *            equal to {@code start} parameter, also less than {@code charSequence.length()}
16082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
16182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
16282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
16382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @IntRange(from = 0) int end) {
16482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Preconditions.checkArgumentNonnegative(start, "start cannot be negative");
16582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Preconditions.checkArgumentNonnegative(end, "end cannot be negative");
16682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Preconditions.checkArgument(start <= end, "start should be <= than end");
16782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
16882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // early return since there is nothing to do
16982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (charSequence == null) {
17082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return charSequence;
17182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
17282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
17382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Preconditions.checkArgument(start <= charSequence.length(),
17482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                "start should be < than charSequence length");
17582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Preconditions.checkArgument(end <= charSequence.length(),
17682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                "end should be < than charSequence length");
17782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
17882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // early return since there is nothing to do
17982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (charSequence.length() == 0 || start == end) {
18082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return charSequence;
18182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
18282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
18382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        Spannable spannable = null;
18482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (charSequence instanceof Spannable) {
18582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            spannable = (Spannable) charSequence;
18682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
18782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
18882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spannable != null) {
18982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class);
19082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (spans != null && spans.length > 0) {
19182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                // remove existing spans, and realign the start, end according to spans
19282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                // if start or end is in the middle of an emoji they should be aligned
19382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int length = spans.length;
19482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                for (int index = 0; index < length; index++) {
19582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    final EmojiSpan span = spans[index];
19682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    final int spanStart = spannable.getSpanStart(span);
19782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    final int spanEnd = spannable.getSpanEnd(span);
19882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // Remove span only when its spanStart is NOT equal to current end.
19982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // During add operation an emoji at index 0 is added with 0-1 as start and
20082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // end indices. Therefore if there are emoji spans at [0-1] and [1-2]
20182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // and end is 1, the span between 0-1 should be deleted, not 1-2.
20282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (spanStart != end) {
20382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        spannable.removeSpan(span);
20482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
20582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    start = Math.min(spanStart, start);
20682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    end = Math.max(spanEnd, end);
20782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
20882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
20982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
21082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
21182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (start == end || start >= charSequence.length()) {
21282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return charSequence;
21382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
21482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
21582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // add new ones
21682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int addedCount = 0;
21782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode());
21882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
21982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int currentOffset = start;
22082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int codePoint = Character.codePointAt(charSequence, currentOffset);
22182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
22282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        while (currentOffset < end && addedCount < mMaxEmojiPerText) {
22382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action = sm.check(codePoint);
22482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
22582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            switch (action) {
22682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case ACTION_ADVANCE_BOTH:
22782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    currentOffset += Character.charCount(codePoint);
22882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    start = currentOffset;
22982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (currentOffset < end) {
23082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        codePoint = Character.codePointAt(charSequence, currentOffset);
23182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
23282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
23382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case ACTION_ADVANCE_END:
23482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    currentOffset += Character.charCount(codePoint);
23582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (currentOffset < end) {
23682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        codePoint = Character.codePointAt(charSequence, currentOffset);
23782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
23882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
23982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case ACTION_FLUSH:
24082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset,
24182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            sm.getFlushMetadata())) {
24282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        if (spannable == null) {
24382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            spannable = new SpannableString(charSequence);
24482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
24582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset);
24682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        addedCount++;
24782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
24882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    start = currentOffset;
24982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
25082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
25182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
25282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
25382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // After the last codepoint is consumed the state machine might be in a state where it
25482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed,
25582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ).
25682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // Need to check if it is in such a state.
25782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (sm.isInFlushableState() && addedCount < mMaxEmojiPerText) {
25882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset,
25982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    sm.getCurrentMetadata())) {
26082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (spannable == null) {
26182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    spannable = new SpannableString(charSequence);
26282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
26382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset);
26482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                addedCount++;
26582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
26682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
26782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
26882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return spannable == null ? charSequence : spannable;
26982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
27082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
27182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
27282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
27382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
27482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
27582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted with the characters it covers.
27682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
27782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
27882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
27982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
28082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
28182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                 Editable, int, KeyEvent)}
28282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
28382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *                int, KeyEvent)}
28482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
28582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *              int, KeyEvent)}
28682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
28782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
28882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
28982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
29082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final KeyEvent event) {
29182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final boolean handled;
29282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        switch (keyCode) {
29382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_DEL:
29482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, false /*forwardDelete*/);
29582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
29682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            case KeyEvent.KEYCODE_FORWARD_DEL:
29782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = delete(editable, event, true /*forwardDelete*/);
29882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
29982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            default:
30082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                handled = false;
30182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                break;
30282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
30382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
30482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (handled) {
30582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetaKeyKeyListener.adjustMetaAfterKeypress(editable);
30682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
30782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
30882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
30982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
31082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
31182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
31282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean delete(final Editable content, final KeyEvent event,
31382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final boolean forwardDelete) {
31482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasModifiers(event)) {
31582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
31682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
31782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
31882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int start = Selection.getSelectionStart(content);
31982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int end = Selection.getSelectionEnd(content);
32082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(start, end)) {
32182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
32282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
32382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
32482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class);
32582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
32682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
32782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
32882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
32982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanStart = content.getSpanStart(span);
33082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int spanEnd = content.getSpanEnd(span);
33182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if ((forwardDelete && spanStart == start)
33282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (!forwardDelete && spanEnd == start)
33382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        || (start > spanStart && start < spanEnd)) {
33482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    content.delete(spanStart, spanEnd);
33582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return true;
33682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
33782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
33882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
33982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
34082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
34182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
34282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
34382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
34482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
34582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
34682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * deleted.
34782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * <p/>
34882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * If there is a selection where selection start is not equal to selection end, does not
34982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * delete.
35082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
35182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inputConnection InputConnection instance
35282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param editable TextView.Editable instance
35382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param beforeLength the number of characters before the cursor to be deleted
35482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param afterLength the number of characters after the cursor to be deleted
35582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param inCodePoints {@code true} if length parameters are in codepoints
35682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
35782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if an {@link EmojiSpan} is deleted
35882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
35982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection,
36082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength,
36182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            @IntRange(from = 0) final int afterLength, final boolean inCodePoints) {
36282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (editable == null || inputConnection == null) {
36382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
36482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
36582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
36682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (beforeLength < 0 || afterLength < 0) {
36782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
36882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
36982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionStart = Selection.getSelectionStart(editable);
37182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final int selectionEnd = Selection.getSelectionEnd(editable);
37282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (hasInvalidSelection(selectionStart, selectionEnd)) {
37482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
37582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
37682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
37782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int start;
37882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int end;
37982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (inCodePoints) {
38082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            // go backwards in terms of codepoints
38182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = CodepointIndexFinder.findIndexBackward(editable, selectionStart,
38282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(beforeLength, 0));
38382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = CodepointIndexFinder.findIndexForward(editable, selectionEnd,
38482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    Math.max(afterLength, 0));
38582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
38682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (start == CodepointIndexFinder.INVALID_INDEX
38782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || end == CodepointIndexFinder.INVALID_INDEX) {
38882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return false;
38982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
39082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        } else {
39182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(selectionStart - beforeLength, 0);
39282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(selectionEnd + afterLength, editable.length());
39382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
39482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
39582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class);
39682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (spans != null && spans.length > 0) {
39782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = spans.length;
39882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            for (int index = 0; index < length; index++) {
39982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final EmojiSpan span = spans[index];
40082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanStart = editable.getSpanStart(span);
40182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                int spanEnd = editable.getSpanEnd(span);
40282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                start = Math.min(spanStart, start);
40382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                end = Math.max(spanEnd, end);
40482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
40582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
40682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            start = Math.max(start, 0);
40782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            end = Math.min(end, editable.length());
40882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
40982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.beginBatchEdit();
41082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            editable.delete(start, end);
41182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            inputConnection.endBatchEdit();
41282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return true;
41382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
41482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
41582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return false;
41682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
41782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
41882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasInvalidSelection(final int start, final int end) {
41982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return start == -1 || end == -1 || start != end;
42082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
42182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
42282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static boolean hasModifiers(KeyEvent event) {
42382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
42482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
42582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
42682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata,
42782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int start, final int end) {
42882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        final EmojiSpan span = mSpanFactory.createSpan(metadata);
42982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
43082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
43182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
43282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
43382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Checks whether the current OS can render a given emoji. Used by the system to decide if an
43482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * emoji span should be added. If the system cannot render it, an emoji span will be added.
43582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Used only for the case where replaceAll is set to {@code false}.
43682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
43782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param charSequence the CharSequence that the emoji is in
43882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param start start index of the emoji in the CharSequence
43982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param end end index of the emoji in the CharSequence
44082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @param metadata EmojiMetadata instance for the emoji
44182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     *
44282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * @return {@code true} if the OS can render emoji, {@code false} otherwise
44382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
44482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private boolean hasGlyph(final CharSequence charSequence, int start, final int end,
44582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final EmojiMetadata metadata) {
44682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // if the emoji was added to OS at a later version we know that the system cannot render it.
44782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // without this check pre M devices result in more false positives.
44882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (metadata.getSdkAdded() > Build.VERSION.SDK_INT) {
44982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return false;
45082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
45182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
45282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        // if the existence is not calculated yet
45382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) {
45482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final StringBuilder builder = getStringBuilder();
45582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            builder.setLength(0);
45682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
45782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (start < end) {
45882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                builder.append(charSequence.charAt(start));
45982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                start++;
46082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
46182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final boolean hasGlyph = PaintCompat.hasGlyph(mTextPaint, builder.toString());
46382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            metadata.setHasGlyph(hasGlyph);
46482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
46582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS;
46782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
46882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
46982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static StringBuilder getStringBuilder() {
47082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        if (sStringBuilder.get() == null) {
47182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            sStringBuilder.set(new StringBuilder());
47282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
47382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        return sStringBuilder.get();
47482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
47582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
47682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
47782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * State machine for walking over the metadata trie.
47882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
47982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    static final class ProcessorSm {
48082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_DEFAULT = 1;
48282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int STATE_WALKING = 2;
48382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mState = STATE_DEFAULT;
48582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
48682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
48782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Root of the trie
48882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
48982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private final MetadataRepo.Node mRootNode;
49082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
49182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
49282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Pointer to the node after last codepoint.
49382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
49482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mCurrentNode;
49582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
49682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
49782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The node where ACTION_FLUSH is called. Required since after flush action is
49882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * returned mCurrentNode is reset to be the root.
49982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
50082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private MetadataRepo.Node mFlushNode;
50182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
50282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
50382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * The code point that was checked.
50482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
50582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mLastCodepoint;
50682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
50782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
50882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Level for mCurrentNode. Root is 0.
50982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
51082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int mCurrentDepth;
51182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
51282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        ProcessorSm(MetadataRepo.Node rootNode) {
51382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mRootNode = rootNode;
51482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = rootNode;
51582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
51682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
51782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
51882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        int check(final int codePoint) {
51982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int action;
52082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            MetadataRepo.Node node = mCurrentNode.get(codePoint);
52182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            switch (mState) {
52282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_WALKING:
52382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node != null) {
52482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
52582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth += 1;
52682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
52782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
52882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        if (isTextStyle(codePoint)) {
52982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
53082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (isEmojiStyle(codePoint)) {
53182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = ACTION_ADVANCE_END;
53282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else if (mCurrentNode.getData() != null) {
53382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            if (mCurrentDepth == 1) {
53482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                if (mCurrentNode.getData().isDefaultEmoji()
53582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                        || isEmojiStyle(mLastCodepoint)) {
53682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    mFlushNode = mCurrentNode;
53782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = ACTION_FLUSH;
53882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    reset();
53982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                } else {
54082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                    action = reset();
54182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                }
54282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            } else {
54382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                mFlushNode = mCurrentNode;
54482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                action = ACTION_FLUSH;
54582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                                reset();
54682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            }
54782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        } else {
54882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                            action = reset();
54982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        }
55082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
55182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
55282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                case STATE_DEFAULT:
55382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                default:
55482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (node == null) {
55582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = reset();
55682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    } else {
55782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mState = STATE_WALKING;
55882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentNode = node;
55982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        mCurrentDepth = 1;
56082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        action = ACTION_ADVANCE_END;
56182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
56282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    break;
56382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
56482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
56582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mLastCodepoint = codePoint;
56682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return action;
56782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
56882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
56982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        @Action
57082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private int reset() {
57182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mState = STATE_DEFAULT;
57282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentNode = mRootNode;
57382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            mCurrentDepth = 0;
57482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return ACTION_ADVANCE_BOTH;
57582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
57682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
57782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
57882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return the metadata node when ACTION_FLUSH is returned
57982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
58082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getFlushMetadata() {
58182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mFlushNode.getData();
58282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
58382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
58482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
58582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return current pointer to the metadata node in the trie
58682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
58782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        EmojiMetadata getCurrentMetadata() {
58882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mCurrentNode.getData();
58982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
59082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
59182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
59282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Need for the case where input is consumed, but action_flush was not called. For example
59382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * when the char sequence has single codepoint character which is a default emoji. State
59482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * machine will wait for the next.
59582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
59682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return whether the current state requires an emoji to be added
59782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
59882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        boolean isInFlushableState() {
59982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return mState == STATE_WALKING && mCurrentNode.getData() != null
60082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    && (mCurrentNode.getData().isDefaultEmoji()
60182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || isEmojiStyle(mLastCodepoint)
60282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    || mCurrentDepth > 1);
60382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
60482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
60582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
60682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
60782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
60882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a emoji style standardized variation selector
60982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
61082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isEmojiStyle(int codePoint) {
61182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0F;
61282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
61382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
61482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
61582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param codePoint CodePoint to check
61682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
61782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return {@code true} if the codepoint is a text style standardized variation selector
61882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
61982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static boolean isTextStyle(int codePoint) {
62082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            return codePoint == 0xFE0E;
62182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
62282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
62382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
62482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    /**
62582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     * Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
62682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir     */
62782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    private static final class CodepointIndexFinder {
62882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static final int INVALID_INDEX = -1;
62982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
63082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
63182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} behind
63282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
63382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
63482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
63582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going backwards
63682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
63782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
63882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
63982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
64082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexBackward(final CharSequence cs, final int from,
64182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
64282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
64382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingHighSurrogate = false;
64482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
64582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
64682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
64782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
64882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
64982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
65082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
65182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
65282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
65382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
65482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
65582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
65682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
65782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                --currentIndex;
65882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex < 0) {
65982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingHighSurrogate) {
66082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
66182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
66282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return 0;  // Reached to the beginning of the text w/o any invalid surrogate
66382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
66482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
66582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
66682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingHighSurrogate) {
66782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isHighSurrogate(c)) {
66882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
66982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
67082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingHighSurrogate = false;
67182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
67282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
67382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
67482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
67582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
67682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
67782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
67882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isHighSurrogate(c)) {
67982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
68082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
68182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingHighSurrogate = true;
68282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
68382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
68482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
68582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        /**
68682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead
68782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * starting from {@code from}.
68882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
68982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param cs CharSequence to work on
69082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param from the index to start going forward
69182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @param numCodePoints the number of codepoints
69282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         *
69382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         * @return start index of the character
69482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir         */
69582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        private static int findIndexForward(final CharSequence cs, final int from,
69682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final int numCodePoints) {
69782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int currentIndex = from;
69882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            boolean waitingLowSurrogate = false;
69982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            final int length = cs.length();
70082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (currentIndex < 0 || length < currentIndex) {
70182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // The starting point is out of range.
70282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
70382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            if (numCodePoints < 0) {
70482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                return INVALID_INDEX;  // Basically this should not happen.
70582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
70682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            int remainingCodePoints = numCodePoints;
70782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
70882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            while (true) {
70982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (remainingCodePoints == 0) {
71082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return currentIndex;  // Reached to the requested length in code points.
71182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
71282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir
71382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (currentIndex >= length) {
71482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (waitingLowSurrogate) {
71582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
71682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
71782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return length;  // Reached to the end of the text w/o any invalid surrogate
71882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    // pair.
71982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
72082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                final char c = cs.charAt(currentIndex);
72182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (waitingLowSurrogate) {
72282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    if (!Character.isLowSurrogate(c)) {
72382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                        return INVALID_INDEX;  // An invalid surrogate pair is found.
72482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    }
72582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
72682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    waitingLowSurrogate = false;
72782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
72882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
72982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
73082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (!Character.isSurrogate(c)) {
73182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    --remainingCodePoints;
73282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    ++currentIndex;
73382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    continue;
73482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
73582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                if (Character.isLowSurrogate(c)) {
73682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                    return INVALID_INDEX;  // A invalid surrogate pair is found.
73782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                }
73882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                waitingLowSurrogate = true;
73982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir                ++currentIndex;
74082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir            }
74182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir        }
74282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir    }
74382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir}
744