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