EmojiProcessor.java revision 77b5c5b734f9f665577d1e3d178615db43ae1d4f
182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/* 282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Copyright (C) 2017 The Android Open Source Project 382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Licensed under the Apache License, Version 2.0 (the "License"); 582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * you may not use this file except in compliance with the License. 682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * You may obtain a copy of the License at 782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * http://www.apache.org/licenses/LICENSE-2.0 982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 1082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Unless required by applicable law or agreed to in writing, software 1182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * distributed under the License is distributed on an "AS IS" BASIS, 1282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * See the License for the specific language governing permissions and 1482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * limitations under the License. 1582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 1682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirpackage android.support.text.emoji; 1782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 1882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 1982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 2082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.os.Build; 2182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.AnyThread; 2282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.IntDef; 2382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.IntRange; 2482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.NonNull; 2577b5c5b734f9f665577d1e3d178615db43ae1d4fSiyamed Sinirimport android.support.annotation.RequiresApi; 2682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.annotation.RestrictTo; 27f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinirimport android.support.text.emoji.widget.SpannableBuilder; 2882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.support.v4.graphics.PaintCompat; 2982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Editable; 3082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Selection; 3182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spannable; 3282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.SpannableString; 3382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.Spanned; 3482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.TextPaint; 3582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.KeyListener; 3682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.text.method.MetaKeyKeyListener; 3782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.KeyEvent; 3882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport android.view.inputmethod.InputConnection; 3982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 4082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.Retention; 4182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirimport java.lang.annotation.RetentionPolicy; 4282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 4382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir/** 4482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Processes the CharSequence and adds the emojis. 4582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 4682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @hide 4782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 4882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@AnyThread 4982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir@RestrictTo(LIBRARY_GROUP) 5077b5c5b734f9f665577d1e3d178615db43ae1d4fSiyamed Sinir@RequiresApi(19) 5182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinirfinal class EmojiProcessor { 5282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 5382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 5482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * State transition commands. 5582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 5682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH}) 5782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @Retention(RetentionPolicy.SOURCE) 5882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @interface Action { 5982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 6082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 6182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 6282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Advance the end pointer in CharSequence and reset the start to be the end. 6382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 6482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final int ACTION_ADVANCE_BOTH = 1; 6582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 6682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 6782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Advance end pointer in CharSequence. 6882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 6982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final int ACTION_ADVANCE_END = 2; 7082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 7182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 7282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end 7382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * pointer in CharSequence and reset the start to be the end. 7482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 7582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final int ACTION_FLUSH = 3; 7682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 7782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 7882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Default text size for {@link #mTextPaint}. 7982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 8082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final int PAINT_TEXT_SIZE = 10; 8182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 8282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 8382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Used to create strings required by 8482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}. 8582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 8682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>(); 8782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 8882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 890bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir * @hide 9082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 910bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir @RestrictTo(LIBRARY_GROUP) 920bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir static final int EMOJI_COUNT_UNLIMITED = Integer.MAX_VALUE; 9382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 9482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 950bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir * Factory used to create EmojiSpans. 9682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 970bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir private final EmojiCompat.SpanFactory mSpanFactory; 9882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 9982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 10082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @see EmojiCompat.Config#setReplaceAll(boolean) 10182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 10282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private final boolean mReplaceAll; 10382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 10482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 10582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Emoji metadata repository. 10682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 10782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private final MetadataRepo mMetadataRepo; 10882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 10982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 11082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check. 11182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 11282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private final TextPaint mTextPaint; 11382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 11482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir EmojiProcessor(@NonNull final MetadataRepo metadataRepo, 11582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @NonNull final EmojiCompat.SpanFactory spanFactory, 1160bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir final boolean replaceAll) { 11782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mSpanFactory = spanFactory; 11882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mMetadataRepo = metadataRepo; 11982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mReplaceAll = replaceAll; 12082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mTextPaint = new TextPaint(); 12182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mTextPaint.setTextSize(PAINT_TEXT_SIZE); 12282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 12382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 12482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) { 12582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode()); 12682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int end = charSequence.length(); 12782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int currentOffset = 0; 12882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 12982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir while (currentOffset < end) { 13082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int codePoint = Character.codePointAt(charSequence, currentOffset); 13182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int action = sm.check(codePoint); 13282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (action != ACTION_ADVANCE_END) { 13382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return null; 13482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 13582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir currentOffset += Character.charCount(codePoint); 13682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 13782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 13882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (sm.isInFlushableState()) { 13982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return sm.getCurrentMetadata(); 14082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 14182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 14282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return null; 14382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 14482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 14582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 14682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. 14782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <p> 14882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <ul> 14982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <li>If no emojis are found, {@code charSequence} given as the input is returned without 15082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * any changes. i.e. charSequence is a String, and no emojis are found, the same String is 15182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * returned.</li> 15282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <li>If the given input is not a Spannable (such as String), and at least one emoji is found 15382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * a new {@link android.text.Spannable} instance is returned. </li> 15482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <li>If the given input is a Spannable, the same instance is returned. </li> 15582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * </ul> 15682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 15782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null} 15882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param start start index in the charSequence to look for emojis, should be greater than or 15982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * equal to {@code 0}, also less than {@code charSequence.length()} 16082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param end end index in the charSequence to look for emojis, should be greater than or 16182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * equal to {@code start} parameter, also less than {@code charSequence.length()} 1620bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater 1630bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir * than or equal to {@code 0} 16482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 16582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start, 1660bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount) { 167f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder; 168f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (isSpannableBuilder) { 169f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir ((SpannableBuilder) charSequence).beginBatchEdit(); 17082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 17182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 172f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir try { 173f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir Spannable spannable = null; 174f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // if it is a spannable already, use the same instance to add/remove EmojiSpans. 175f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // otherwise wait until the the first EmojiSpan found in order to change the result 176f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // into a Spannable. 177f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (isSpannableBuilder || charSequence instanceof Spannable) { 178f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir spannable = (Spannable) charSequence; 179f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 180f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir 181f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (spannable != null) { 182f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class); 183f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (spans != null && spans.length > 0) { 184f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // remove existing spans, and realign the start, end according to spans 185f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // if start or end is in the middle of an emoji they should be aligned 186f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final int length = spans.length; 187f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir for (int index = 0; index < length; index++) { 188f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final EmojiSpan span = spans[index]; 189f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final int spanStart = spannable.getSpanStart(span); 190f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final int spanEnd = spannable.getSpanEnd(span); 191f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // Remove span only when its spanStart is NOT equal to current end. 192f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // During add operation an emoji at index 0 is added with 0-1 as start and 193f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // end indices. Therefore if there are emoji spans at [0-1] and [1-2] 194f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // and end is 1, the span between 0-1 should be deleted, not 1-2. 195f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (spanStart != end) { 196f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir spannable.removeSpan(span); 197f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 198f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir start = Math.min(spanStart, start); 199f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir end = Math.max(spanEnd, end); 20082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 20182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 20282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 20382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 204f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (start == end || start >= charSequence.length()) { 205f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir return charSequence; 206f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 20782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 2080bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir // calculate max number of emojis that can be added. since getSpans call is a relatively 2090bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir // expensive operation, do it only when maxEmojiCount is not unlimited. 2100bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir if (maxEmojiCount != EMOJI_COUNT_UNLIMITED && spannable != null) { 2110bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir maxEmojiCount -= spannable.getSpans(0, spannable.length(), EmojiSpan.class).length; 2120bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir } 213f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // add new ones 214f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir int addedCount = 0; 215f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode()); 21682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 217f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir int currentOffset = start; 218f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir int codePoint = Character.codePointAt(charSequence, currentOffset); 21982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 2200bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir while (currentOffset < end && addedCount < maxEmojiCount) { 221f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir final int action = sm.check(codePoint); 22282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 223f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir switch (action) { 224f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir case ACTION_ADVANCE_BOTH: 225f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir currentOffset += Character.charCount(codePoint); 226f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir start = currentOffset; 227f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (currentOffset < end) { 228f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir codePoint = Character.codePointAt(charSequence, currentOffset); 22982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 230f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir break; 231f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir case ACTION_ADVANCE_END: 232f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir currentOffset += Character.charCount(codePoint); 233f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (currentOffset < end) { 234f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir codePoint = Character.codePointAt(charSequence, currentOffset); 235f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 236f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir break; 237f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir case ACTION_FLUSH: 238f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset, 239f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir sm.getFlushMetadata())) { 240f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (spannable == null) { 241f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir spannable = new SpannableString(charSequence); 242f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 243f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset); 244f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir addedCount++; 245f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 246f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir start = currentOffset; 247f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir break; 248f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 24982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 25082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 251f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // After the last codepoint is consumed the state machine might be in a state where it 252f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed 253f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ). 254f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir // Need to check if it is in such a state. 2550bd2e404e8efbc6408e2bea7531b858bce639b03Siyamed Sinir if (sm.isInFlushableState() && addedCount < maxEmojiCount) { 256f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (mReplaceAll || !hasGlyph(charSequence, start, currentOffset, 257f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir sm.getCurrentMetadata())) { 258f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (spannable == null) { 259f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir spannable = new SpannableString(charSequence); 260f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 261f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset); 262f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir addedCount++; 26382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 264f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } 265f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir return spannable == null ? charSequence : spannable; 266f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir } finally { 267f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir if (isSpannableBuilder) { 268f8ec169d022fbed42fd82091d24c45f3767cdfe7Siyamed Sinir ((SpannableBuilder) charSequence).endBatchEdit(); 26982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 27082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 27182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 27282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 27382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 27482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of 27582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an 27682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 27782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * deleted with the characters it covers. 27882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <p/> 27982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * If there is a selection where selection start is not equal to selection end, does not 28082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * delete. 28182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 28282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View, 28382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Editable, int, KeyEvent)} 28482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 28582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * int, KeyEvent)} 28682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 28782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * int, KeyEvent)} 28882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 28982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return {@code true} if an {@link EmojiSpan} is deleted 29082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 29182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode, 29282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final KeyEvent event) { 29382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final boolean handled; 29482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir switch (keyCode) { 29582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir case KeyEvent.KEYCODE_DEL: 29682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir handled = delete(editable, event, false /*forwardDelete*/); 29782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir break; 29882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir case KeyEvent.KEYCODE_FORWARD_DEL: 29982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir handled = delete(editable, event, true /*forwardDelete*/); 30082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir break; 30182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir default: 30282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir handled = false; 30382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir break; 30482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 30582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 30682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (handled) { 30782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir MetaKeyKeyListener.adjustMetaAfterKeypress(editable); 30882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return true; 30982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 31082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 31182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 31282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 31382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 31482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static boolean delete(final Editable content, final KeyEvent event, 31582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final boolean forwardDelete) { 31682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (hasModifiers(event)) { 31782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 31882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 31982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 32082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int start = Selection.getSelectionStart(content); 32182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int end = Selection.getSelectionEnd(content); 32282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (hasInvalidSelection(start, end)) { 32382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 32482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 32582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 32682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class); 32782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (spans != null && spans.length > 0) { 32882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int length = spans.length; 32982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir for (int index = 0; index < length; index++) { 33082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiSpan span = spans[index]; 33182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int spanStart = content.getSpanStart(span); 33282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int spanEnd = content.getSpanEnd(span); 33382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if ((forwardDelete && spanStart == start) 33482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || (!forwardDelete && spanEnd == start) 33582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || (start > spanStart && start < spanEnd)) { 33682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir content.delete(spanStart, spanEnd); 33782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return true; 33882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 33982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 34082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 34182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 34282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 34382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 34482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 34582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 34682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an 34782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 34882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * deleted. 34982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * <p/> 35082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * If there is a selection where selection start is not equal to selection end, does not 35182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * delete. 35282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 35382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param inputConnection InputConnection instance 35482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param editable TextView.Editable instance 35582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param beforeLength the number of characters before the cursor to be deleted 35682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param afterLength the number of characters after the cursor to be deleted 35782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param inCodePoints {@code true} if length parameters are in codepoints 35882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 35982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return {@code true} if an {@link EmojiSpan} is deleted 36082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 36182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection, 36282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength, 36382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @IntRange(from = 0) final int afterLength, final boolean inCodePoints) { 36482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (editable == null || inputConnection == null) { 36582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 36682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 36782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 36882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (beforeLength < 0 || afterLength < 0) { 36982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 37082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 37182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 37282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int selectionStart = Selection.getSelectionStart(editable); 37382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int selectionEnd = Selection.getSelectionEnd(editable); 37482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 37582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (hasInvalidSelection(selectionStart, selectionEnd)) { 37682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 37782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 37882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 37982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int start; 38082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int end; 38182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (inCodePoints) { 38282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir // go backwards in terms of codepoints 38382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir start = CodepointIndexFinder.findIndexBackward(editable, selectionStart, 38482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir Math.max(beforeLength, 0)); 38582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir end = CodepointIndexFinder.findIndexForward(editable, selectionEnd, 38682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir Math.max(afterLength, 0)); 38782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 38882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (start == CodepointIndexFinder.INVALID_INDEX 38982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || end == CodepointIndexFinder.INVALID_INDEX) { 39082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 39182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 39282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 39382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir start = Math.max(selectionStart - beforeLength, 0); 39482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir end = Math.min(selectionEnd + afterLength, editable.length()); 39582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 39682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 39782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class); 39882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (spans != null && spans.length > 0) { 39982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int length = spans.length; 40082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir for (int index = 0; index < length; index++) { 40182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiSpan span = spans[index]; 40282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int spanStart = editable.getSpanStart(span); 40382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int spanEnd = editable.getSpanEnd(span); 40482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir start = Math.min(spanStart, start); 40582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir end = Math.max(spanEnd, end); 40682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 40782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 40882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir start = Math.max(start, 0); 40982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir end = Math.min(end, editable.length()); 41082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 41182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir inputConnection.beginBatchEdit(); 41282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir editable.delete(start, end); 41382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir inputConnection.endBatchEdit(); 41482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return true; 41582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 41682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 41782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 41882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 41982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 42082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static boolean hasInvalidSelection(final int start, final int end) { 42182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return start == -1 || end == -1 || start != end; 42282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 42382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 42482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static boolean hasModifiers(KeyEvent event) { 42582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return !KeyEvent.metaStateHasNoModifiers(event.getMetaState()); 42682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 42782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 42882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata, 42982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int start, final int end) { 43082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiSpan span = mSpanFactory.createSpan(metadata); 43182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 43282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 43382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 43482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 43582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Checks whether the current OS can render a given emoji. Used by the system to decide if an 43682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * emoji span should be added. If the system cannot render it, an emoji span will be added. 43782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Used only for the case where replaceAll is set to {@code false}. 43882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 43982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param charSequence the CharSequence that the emoji is in 44082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param start start index of the emoji in the CharSequence 44182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param end end index of the emoji in the CharSequence 44282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param metadata EmojiMetadata instance for the emoji 44382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 44482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return {@code true} if the OS can render emoji, {@code false} otherwise 44582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 44682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private boolean hasGlyph(final CharSequence charSequence, int start, final int end, 44782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final EmojiMetadata metadata) { 448611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir // For pre M devices, heuristic in PaintCompat can result in false positives. we are 449611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir // adding another heuristic using the sdkAdded field. if the emoji was added to OS 450611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir // at a later version we assume that the system probably cannot render it. 451611b9e77742d232d7eee9e05e95e6db63d3d97e4Siyamed Sinir if (Build.VERSION.SDK_INT < 23 && metadata.getSdkAdded() > Build.VERSION.SDK_INT) { 45282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return false; 45382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 45482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 45582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir // if the existence is not calculated yet 45682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) { 45782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final StringBuilder builder = getStringBuilder(); 45882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir builder.setLength(0); 45982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 46082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir while (start < end) { 46182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir builder.append(charSequence.charAt(start)); 46282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir start++; 46382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 46482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 46582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final boolean hasGlyph = PaintCompat.hasGlyph(mTextPaint, builder.toString()); 46682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir metadata.setHasGlyph(hasGlyph); 46782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 46882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 46982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_EXISTS; 47082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 47182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 47282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static StringBuilder getStringBuilder() { 47382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (sStringBuilder.get() == null) { 47482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir sStringBuilder.set(new StringBuilder()); 47582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 47682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return sStringBuilder.get(); 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 51582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir ProcessorSm(MetadataRepo.Node rootNode) { 51682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mRootNode = rootNode; 51782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentNode = rootNode; 51882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 51982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 52082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @Action 52182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int check(final int codePoint) { 52282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int action; 52382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir MetadataRepo.Node node = mCurrentNode.get(codePoint); 52482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir switch (mState) { 52582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir case STATE_WALKING: 52682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (node != null) { 52782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentNode = node; 52882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentDepth += 1; 52982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = ACTION_ADVANCE_END; 53082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 53182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (isTextStyle(codePoint)) { 53282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = reset(); 53382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else if (isEmojiStyle(codePoint)) { 53482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = ACTION_ADVANCE_END; 53582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else if (mCurrentNode.getData() != null) { 53682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (mCurrentDepth == 1) { 53782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (mCurrentNode.getData().isDefaultEmoji() 53882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || isEmojiStyle(mLastCodepoint)) { 53982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mFlushNode = mCurrentNode; 54082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = ACTION_FLUSH; 54182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir reset(); 54282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 54382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = reset(); 54482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 54582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 54682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mFlushNode = mCurrentNode; 54782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = ACTION_FLUSH; 54882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir reset(); 54982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 55082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 55182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = reset(); 55282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 55382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 55482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir break; 55582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir case STATE_DEFAULT: 55682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir default: 55782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (node == null) { 55882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = reset(); 55982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } else { 56082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mState = STATE_WALKING; 56182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentNode = node; 56282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentDepth = 1; 56382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir action = ACTION_ADVANCE_END; 56482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 56582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir break; 56682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 56782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 56882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mLastCodepoint = codePoint; 56982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return action; 57082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 57182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 57282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir @Action 57382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private int reset() { 57482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mState = STATE_DEFAULT; 57582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentNode = mRootNode; 57682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir mCurrentDepth = 0; 57782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return ACTION_ADVANCE_BOTH; 57882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 57982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 58082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 58182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return the metadata node when ACTION_FLUSH is returned 58282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 58382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir EmojiMetadata getFlushMetadata() { 58482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return mFlushNode.getData(); 58582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 58682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 58782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 58882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return current pointer to the metadata node in the trie 58982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 59082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir EmojiMetadata getCurrentMetadata() { 59182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return mCurrentNode.getData(); 59282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 59382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 59482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 59582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Need for the case where input is consumed, but action_flush was not called. For example 59682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * when the char sequence has single codepoint character which is a default emoji. State 59782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * machine will wait for the next. 59882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 59982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return whether the current state requires an emoji to be added 60082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 60182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir boolean isInFlushableState() { 60282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return mState == STATE_WALKING && mCurrentNode.getData() != null 60382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir && (mCurrentNode.getData().isDefaultEmoji() 60482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || isEmojiStyle(mLastCodepoint) 60582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir || mCurrentDepth > 1); 60682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 60782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 60882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 60982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param codePoint CodePoint to check 61082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 61182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return {@code true} if the codepoint is a emoji style standardized variation selector 61282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 61382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static boolean isEmojiStyle(int codePoint) { 61482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return codePoint == 0xFE0F; 61582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 61682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 61782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 61882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param codePoint CodePoint to check 61982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 62082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return {@code true} if the codepoint is a text style standardized variation selector 62182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 62282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static boolean isTextStyle(int codePoint) { 62382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return codePoint == 0xFE0E; 62482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 62582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 62682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 62782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 62882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Copy of BaseInputConnection findIndexBackward and findIndexForward functions. 62982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 63082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final class CodepointIndexFinder { 63182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static final int INVALID_INDEX = -1; 63282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 63382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 63482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Find start index of the character in {@code cs} that is {@code numCodePoints} behind 63582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * starting from {@code from}. 63682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 63782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param cs CharSequence to work on 63882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param from the index to start going backwards 63982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param numCodePoints the number of codepoints 64082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 64182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return start index of the character 64282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 64382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static int findIndexBackward(final CharSequence cs, final int from, 64482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int numCodePoints) { 64582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int currentIndex = from; 64682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir boolean waitingHighSurrogate = false; 64782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int length = cs.length(); 64882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (currentIndex < 0 || length < currentIndex) { 64982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // The starting point is out of range. 65082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 65182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (numCodePoints < 0) { 65282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // Basically this should not happen. 65382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 65482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int remainingCodePoints = numCodePoints; 65582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir while (true) { 65682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (remainingCodePoints == 0) { 65782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return currentIndex; // Reached to the requested length in code points. 65882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 65982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 66082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir --currentIndex; 66182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (currentIndex < 0) { 66282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (waitingHighSurrogate) { 66382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // An invalid surrogate pair is found. 66482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 66582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return 0; // Reached to the beginning of the text w/o any invalid surrogate 66682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir // pair. 66782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 66882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final char c = cs.charAt(currentIndex); 66982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (waitingHighSurrogate) { 67082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (!Character.isHighSurrogate(c)) { 67182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // An invalid surrogate pair is found. 67282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 67382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir waitingHighSurrogate = false; 67482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir --remainingCodePoints; 67582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir continue; 67682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 67782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (!Character.isSurrogate(c)) { 67882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir --remainingCodePoints; 67982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir continue; 68082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 68182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (Character.isHighSurrogate(c)) { 68282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // A invalid surrogate pair is found. 68382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 68482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir waitingHighSurrogate = true; 68582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 68682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 68782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 68882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir /** 68982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead 69082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * starting from {@code from}. 69182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 69282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param cs CharSequence to work on 69382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param from the index to start going forward 69482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @param numCodePoints the number of codepoints 69582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * 69682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir * @return start index of the character 69782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir */ 69882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir private static int findIndexForward(final CharSequence cs, final int from, 69982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int numCodePoints) { 70082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int currentIndex = from; 70182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir boolean waitingLowSurrogate = false; 70282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final int length = cs.length(); 70382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (currentIndex < 0 || length < currentIndex) { 70482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // The starting point is out of range. 70582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 70682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (numCodePoints < 0) { 70782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // Basically this should not happen. 70882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 70982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir int remainingCodePoints = numCodePoints; 71082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 71182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir while (true) { 71282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (remainingCodePoints == 0) { 71382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return currentIndex; // Reached to the requested length in code points. 71482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 71582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir 71682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (currentIndex >= length) { 71782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (waitingLowSurrogate) { 71882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // An invalid surrogate pair is found. 71982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 72082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return length; // Reached to the end of the text w/o any invalid surrogate 72182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir // pair. 72282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 72382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir final char c = cs.charAt(currentIndex); 72482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (waitingLowSurrogate) { 72582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (!Character.isLowSurrogate(c)) { 72682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // An invalid surrogate pair is found. 72782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 72882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir --remainingCodePoints; 72982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir waitingLowSurrogate = false; 73082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir ++currentIndex; 73182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir continue; 73282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 73382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (!Character.isSurrogate(c)) { 73482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir --remainingCodePoints; 73582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir ++currentIndex; 73682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir continue; 73782d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 73882d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir if (Character.isLowSurrogate(c)) { 73982d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir return INVALID_INDEX; // A invalid surrogate pair is found. 74082d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 74182d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir waitingLowSurrogate = true; 74282d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir ++currentIndex; 74382d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 74482d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 74582d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir } 74682d2cc1cf0c2bfdd5121e6d6913dfe9fcaacf439Siyamed Sinir} 747