Home | History | Annotate | Download | only in text
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package androidx.emoji.text;
     17 
     18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
     19 
     20 import android.graphics.Color;
     21 import android.os.Build;
     22 import android.os.Handler;
     23 import android.os.Looper;
     24 import android.text.Editable;
     25 import android.text.method.KeyListener;
     26 import android.view.KeyEvent;
     27 import android.view.inputmethod.EditorInfo;
     28 import android.view.inputmethod.InputConnection;
     29 
     30 import androidx.annotation.AnyThread;
     31 import androidx.annotation.CheckResult;
     32 import androidx.annotation.ColorInt;
     33 import androidx.annotation.GuardedBy;
     34 import androidx.annotation.IntDef;
     35 import androidx.annotation.IntRange;
     36 import androidx.annotation.NonNull;
     37 import androidx.annotation.Nullable;
     38 import androidx.annotation.RequiresApi;
     39 import androidx.annotation.RestrictTo;
     40 import androidx.annotation.VisibleForTesting;
     41 import androidx.collection.ArraySet;
     42 import androidx.core.util.Preconditions;
     43 
     44 import java.lang.annotation.Retention;
     45 import java.lang.annotation.RetentionPolicy;
     46 import java.util.ArrayList;
     47 import java.util.Arrays;
     48 import java.util.Collection;
     49 import java.util.List;
     50 import java.util.Set;
     51 import java.util.concurrent.locks.ReadWriteLock;
     52 import java.util.concurrent.locks.ReentrantReadWriteLock;
     53 
     54 /**
     55  * Main class to keep Android devices up to date with the newest emojis by adding {@link EmojiSpan}s
     56  * to a given {@link CharSequence}. It is a singleton class that can be configured using a {@link
     57  * EmojiCompat.Config} instance.
     58  * <p/>
     59  * EmojiCompat has to be initialized using {@link #init(EmojiCompat.Config)} function before it can
     60  * process a {@link CharSequence}.
     61  * <pre><code>EmojiCompat.init(&#47;* a config instance *&#47;);</code></pre>
     62  * <p/>
     63  * It is suggested to make the initialization as early as possible in your app. Please check {@link
     64  * EmojiCompat.Config} for more configuration parameters. Once {@link #init(EmojiCompat.Config)} is
     65  * called a singleton instance will be created. Any call after that will not create a new instance
     66  * and will return immediately.
     67  * <p/>
     68  * During initialization information about emojis is loaded on a background thread. Before the
     69  * EmojiCompat instance is initialized, calls to functions such as {@link
     70  * EmojiCompat#process(CharSequence)} will throw an exception. You can use the {@link InitCallback}
     71  * class to be informed about the state of initialization.
     72  * <p/>
     73  * After initialization the {@link #get()} function can be used to get the configured instance and
     74  * the {@link #process(CharSequence)} function can be used to update a CharSequence with emoji
     75  * EmojiSpans.
     76  * <p/>
     77  * <pre><code>CharSequence processedSequence = EmojiCompat.get().process("some string")</pre>
     78  */
     79 @AnyThread
     80 public class EmojiCompat {
     81     /**
     82      * Key in {@link EditorInfo#extras} that represents the emoji metadata version used by the
     83      * widget. The existence of the value means that the widget is using EmojiCompat.
     84      * <p/>
     85      * If exists, the value for the key is an {@code int} and can be used to query EmojiCompat to
     86      * see whether the widget has the ability to display a certain emoji using
     87      * {@link #hasEmojiGlyph(CharSequence, int)}.
     88      */
     89     public static final String EDITOR_INFO_METAVERSION_KEY =
     90             "android.support.text.emoji.emojiCompat_metadataVersion";
     91 
     92     /**
     93      * Key in {@link EditorInfo#extras} that represents {@link
     94      * EmojiCompat.Config#setReplaceAll(boolean)} configuration parameter. The key is added only if
     95      * EmojiCompat is used by the widget. If exists, the value is a boolean.
     96      */
     97     public static final String EDITOR_INFO_REPLACE_ALL_KEY =
     98             "android.support.text.emoji.emojiCompat_replaceAll";
     99 
    100     /**
    101      * EmojiCompat instance is constructed, however the initialization did not start yet.
    102      */
    103     public static final int LOAD_STATE_DEFAULT = 3;
    104 
    105     /**
    106      * EmojiCompat is initializing.
    107      */
    108     public static final int LOAD_STATE_LOADING = 0;
    109 
    110     /**
    111      * EmojiCompat successfully initialized.
    112      */
    113     public static final int LOAD_STATE_SUCCEEDED = 1;
    114 
    115     /**
    116      * An unrecoverable error occurred during initialization of EmojiCompat. Calls to functions
    117      * such as {@link #process(CharSequence)} will fail.
    118      */
    119     public static final int LOAD_STATE_FAILED = 2;
    120 
    121     /**
    122      * @hide
    123      */
    124     @RestrictTo(LIBRARY_GROUP)
    125     @IntDef({LOAD_STATE_DEFAULT, LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
    126     @Retention(RetentionPolicy.SOURCE)
    127     public @interface LoadState {
    128     }
    129 
    130     /**
    131      * Replace strategy that uses the value given in {@link EmojiCompat.Config}.
    132      */
    133     public static final int REPLACE_STRATEGY_DEFAULT = 0;
    134 
    135     /**
    136      * Replace strategy to add {@link EmojiSpan}s for all emoji that were found.
    137      */
    138     public static final int REPLACE_STRATEGY_ALL = 1;
    139 
    140     /**
    141      * Replace strategy to add {@link EmojiSpan}s only for emoji that do not exist in the system.
    142      */
    143     public static final int REPLACE_STRATEGY_NON_EXISTENT = 2;
    144 
    145     /**
    146      * @hide
    147      */
    148     @RestrictTo(LIBRARY_GROUP)
    149     @IntDef({REPLACE_STRATEGY_DEFAULT, REPLACE_STRATEGY_NON_EXISTENT, REPLACE_STRATEGY_ALL})
    150     @Retention(RetentionPolicy.SOURCE)
    151     public @interface ReplaceStrategy {
    152     }
    153 
    154     /**
    155      * {@link EmojiCompat} will start loading metadata when {@link #init(Config)} is called.
    156      */
    157     public static final int LOAD_STRATEGY_DEFAULT = 0;
    158 
    159     /**
    160      * {@link EmojiCompat} will wait for {@link #load()} to be called by developer in order to
    161      * start loading metadata.
    162      */
    163     public static final int LOAD_STRATEGY_MANUAL = 1;
    164 
    165     /**
    166      * @hide
    167      */
    168     @RestrictTo(LIBRARY_GROUP)
    169     @IntDef({LOAD_STRATEGY_DEFAULT, LOAD_STRATEGY_MANUAL})
    170     @Retention(RetentionPolicy.SOURCE)
    171     public @interface LoadStrategy {
    172     }
    173 
    174     /**
    175      * @hide
    176      */
    177     @RestrictTo(LIBRARY_GROUP)
    178     static final int EMOJI_COUNT_UNLIMITED = Integer.MAX_VALUE;
    179 
    180     private static final Object sInstanceLock = new Object();
    181 
    182     @GuardedBy("sInstanceLock")
    183     private static volatile EmojiCompat sInstance;
    184 
    185     private final ReadWriteLock mInitLock;
    186 
    187     @GuardedBy("mInitLock")
    188     private final Set<InitCallback> mInitCallbacks;
    189 
    190     @GuardedBy("mInitLock")
    191     @LoadState
    192     private int mLoadState;
    193 
    194     /**
    195      * Handler with main looper to run the callbacks on.
    196      */
    197     private final Handler mMainHandler;
    198 
    199     /**
    200      * Helper class for pre 19 compatibility.
    201      */
    202     private final CompatInternal mHelper;
    203 
    204     /**
    205      * Metadata loader instance given in the Config instance.
    206      */
    207     private final MetadataRepoLoader mMetadataLoader;
    208 
    209     /**
    210      * @see Config#setReplaceAll(boolean)
    211      */
    212     private final boolean mReplaceAll;
    213 
    214     /**
    215      * @see Config#setUseEmojiAsDefaultStyle(boolean)
    216      */
    217     private final boolean mUseEmojiAsDefaultStyle;
    218 
    219     /**
    220      * @see Config#setUseEmojiAsDefaultStyle(boolean, List)
    221      */
    222     private final int[] mEmojiAsDefaultStyleExceptions;
    223 
    224     /**
    225      * @see Config#setEmojiSpanIndicatorEnabled(boolean)
    226      */
    227     private final boolean mEmojiSpanIndicatorEnabled;
    228 
    229     /**
    230      * @see Config#setEmojiSpanIndicatorColor(int)
    231      */
    232     private final int mEmojiSpanIndicatorColor;
    233 
    234     /**
    235      * @see Config#setMetadataLoadStrategy(int)
    236      */
    237     @LoadStrategy private final int mMetadataLoadStrategy;
    238 
    239     /**
    240      * Private constructor for singleton instance.
    241      *
    242      * @see #init(Config)
    243      */
    244     private EmojiCompat(@NonNull final Config config) {
    245         mInitLock = new ReentrantReadWriteLock();
    246         mLoadState = LOAD_STATE_DEFAULT;
    247         mReplaceAll = config.mReplaceAll;
    248         mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle;
    249         mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions;
    250         mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
    251         mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
    252         mMetadataLoader = config.mMetadataLoader;
    253         mMetadataLoadStrategy = config.mMetadataLoadStrategy;
    254         mMainHandler = new Handler(Looper.getMainLooper());
    255         mInitCallbacks = new ArraySet<>();
    256         if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
    257             mInitCallbacks.addAll(config.mInitCallbacks);
    258         }
    259         mHelper = Build.VERSION.SDK_INT < 19 ? new CompatInternal(this) : new CompatInternal19(
    260                 this);
    261         loadMetadata();
    262     }
    263 
    264     /**
    265      * Initialize the singleton instance with a configuration. When used on devices running API 18
    266      * or below, the singleton instance is immediately moved into {@link #LOAD_STATE_SUCCEEDED}
    267      * state without loading any metadata. When called for the first time, the library will create
    268      * the singleton instance and any call after that will not create a new instance and return
    269      * immediately.
    270      *
    271      * @see EmojiCompat.Config
    272      */
    273     @SuppressWarnings("GuardedBy")
    274     public static EmojiCompat init(@NonNull final Config config) {
    275         if (sInstance == null) {
    276             synchronized (sInstanceLock) {
    277                 if (sInstance == null) {
    278                     sInstance = new EmojiCompat(config);
    279                 }
    280             }
    281         }
    282         return sInstance;
    283     }
    284 
    285     /**
    286      * Used by the tests to reset EmojiCompat with a new configuration. Every time it is called a
    287      * new instance is created with the new configuration.
    288      *
    289      * @hide
    290      */
    291     @SuppressWarnings("GuardedBy")
    292     @RestrictTo(LIBRARY_GROUP)
    293     @VisibleForTesting
    294     public static EmojiCompat reset(@NonNull final Config config) {
    295         synchronized (sInstanceLock) {
    296             sInstance = new EmojiCompat(config);
    297         }
    298         return sInstance;
    299     }
    300 
    301     /**
    302      * Used by the tests to reset EmojiCompat with a new singleton instance.
    303      *
    304      * @hide
    305      */
    306     @SuppressWarnings("GuardedBy")
    307     @RestrictTo(LIBRARY_GROUP)
    308     @VisibleForTesting
    309     public static EmojiCompat reset(final EmojiCompat emojiCompat) {
    310         synchronized (sInstanceLock) {
    311             sInstance = emojiCompat;
    312         }
    313         return sInstance;
    314     }
    315 
    316     /**
    317      * Used by the tests to set GlyphChecker for EmojiProcessor.
    318      *
    319      * @hide
    320      */
    321     @RestrictTo(LIBRARY_GROUP)
    322     @VisibleForTesting
    323     void setGlyphChecker(@NonNull final EmojiProcessor.GlyphChecker glyphChecker) {
    324         mHelper.setGlyphChecker(glyphChecker);
    325     }
    326 
    327     /**
    328      * Return singleton EmojiCompat instance. Should be called after
    329      * {@link #init(EmojiCompat.Config)} is called to initialize the singleton instance.
    330      *
    331      * @return EmojiCompat instance
    332      *
    333      * @throws IllegalStateException if called before {@link #init(EmojiCompat.Config)}
    334      */
    335     public static EmojiCompat get() {
    336         synchronized (sInstanceLock) {
    337             Preconditions.checkState(sInstance != null,
    338                     "EmojiCompat is not initialized. Please call EmojiCompat.init() first");
    339             return sInstance;
    340         }
    341     }
    342 
    343     /**
    344      * When {@link Config#setMetadataLoadStrategy(int)} is set to {@link #LOAD_STRATEGY_MANUAL},
    345      * this function starts loading the metadata. Calling the function when
    346      * {@link Config#setMetadataLoadStrategy(int)} is {@code not} set to
    347      * {@link #LOAD_STRATEGY_MANUAL} will throw an exception. The load will {@code not} start if:
    348      * <ul>
    349      *     <li>the metadata is already loaded successfully and {@link #getLoadState()} is
    350      *     {@link #LOAD_STATE_SUCCEEDED}.
    351      *     </li>
    352      *      <li>a previous load attempt is not finished yet and {@link #getLoadState()} is
    353      *     {@link #LOAD_STATE_LOADING}.</li>
    354      * </ul>
    355      *
    356      * @throws IllegalStateException when {@link Config#setMetadataLoadStrategy(int)} is not set
    357      * to {@link #LOAD_STRATEGY_MANUAL}
    358      */
    359     public void load() {
    360         Preconditions.checkState(mMetadataLoadStrategy == LOAD_STRATEGY_MANUAL,
    361                 "Set metadataLoadStrategy to LOAD_STRATEGY_MANUAL to execute manual loading");
    362         if (isInitialized()) return;
    363 
    364         mInitLock.writeLock().lock();
    365         try {
    366             if (mLoadState == LOAD_STATE_LOADING) return;
    367             mLoadState = LOAD_STATE_LOADING;
    368         } finally {
    369             mInitLock.writeLock().unlock();
    370         }
    371 
    372         mHelper.loadMetadata();
    373     }
    374 
    375     private void loadMetadata() {
    376         mInitLock.writeLock().lock();
    377         try {
    378             if (mMetadataLoadStrategy == LOAD_STRATEGY_DEFAULT) {
    379                 mLoadState = LOAD_STATE_LOADING;
    380             }
    381         } finally {
    382             mInitLock.writeLock().unlock();
    383         }
    384 
    385         if (getLoadState() == LOAD_STATE_LOADING) {
    386             mHelper.loadMetadata();
    387         }
    388     }
    389 
    390     private void onMetadataLoadSuccess() {
    391         final Collection<InitCallback> initCallbacks = new ArrayList<>();
    392         mInitLock.writeLock().lock();
    393         try {
    394             mLoadState = LOAD_STATE_SUCCEEDED;
    395             initCallbacks.addAll(mInitCallbacks);
    396             mInitCallbacks.clear();
    397         } finally {
    398             mInitLock.writeLock().unlock();
    399         }
    400 
    401         mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState));
    402     }
    403 
    404     private void onMetadataLoadFailed(@Nullable final Throwable throwable) {
    405         final Collection<InitCallback> initCallbacks = new ArrayList<>();
    406         mInitLock.writeLock().lock();
    407         try {
    408             mLoadState = LOAD_STATE_FAILED;
    409             initCallbacks.addAll(mInitCallbacks);
    410             mInitCallbacks.clear();
    411         } finally {
    412             mInitLock.writeLock().unlock();
    413         }
    414         mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState, throwable));
    415     }
    416 
    417     /**
    418      * Registers an initialization callback. If the initialization is already completed by the time
    419      * the listener is added, the callback functions are called immediately. Callbacks are called on
    420      * the main looper.
    421      * <p/>
    422      * When used on devices running API 18 or below, {@link InitCallback#onInitialized()} is called
    423      * without loading any metadata. In such cases {@link InitCallback#onFailed(Throwable)} is never
    424      * called.
    425      *
    426      * @param initCallback the initialization callback to register, cannot be {@code null}
    427      *
    428      * @see #unregisterInitCallback(InitCallback)
    429      */
    430     public void registerInitCallback(@NonNull InitCallback initCallback) {
    431         Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
    432 
    433         mInitLock.writeLock().lock();
    434         try {
    435             if (mLoadState == LOAD_STATE_SUCCEEDED || mLoadState == LOAD_STATE_FAILED) {
    436                 mMainHandler.post(new ListenerDispatcher(initCallback, mLoadState));
    437             } else {
    438                 mInitCallbacks.add(initCallback);
    439             }
    440         } finally {
    441             mInitLock.writeLock().unlock();
    442         }
    443     }
    444 
    445     /**
    446      * Unregisters a callback that was added before.
    447      *
    448      * @param initCallback the callback to be removed, cannot be {@code null}
    449      */
    450     public void unregisterInitCallback(@NonNull InitCallback initCallback) {
    451         Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
    452         mInitLock.writeLock().lock();
    453         try {
    454             mInitCallbacks.remove(initCallback);
    455         } finally {
    456             mInitLock.writeLock().unlock();
    457         }
    458     }
    459 
    460     /**
    461      * Returns loading state of the EmojiCompat instance. When used on devices running API 18 or
    462      * below always returns {@link #LOAD_STATE_SUCCEEDED}.
    463      *
    464      * @return one of {@link #LOAD_STATE_DEFAULT}, {@link #LOAD_STATE_LOADING},
    465      * {@link #LOAD_STATE_SUCCEEDED}, {@link #LOAD_STATE_FAILED}
    466      */
    467     public @LoadState int getLoadState() {
    468         mInitLock.readLock().lock();
    469         try {
    470             return mLoadState;
    471         } finally {
    472             mInitLock.readLock().unlock();
    473         }
    474     }
    475 
    476     /**
    477      * @return {@code true} if EmojiCompat is successfully initialized
    478      */
    479     private boolean isInitialized() {
    480         return getLoadState() == LOAD_STATE_SUCCEEDED;
    481     }
    482 
    483     /**
    484      * @return whether a background should be drawn for the emoji.
    485      * @hide
    486      */
    487     @RestrictTo(LIBRARY_GROUP)
    488     boolean isEmojiSpanIndicatorEnabled() {
    489         return mEmojiSpanIndicatorEnabled;
    490     }
    491 
    492     /**
    493      * @return whether a background should be drawn for the emoji.
    494      * @hide
    495      */
    496     @RestrictTo(LIBRARY_GROUP)
    497     @ColorInt int getEmojiSpanIndicatorColor() {
    498         return mEmojiSpanIndicatorColor;
    499     }
    500 
    501     /**
    502      * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
    503      * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
    504      * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
    505      * deleted with the characters it covers.
    506      * <p/>
    507      * If there is a selection where selection start is not equal to selection end, does not
    508      * delete.
    509      * <p/>
    510      * When used on devices running API 18 or below, always returns {@code false}.
    511      *
    512      * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
    513      *                 Editable, int, KeyEvent)}
    514      * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
    515      *                int, KeyEvent)}
    516      * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
    517      *              int, KeyEvent)}
    518      *
    519      * @return {@code true} if an {@link EmojiSpan} is deleted
    520      */
    521     public static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
    522             final KeyEvent event) {
    523         if (Build.VERSION.SDK_INT >= 19) {
    524             return EmojiProcessor.handleOnKeyDown(editable, keyCode, event);
    525         } else {
    526             return false;
    527         }
    528     }
    529 
    530     /**
    531      * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
    532      * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
    533      * deleted.
    534      * <p/>
    535      * If there is a selection where selection start is not equal to selection end, does not
    536      * delete.
    537      * <p/>
    538      * When used on devices running API 18 or below, always returns {@code false}.
    539      *
    540      * @param inputConnection InputConnection instance
    541      * @param editable TextView.Editable instance
    542      * @param beforeLength the number of characters before the cursor to be deleted
    543      * @param afterLength the number of characters after the cursor to be deleted
    544      * @param inCodePoints {@code true} if length parameters are in codepoints
    545      *
    546      * @return {@code true} if an {@link EmojiSpan} is deleted
    547      */
    548     public static boolean handleDeleteSurroundingText(
    549             @NonNull final InputConnection inputConnection, @NonNull final Editable editable,
    550             @IntRange(from = 0) final int beforeLength, @IntRange(from = 0) final int afterLength,
    551             final boolean inCodePoints) {
    552         if (Build.VERSION.SDK_INT >= 19) {
    553             return EmojiProcessor.handleDeleteSurroundingText(inputConnection, editable,
    554                     beforeLength, afterLength, inCodePoints);
    555         } else {
    556             return false;
    557         }
    558     }
    559 
    560     /**
    561      * Returns {@code true} if EmojiCompat is capable of rendering an emoji. When used on devices
    562      * running API 18 or below, always returns {@code false}.
    563      *
    564      * @param sequence CharSequence representing the emoji
    565      *
    566      * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null}
    567      *
    568      * @throws IllegalStateException if not initialized yet
    569      */
    570     public boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
    571         Preconditions.checkState(isInitialized(), "Not initialized yet");
    572         Preconditions.checkNotNull(sequence, "sequence cannot be null");
    573         return mHelper.hasEmojiGlyph(sequence);
    574     }
    575 
    576     /**
    577      * Returns {@code true} if EmojiCompat is capable of rendering an emoji at the given metadata
    578      * version. When used on devices running API 18 or below, always returns {@code false}.
    579      *
    580      * @param sequence CharSequence representing the emoji
    581      * @param metadataVersion the metadata version to check against, should be greater than or
    582      *                        equal to {@code 0},
    583      *
    584      * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null}
    585      *
    586      * @throws IllegalStateException if not initialized yet
    587      */
    588     public boolean hasEmojiGlyph(@NonNull final CharSequence sequence,
    589             @IntRange(from = 0) final int metadataVersion) {
    590         Preconditions.checkState(isInitialized(), "Not initialized yet");
    591         Preconditions.checkNotNull(sequence, "sequence cannot be null");
    592         return mHelper.hasEmojiGlyph(sequence, metadataVersion);
    593     }
    594 
    595     /**
    596      * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. When
    597      * used on devices running API 18 or below, returns the given {@code charSequence} without
    598      * processing it.
    599      *
    600      * @param charSequence CharSequence to add the EmojiSpans
    601      *
    602      * @throws IllegalStateException if not initialized yet
    603      * @see #process(CharSequence, int, int)
    604      */
    605     @CheckResult
    606     public CharSequence process(@NonNull final CharSequence charSequence) {
    607         // since charSequence might be null here we have to check it. Passing through here to the
    608         // main function so that it can do all the checks including isInitialized. It will also
    609         // be the main point that decides what to return.
    610         //noinspection ConstantConditions
    611         @IntRange(from = 0) final int length = charSequence == null ? 0 : charSequence.length();
    612         return process(charSequence, 0, length);
    613     }
    614 
    615     /**
    616      * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
    617      * <p>
    618      * <ul>
    619      * <li>If no emojis are found, {@code charSequence} given as the input is returned without
    620      * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
    621      * returned.</li>
    622      * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
    623      * a new {@link android.text.Spannable} instance is returned. </li>
    624      * <li>If the given input is a Spannable, the same instance is returned. </li>
    625      * </ul>
    626      * When used on devices running API 18 or below, returns the given {@code charSequence} without
    627      * processing it.
    628      *
    629      * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
    630      * @param start start index in the charSequence to look for emojis, should be greater than or
    631      *              equal to {@code 0}, also less than {@code charSequence.length()}
    632      * @param end end index in the charSequence to look for emojis, should be greater than or
    633      *            equal to {@code start} parameter, also less than {@code charSequence.length()}
    634      *
    635      * @throws IllegalStateException if not initialized yet
    636      * @throws IllegalArgumentException in the following cases:
    637      *                                  {@code start < 0}, {@code end < 0}, {@code end < start},
    638      *                                  {@code start > charSequence.length()},
    639      *                                  {@code end > charSequence.length()}
    640      */
    641     @CheckResult
    642     public CharSequence process(@NonNull final CharSequence charSequence,
    643             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end) {
    644         return process(charSequence, start, end, EMOJI_COUNT_UNLIMITED);
    645     }
    646 
    647     /**
    648      * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
    649      * <p>
    650      * <ul>
    651      * <li>If no emojis are found, {@code charSequence} given as the input is returned without
    652      * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
    653      * returned.</li>
    654      * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
    655      * a new {@link android.text.Spannable} instance is returned. </li>
    656      * <li>If the given input is a Spannable, the same instance is returned. </li>
    657      * </ul>
    658      * When used on devices running API 18 or below, returns the given {@code charSequence} without
    659      * processing it.
    660      *
    661      * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
    662      * @param start start index in the charSequence to look for emojis, should be greater than or
    663      *              equal to {@code 0}, also less than {@code charSequence.length()}
    664      * @param end end index in the charSequence to look for emojis, should be greater than or
    665      *            equal to {@code start} parameter, also less than {@code charSequence.length()}
    666      * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
    667      *                      than or equal to {@code 0}
    668      *
    669      * @throws IllegalStateException if not initialized yet
    670      * @throws IllegalArgumentException in the following cases:
    671      *                                  {@code start < 0}, {@code end < 0}, {@code end < start},
    672      *                                  {@code start > charSequence.length()},
    673      *                                  {@code end > charSequence.length()}
    674      *                                  {@code maxEmojiCount < 0}
    675      */
    676     @CheckResult
    677     public CharSequence process(@NonNull final CharSequence charSequence,
    678             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
    679             @IntRange(from = 0) final int maxEmojiCount) {
    680         return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT);
    681     }
    682 
    683     /**
    684      * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
    685      * <p>
    686      * <ul>
    687      * <li>If no emojis are found, {@code charSequence} given as the input is returned without
    688      * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
    689      * returned.</li>
    690      * <li>If the given input is not a Spannable (such as String), and at least one emoji is found
    691      * a new {@link android.text.Spannable} instance is returned. </li>
    692      * <li>If the given input is a Spannable, the same instance is returned. </li>
    693      * </ul>
    694      * When used on devices running API 18 or below, returns the given {@code charSequence} without
    695      * processing it.
    696      *
    697      * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
    698      * @param start start index in the charSequence to look for emojis, should be greater than or
    699      *              equal to {@code 0}, also less than {@code charSequence.length()}
    700      * @param end end index in the charSequence to look for emojis, should be greater than or
    701      *            equal to {@code start} parameter, also less than {@code charSequence.length()}
    702      * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
    703      *                      than or equal to {@code 0}
    704      * @param replaceStrategy whether to replace all emoji with {@link EmojiSpan}s, should be one of
    705      *                        {@link #REPLACE_STRATEGY_DEFAULT},
    706      *                        {@link #REPLACE_STRATEGY_NON_EXISTENT},
    707      *                        {@link #REPLACE_STRATEGY_ALL}
    708      *
    709      * @throws IllegalStateException if not initialized yet
    710      * @throws IllegalArgumentException in the following cases:
    711      *                                  {@code start < 0}, {@code end < 0}, {@code end < start},
    712      *                                  {@code start > charSequence.length()},
    713      *                                  {@code end > charSequence.length()}
    714      *                                  {@code maxEmojiCount < 0}
    715      */
    716     @CheckResult
    717     public CharSequence process(@NonNull final CharSequence charSequence,
    718             @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
    719             @IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) {
    720         Preconditions.checkState(isInitialized(), "Not initialized yet");
    721         Preconditions.checkArgumentNonnegative(start, "start cannot be negative");
    722         Preconditions.checkArgumentNonnegative(end, "end cannot be negative");
    723         Preconditions.checkArgumentNonnegative(maxEmojiCount, "maxEmojiCount cannot be negative");
    724         Preconditions.checkArgument(start <= end, "start should be <= than end");
    725 
    726         // early return since there is nothing to do
    727         //noinspection ConstantConditions
    728         if (charSequence == null) {
    729             return charSequence;
    730         }
    731 
    732         Preconditions.checkArgument(start <= charSequence.length(),
    733                 "start should be < than charSequence length");
    734         Preconditions.checkArgument(end <= charSequence.length(),
    735                 "end should be < than charSequence length");
    736 
    737         // early return since there is nothing to do
    738         if (charSequence.length() == 0 || start == end) {
    739             return charSequence;
    740         }
    741 
    742         final boolean replaceAll;
    743         switch (replaceStrategy) {
    744             case REPLACE_STRATEGY_ALL:
    745                 replaceAll = true;
    746                 break;
    747             case REPLACE_STRATEGY_NON_EXISTENT:
    748                 replaceAll = false;
    749                 break;
    750             case REPLACE_STRATEGY_DEFAULT:
    751             default:
    752                 replaceAll = mReplaceAll;
    753                 break;
    754         }
    755 
    756         return mHelper.process(charSequence, start, end, maxEmojiCount, replaceAll);
    757     }
    758 
    759     /**
    760      * Returns signature for the currently loaded emoji assets. The signature is a SHA that is
    761      * constructed using emoji assets. Can be used to detect if currently loaded asset is different
    762      * then previous executions. When used on devices running API 18 or below, returns empty string.
    763      *
    764      * @throws IllegalStateException if not initialized yet
    765      */
    766     @NonNull
    767     public String getAssetSignature() {
    768         Preconditions.checkState(isInitialized(), "Not initialized yet");
    769         return mHelper.getAssetSignature();
    770     }
    771 
    772     /**
    773      * Updates the EditorInfo attributes in order to communicate information to Keyboards. When
    774      * used on devices running API 18 or below, does not update EditorInfo attributes.
    775      *
    776      * @param outAttrs EditorInfo instance passed to
    777      *                 {@link android.widget.TextView#onCreateInputConnection(EditorInfo)}
    778      *
    779      * @see #EDITOR_INFO_METAVERSION_KEY
    780      * @see #EDITOR_INFO_REPLACE_ALL_KEY
    781      *
    782      * @hide
    783      */
    784     @RestrictTo(LIBRARY_GROUP)
    785     public void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
    786         //noinspection ConstantConditions
    787         if (isInitialized() && outAttrs != null && outAttrs.extras != null) {
    788             mHelper.updateEditorInfoAttrs(outAttrs);
    789         }
    790     }
    791 
    792     /**
    793      * Factory class that creates the EmojiSpans. By default it creates {@link TypefaceEmojiSpan}.
    794      *
    795      * @hide
    796      */
    797     @RestrictTo(LIBRARY_GROUP)
    798     @RequiresApi(19)
    799     static class SpanFactory {
    800         /**
    801          * Create EmojiSpan instance.
    802          *
    803          * @param metadata EmojiMetadata instance
    804          *
    805          * @return EmojiSpan instance
    806          */
    807         EmojiSpan createSpan(@NonNull final EmojiMetadata metadata) {
    808             return new TypefaceEmojiSpan(metadata);
    809         }
    810     }
    811 
    812     /**
    813      * Listener class for the initialization of the EmojiCompat.
    814      */
    815     public abstract static class InitCallback {
    816         /**
    817          * Called when EmojiCompat is initialized and the emoji data is loaded. When used on devices
    818          * running API 18 or below, this function is always called.
    819          */
    820         public void onInitialized() {
    821         }
    822 
    823         /**
    824          * Called when an unrecoverable error occurs during EmojiCompat initialization. When used on
    825          * devices running API 18 or below, this function is never called.
    826          */
    827         public void onFailed(@Nullable Throwable throwable) {
    828         }
    829     }
    830 
    831     /**
    832      * Interface to load emoji metadata.
    833      */
    834     public interface MetadataRepoLoader {
    835         /**
    836          * Start loading the metadata. When the loading operation is finished {@link
    837          * MetadataRepoLoaderCallback#onLoaded(MetadataRepo)} or
    838          * {@link MetadataRepoLoaderCallback#onFailed(Throwable)} should be called. When used on
    839          * devices running API 18 or below, this function is never called.
    840          *
    841          * @param loaderCallback callback to signal the loading state
    842          */
    843         void load(@NonNull MetadataRepoLoaderCallback loaderCallback);
    844     }
    845 
    846     /**
    847      * Callback to inform EmojiCompat about the state of the metadata load. Passed to
    848      * MetadataRepoLoader during {@link MetadataRepoLoader#load(MetadataRepoLoaderCallback)} call.
    849      */
    850     public abstract static class MetadataRepoLoaderCallback {
    851         /**
    852          * Called by {@link MetadataRepoLoader} when metadata is loaded successfully.
    853          *
    854          * @param metadataRepo MetadataRepo instance, cannot be {@code null}
    855          */
    856         public abstract void onLoaded(@NonNull MetadataRepo metadataRepo);
    857 
    858         /**
    859          * Called by {@link MetadataRepoLoader} if an error occurs while loading the metadata.
    860          *
    861          * @param throwable the exception that caused the failure, {@code nullable}
    862          */
    863         public abstract void onFailed(@Nullable Throwable throwable);
    864     }
    865 
    866     /**
    867      * Configuration class for EmojiCompat. Changes to the values will be ignored after
    868      * {@link #init(Config)} is called.
    869      *
    870      * @see #init(EmojiCompat.Config)
    871      */
    872     public abstract static class Config {
    873         private final MetadataRepoLoader mMetadataLoader;
    874         private boolean mReplaceAll;
    875         private boolean mUseEmojiAsDefaultStyle;
    876         private int[] mEmojiAsDefaultStyleExceptions;
    877         private Set<InitCallback> mInitCallbacks;
    878         private boolean mEmojiSpanIndicatorEnabled;
    879         private int mEmojiSpanIndicatorColor = Color.GREEN;
    880         @LoadStrategy private int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
    881 
    882         /**
    883          * Default constructor.
    884          *
    885          * @param metadataLoader MetadataRepoLoader instance, cannot be {@code null}
    886          */
    887         protected Config(@NonNull final MetadataRepoLoader metadataLoader) {
    888             Preconditions.checkNotNull(metadataLoader, "metadataLoader cannot be null.");
    889             mMetadataLoader = metadataLoader;
    890         }
    891 
    892         /**
    893          * Registers an initialization callback.
    894          *
    895          * @param initCallback the initialization callback to register, cannot be {@code null}
    896          *
    897          * @return EmojiCompat.Config instance
    898          */
    899         public Config registerInitCallback(@NonNull InitCallback initCallback) {
    900             Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
    901             if (mInitCallbacks == null) {
    902                 mInitCallbacks = new ArraySet<>();
    903             }
    904 
    905             mInitCallbacks.add(initCallback);
    906 
    907             return this;
    908         }
    909 
    910         /**
    911          * Unregisters a callback that was added before.
    912          *
    913          * @param initCallback the initialization callback to be removed, cannot be {@code null}
    914          *
    915          * @return EmojiCompat.Config instance
    916          */
    917         public Config unregisterInitCallback(@NonNull InitCallback initCallback) {
    918             Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
    919             if (mInitCallbacks != null) {
    920                 mInitCallbacks.remove(initCallback);
    921             }
    922             return this;
    923         }
    924 
    925         /**
    926          * Determines whether EmojiCompat should replace all the emojis it finds with the
    927          * EmojiSpans. By default EmojiCompat tries its best to understand if the system already
    928          * can render an emoji and do not replace those emojis.
    929          *
    930          * @param replaceAll replace all emojis found with EmojiSpans
    931          *
    932          * @return EmojiCompat.Config instance
    933          */
    934         public Config setReplaceAll(final boolean replaceAll) {
    935             mReplaceAll = replaceAll;
    936             return this;
    937         }
    938 
    939         /**
    940          * Determines whether EmojiCompat should use the emoji presentation style for emojis
    941          * that have text style as default. By default, the text style would be used, unless these
    942          * are followed by the U+FE0F variation selector.
    943          * Details about emoji presentation and text presentation styles can be found here:
    944          * http://unicode.org/reports/tr51/#Presentation_Style
    945          * If useEmojiAsDefaultStyle is true, the emoji presentation style will be used for all
    946          * emojis, including potentially unexpected ones (such as digits or other keycap emojis). If
    947          * this is not the expected behaviour, method
    948          * {@link #setUseEmojiAsDefaultStyle(boolean, List)} can be used to specify the
    949          * exception emojis that should be still presented as text style.
    950          *
    951          * @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis
    952          *                               that would be presented as text style by default
    953          */
    954         public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {
    955             return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle, null);
    956         }
    957 
    958         /**
    959          * @see #setUseEmojiAsDefaultStyle(boolean)
    960          *
    961          * @param emojiAsDefaultStyleExceptions Contains the exception emojis which will be still
    962          *                                      presented as text style even if the
    963          *                                      useEmojiAsDefaultStyle flag is set to {@code true}.
    964          *                                      This list will be ignored if useEmojiAsDefaultStyle
    965          *                                      is {@code false}. Note that emojis with default
    966          *                                      emoji style presentation will remain emoji style
    967          *                                      regardless the value of useEmojiAsDefaultStyle or
    968          *                                      whether they are included in the exceptions list or
    969          *                                      not. When no exception is wanted, the method
    970          *                                      {@link #setUseEmojiAsDefaultStyle(boolean)} should
    971          *                                      be used instead.
    972          */
    973         public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
    974                 @Nullable final List<Integer> emojiAsDefaultStyleExceptions) {
    975             mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
    976             if (mUseEmojiAsDefaultStyle && emojiAsDefaultStyleExceptions != null) {
    977                 mEmojiAsDefaultStyleExceptions = new int[emojiAsDefaultStyleExceptions.size()];
    978                 int i = 0;
    979                 for (Integer exception : emojiAsDefaultStyleExceptions) {
    980                     mEmojiAsDefaultStyleExceptions[i++] = exception;
    981                 }
    982                 Arrays.sort(mEmojiAsDefaultStyleExceptions);
    983             } else {
    984                 mEmojiAsDefaultStyleExceptions = null;
    985             }
    986             return this;
    987         }
    988 
    989         /**
    990          * Determines whether a background will be drawn for the emojis that are found and
    991          * replaced by EmojiCompat. Should be used only for debugging purposes. The indicator color
    992          * can be set using {@link #setEmojiSpanIndicatorColor(int)}.
    993          *
    994          * @param emojiSpanIndicatorEnabled when {@code true} a background is drawn for each emoji
    995          *                                  that is replaced
    996          */
    997         public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {
    998             mEmojiSpanIndicatorEnabled = emojiSpanIndicatorEnabled;
    999             return this;
   1000         }
   1001 
   1002         /**
   1003          * Sets the color used as emoji span indicator. The default value is
   1004          * {@link Color#GREEN Color.GREEN}.
   1005          *
   1006          * @see #setEmojiSpanIndicatorEnabled(boolean)
   1007          */
   1008         public Config setEmojiSpanIndicatorColor(@ColorInt int color) {
   1009             mEmojiSpanIndicatorColor = color;
   1010             return this;
   1011         }
   1012 
   1013         /**
   1014          * Determines the strategy to start loading the metadata. By default {@link EmojiCompat}
   1015          * will start loading the metadata during {@link EmojiCompat#init(Config)}. When set to
   1016          * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}, you should call {@link EmojiCompat#load()} to
   1017          * initiate metadata loading.
   1018          *
   1019          * @param strategy one of {@link EmojiCompat#LOAD_STRATEGY_DEFAULT},
   1020          *                  {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
   1021          *
   1022          */
   1023         public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
   1024             mMetadataLoadStrategy = strategy;
   1025             return this;
   1026         }
   1027 
   1028         /**
   1029          * Returns the {@link MetadataRepoLoader}.
   1030          */
   1031         protected final MetadataRepoLoader getMetadataRepoLoader() {
   1032             return mMetadataLoader;
   1033         }
   1034     }
   1035 
   1036     /**
   1037      * Runnable to call success/failure case for the listeners.
   1038      */
   1039     private static class ListenerDispatcher implements Runnable {
   1040         private final List<InitCallback> mInitCallbacks;
   1041         private final Throwable mThrowable;
   1042         private final int mLoadState;
   1043 
   1044         @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
   1045         ListenerDispatcher(@NonNull final InitCallback initCallback,
   1046                 @LoadState final int loadState) {
   1047             this(Arrays.asList(Preconditions.checkNotNull(initCallback,
   1048                     "initCallback cannot be null")), loadState, null);
   1049         }
   1050 
   1051         ListenerDispatcher(@NonNull final Collection<InitCallback> initCallbacks,
   1052                 @LoadState final int loadState) {
   1053             this(initCallbacks, loadState, null);
   1054         }
   1055 
   1056         ListenerDispatcher(@NonNull final Collection<InitCallback> initCallbacks,
   1057                 @LoadState final int loadState,
   1058                 @Nullable final Throwable throwable) {
   1059             Preconditions.checkNotNull(initCallbacks, "initCallbacks cannot be null");
   1060             mInitCallbacks = new ArrayList<>(initCallbacks);
   1061             mLoadState = loadState;
   1062             mThrowable = throwable;
   1063         }
   1064 
   1065         @Override
   1066         public void run() {
   1067             final int size = mInitCallbacks.size();
   1068             switch (mLoadState) {
   1069                 case LOAD_STATE_SUCCEEDED:
   1070                     for (int i = 0; i < size; i++) {
   1071                         mInitCallbacks.get(i).onInitialized();
   1072                     }
   1073                     break;
   1074                 case LOAD_STATE_FAILED:
   1075                 default:
   1076                     for (int i = 0; i < size; i++) {
   1077                         mInitCallbacks.get(i).onFailed(mThrowable);
   1078                     }
   1079                     break;
   1080             }
   1081         }
   1082     }
   1083 
   1084     /**
   1085      * Internal helper class to behave no-op for certain functions.
   1086      */
   1087     private static class CompatInternal {
   1088         final EmojiCompat mEmojiCompat;
   1089 
   1090         CompatInternal(EmojiCompat emojiCompat) {
   1091             mEmojiCompat = emojiCompat;
   1092         }
   1093 
   1094         void loadMetadata() {
   1095             // Moves into LOAD_STATE_SUCCESS state immediately.
   1096             mEmojiCompat.onMetadataLoadSuccess();
   1097         }
   1098 
   1099         boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
   1100             // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
   1101             return false;
   1102         }
   1103 
   1104         boolean hasEmojiGlyph(@NonNull final CharSequence sequence, final int metadataVersion) {
   1105             // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
   1106             return false;
   1107         }
   1108 
   1109         CharSequence process(@NonNull final CharSequence charSequence,
   1110                 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
   1111                 @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) {
   1112             // Returns the given charSequence as it is.
   1113             return charSequence;
   1114         }
   1115 
   1116         void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
   1117             // Does not add any EditorInfo attributes.
   1118         }
   1119 
   1120         void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) {
   1121             // intentionally empty
   1122         }
   1123 
   1124         String getAssetSignature() {
   1125             return "";
   1126         }
   1127     }
   1128 
   1129     @RequiresApi(19)
   1130     private static final class CompatInternal19 extends CompatInternal {
   1131         /**
   1132          * Responsible to process a CharSequence and add the spans. @{code Null} until the time the
   1133          * metadata is loaded.
   1134          */
   1135         private volatile EmojiProcessor mProcessor;
   1136 
   1137         /**
   1138          * Keeps the information about emojis. Null until the time the data is loaded.
   1139          */
   1140         private volatile MetadataRepo mMetadataRepo;
   1141 
   1142 
   1143         CompatInternal19(EmojiCompat emojiCompat) {
   1144             super(emojiCompat);
   1145         }
   1146 
   1147         @Override
   1148         void loadMetadata() {
   1149             try {
   1150                 final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
   1151                     @Override
   1152                     public void onLoaded(@NonNull MetadataRepo metadataRepo) {
   1153                         onMetadataLoadSuccess(metadataRepo);
   1154                     }
   1155 
   1156                     @Override
   1157                     public void onFailed(@Nullable Throwable throwable) {
   1158                         mEmojiCompat.onMetadataLoadFailed(throwable);
   1159                     }
   1160                 };
   1161                 mEmojiCompat.mMetadataLoader.load(callback);
   1162             } catch (Throwable t) {
   1163                 mEmojiCompat.onMetadataLoadFailed(t);
   1164             }
   1165         }
   1166 
   1167         private void onMetadataLoadSuccess(@NonNull final MetadataRepo metadataRepo) {
   1168             //noinspection ConstantConditions
   1169             if (metadataRepo == null) {
   1170                 mEmojiCompat.onMetadataLoadFailed(
   1171                         new IllegalArgumentException("metadataRepo cannot be null"));
   1172                 return;
   1173             }
   1174 
   1175             mMetadataRepo = metadataRepo;
   1176             mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory(),
   1177                     mEmojiCompat.mUseEmojiAsDefaultStyle,
   1178                     mEmojiCompat.mEmojiAsDefaultStyleExceptions);
   1179 
   1180             mEmojiCompat.onMetadataLoadSuccess();
   1181         }
   1182 
   1183         @Override
   1184         boolean hasEmojiGlyph(@NonNull CharSequence sequence) {
   1185             return mProcessor.getEmojiMetadata(sequence) != null;
   1186         }
   1187 
   1188         @Override
   1189         boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) {
   1190             final EmojiMetadata emojiMetadata = mProcessor.getEmojiMetadata(sequence);
   1191             return emojiMetadata != null && emojiMetadata.getCompatAdded() <= metadataVersion;
   1192         }
   1193 
   1194         @Override
   1195         CharSequence process(@NonNull CharSequence charSequence, int start, int end,
   1196                 int maxEmojiCount, boolean replaceAll) {
   1197             return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll);
   1198         }
   1199 
   1200         @Override
   1201         void updateEditorInfoAttrs(@NonNull EditorInfo outAttrs) {
   1202             outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion());
   1203             outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll);
   1204         }
   1205 
   1206         @Override
   1207         void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) {
   1208             mProcessor.setGlyphChecker(glyphChecker);
   1209         }
   1210 
   1211         @Override
   1212         String getAssetSignature() {
   1213             final String sha = mMetadataRepo.getMetadataList().sourceSha();
   1214             return sha == null ? "" : sha;
   1215         }
   1216     }
   1217 }
   1218