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 */
16package androidx.emoji.text;
17
18import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19
20import android.graphics.Color;
21import android.os.Build;
22import android.os.Handler;
23import android.os.Looper;
24import android.text.Editable;
25import android.text.method.KeyListener;
26import android.view.KeyEvent;
27import android.view.inputmethod.EditorInfo;
28import android.view.inputmethod.InputConnection;
29
30import androidx.annotation.AnyThread;
31import androidx.annotation.CheckResult;
32import androidx.annotation.ColorInt;
33import androidx.annotation.GuardedBy;
34import androidx.annotation.IntDef;
35import androidx.annotation.IntRange;
36import androidx.annotation.NonNull;
37import androidx.annotation.Nullable;
38import androidx.annotation.RequiresApi;
39import androidx.annotation.RestrictTo;
40import androidx.annotation.VisibleForTesting;
41import androidx.collection.ArraySet;
42import androidx.core.util.Preconditions;
43
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.Collection;
49import java.util.List;
50import java.util.Set;
51import java.util.concurrent.locks.ReadWriteLock;
52import 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
80public 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