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