initCallbacks = new ArrayList<>();
+ mInitLock.writeLock().lock();
+ try {
+ mLoadState = LOAD_STATE_FAILED;
+ initCallbacks.addAll(mInitCallbacks);
+ mInitCallbacks.clear();
+ } finally {
+ mInitLock.writeLock().unlock();
+ }
+ mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState, throwable));
+ }
+
+ /**
+ * Registers an initialization callback. If the initialization is already completed by the time
+ * the listener is added, the callback functions are called immediately. Callbacks are called on
+ * the main looper.
+ *
+ * When used on devices running API 18 or below, {@link InitCallback#onInitialized()} is called
+ * without loading any metadata. In such cases {@link InitCallback#onFailed(Throwable)} is never
+ * called.
+ *
+ * @param initCallback the initialization callback to register, cannot be {@code null}
+ *
+ * @see #unregisterInitCallback(InitCallback)
+ */
+ @SuppressWarnings("ExecutorRegistration")
+ public void registerInitCallback(@NonNull InitCallback initCallback) {
+ Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
+
+ mInitLock.writeLock().lock();
+ try {
+ if (mLoadState == LOAD_STATE_SUCCEEDED || mLoadState == LOAD_STATE_FAILED) {
+ mMainHandler.post(new ListenerDispatcher(initCallback, mLoadState));
+ } else {
+ mInitCallbacks.add(initCallback);
+ }
+ } finally {
+ mInitLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Unregisters a callback that was added before.
+ *
+ * @param initCallback the callback to be removed, cannot be {@code null}
+ */
+ public void unregisterInitCallback(@NonNull InitCallback initCallback) {
+ Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
+ mInitLock.writeLock().lock();
+ try {
+ mInitCallbacks.remove(initCallback);
+ } finally {
+ mInitLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Returns loading state of the EmojiCompat instance. When used on devices running API 18 or
+ * below always returns {@link #LOAD_STATE_SUCCEEDED}.
+ *
+ * @return one of {@link #LOAD_STATE_DEFAULT}, {@link #LOAD_STATE_LOADING},
+ * {@link #LOAD_STATE_SUCCEEDED}, {@link #LOAD_STATE_FAILED}
+ */
+ public @LoadState int getLoadState() {
+ mInitLock.readLock().lock();
+ try {
+ return mLoadState;
+ } finally {
+ mInitLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * @return {@code true} if EmojiCompat is successfully initialized
+ */
+ private boolean isInitialized() {
+ return getLoadState() == LOAD_STATE_SUCCEEDED;
+ }
+
+ /**
+ * @return whether a background should be drawn for the emoji for debugging
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public boolean isEmojiSpanIndicatorEnabled() {
+ return mEmojiSpanIndicatorEnabled;
+ }
+
+ /**
+ * @return color of background drawn if {@link EmojiCompat#isEmojiSpanIndicatorEnabled} is true
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public @ColorInt int getEmojiSpanIndicatorColor() {
+ return mEmojiSpanIndicatorColor;
+ }
+
+ /**
+ * Together with {@link #getEmojiEnd(CharSequence, int)}, if the character at {@code offset} is
+ * part of an emoji, returns the index range of that emoji, start index inclusively/end index
+ * exclusively so that {@code charSequence.subSequence(start, end)} will return that emoji.
+ * E.g., getEmojiStart/End("AB😀", 1) will return (-1,-1) since 'B' is not part an emoji;
+ * getEmojiStart/End("AB😀", 3) will return [2,4), note that "😀" contains 2 Chars.
+ * Returns -1 otherwise.
+ * @param charSequence the whole sequence
+ * @param offset index of the emoji to look up
+ * @return the start index inclusively/end index exclusively
+ */
+ public int getEmojiStart(@NonNull final CharSequence charSequence,
+ @IntRange(from = 0) int offset) {
+ return mHelper.getEmojiStart(charSequence, offset);
+ }
+
+ /**
+ * see {@link #getEmojiStart(CharSequence, int)}.
+ */
+ public int getEmojiEnd(@NonNull final CharSequence charSequence,
+ @IntRange(from = 0) int offset) {
+ return mHelper.getEmojiEnd(charSequence, offset);
+ }
+
+ /**
+ * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
+ * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
+ * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
+ * deleted with the characters it covers.
+ *
+ * If there is a selection where selection start is not equal to selection end, does not
+ * delete.
+ *
+ * When used on devices running API 18 or below, always returns {@code false}.
+ *
+ * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
+ * Editable, int, KeyEvent)}
+ * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
+ * int, KeyEvent)}
+ * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
+ * int, KeyEvent)}
+ *
+ * @return {@code true} if an {@link EmojiSpan} is deleted
+ */
+ public static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
+ @NonNull final KeyEvent event) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return EmojiProcessor.handleOnKeyDown(editable, keyCode, event);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
+ * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
+ * deleted.
+ *
+ * If there is a selection where selection start is not equal to selection end, does not
+ * delete.
+ *
+ * When used on devices running API 18 or below, always returns {@code false}.
+ *
+ * @param inputConnection InputConnection instance
+ * @param editable TextView.Editable instance
+ * @param beforeLength the number of characters before the cursor to be deleted
+ * @param afterLength the number of characters after the cursor to be deleted
+ * @param inCodePoints {@code true} if length parameters are in codepoints
+ *
+ * @return {@code true} if an {@link EmojiSpan} is deleted
+ */
+ public static boolean handleDeleteSurroundingText(
+ @NonNull final InputConnection inputConnection, @NonNull final Editable editable,
+ @IntRange(from = 0) final int beforeLength, @IntRange(from = 0) final int afterLength,
+ final boolean inCodePoints) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return EmojiProcessor.handleDeleteSurroundingText(inputConnection, editable,
+ beforeLength, afterLength, inCodePoints);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns {@code true} if EmojiCompat is capable of rendering an emoji. When used on devices
+ * running API 18 or below, always returns {@code false}.
+ *
+ * @deprecated use getEmojiMatch which returns more accurate lookup information.
+ *
+ * @param sequence CharSequence representing the emoji
+ *
+ * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null}
+ *
+ * @throws IllegalStateException if not initialized yet
+ */
+ @Deprecated
+ public boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
+ Preconditions.checkState(isInitialized(), "Not initialized yet");
+ Preconditions.checkNotNull(sequence, "sequence cannot be null");
+ return mHelper.hasEmojiGlyph(sequence);
+ }
+
+ /**
+ * Returns {@code true} if EmojiCompat is capable of rendering an emoji at the given metadata
+ * version. When used on devices running API 18 or below, always returns {@code false}.
+ *
+ * @deprecated use getEmojiMatch which returns more accurate lookup information.
+ *
+ * @param sequence CharSequence representing the emoji
+ * @param metadataVersion the metadata version to check against, should be greater than or
+ * equal to {@code 0},
+ *
+ * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null}
+ *
+ * @throws IllegalStateException if not initialized yet
+ */
+ @Deprecated
+ public boolean hasEmojiGlyph(@NonNull final CharSequence sequence,
+ @IntRange(from = 0) final int metadataVersion) {
+ Preconditions.checkState(isInitialized(), "Not initialized yet");
+ Preconditions.checkNotNull(sequence, "sequence cannot be null");
+ return mHelper.hasEmojiGlyph(sequence, metadataVersion);
+ }
+
+ /**
+ * Attempts to lookup the entire sequence at the specified metadata version and returns what
+ * the runtime match behavior would be.
+ *
+ * To be used by keyboards to show or hide emoji in response to specific metadata support.
+ *
+ * @see #EMOJI_SUPPORTED
+ * @see #EMOJI_UNSUPPORTED
+ * @see #EMOJI_FALLBACK
+ *
+ * @param sequence CharSequence representing an emoji
+ * @param metadataVersion the metada version to check against, should be greater than or
+ * equal to {@code 0},
+ * @return A match result, or decomposes if replaceAll would cause partial subsequence matches.
+ */
+ @CodepointSequenceMatchResult
+ public int getEmojiMatch(@NonNull CharSequence sequence,
+ @IntRange(from = 0) final int metadataVersion) {
+ Preconditions.checkState(isInitialized(), "Not initialized yet");
+ Preconditions.checkNotNull(sequence, "sequence cannot be null");
+ return mHelper.getEmojiMatch(sequence, metadataVersion);
+ }
+
+ /**
+ * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. When
+ * used on devices running API 18 or below, returns the given {@code charSequence} without
+ * processing it.
+ *
+ * @param charSequence CharSequence to add the EmojiSpans
+ *
+ * @throws IllegalStateException if not initialized yet
+ * @see #process(CharSequence, int, int)
+ */
+ @Nullable
+ @CheckResult
+ public CharSequence process(@Nullable final CharSequence charSequence) {
+ // since charSequence might be null here we have to check it. Passing through here to the
+ // main function so that it can do all the checks including isInitialized. It will also
+ // be the main point that decides what to return.
+
+ @IntRange(from = 0) final int length = charSequence == null ? 0 : charSequence.length();
+ return process(charSequence, 0, length);
+ }
+
+ /**
+ * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+ *
+ *
+ * - If no emojis are found, {@code charSequence} given as the input is returned without
+ * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
+ * returned.
+ * - If the given input is not a Spannable (such as String), and at least one emoji is found
+ * a new {@link android.text.Spannable} instance is returned.
+ * - If the given input is a Spannable, the same instance is returned.
+ *
+ * When used on devices running API 18 or below, returns the given {@code charSequence} without
+ * processing it.
+ *
+ * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
+ * @param start start index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code 0}, also less than or equal to {@code charSequence.length()}
+ * @param end end index in the charSequence to look for emojis, should be greater than or equal
+ * to {@code start} parameter, also less than or equal to
+ * {@code charSequence.length()}
+ *
+ * @throws IllegalStateException if not initialized yet
+ * @throws IllegalArgumentException in the following cases:
+ * {@code start < 0}, {@code end < 0}, {@code end < start},
+ * {@code start > charSequence.length()},
+ * {@code end > charSequence.length()}
+ */
+ @Nullable
+ @CheckResult
+ public CharSequence process(@Nullable final CharSequence charSequence,
+ @IntRange(from = 0) final int start, @IntRange(from = 0) final int end) {
+ return process(charSequence, start, end, EMOJI_COUNT_UNLIMITED);
+ }
+
+ /**
+ * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+ *
+ *
+ * - If no emojis are found, {@code charSequence} given as the input is returned without
+ * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
+ * returned.
+ * - If the given input is not a Spannable (such as String), and at least one emoji is found
+ * a new {@link android.text.Spannable} instance is returned.
+ * - If the given input is a Spannable, the same instance is returned.
+ *
+ * When used on devices running API 18 or below, returns the given {@code charSequence} without
+ * processing it.
+ *
+ * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
+ * @param start start index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code 0}, also less than or equal to {@code charSequence.length()}
+ * @param end end index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code start} parameter, also less than or equal to
+ * {@code charSequence.length()}
+ * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
+ * than or equal to {@code 0}
+ *
+ * @throws IllegalStateException if not initialized yet
+ * @throws IllegalArgumentException in the following cases:
+ * {@code start < 0}, {@code end < 0}, {@code end < start},
+ * {@code start > charSequence.length()},
+ * {@code end > charSequence.length()}
+ * {@code maxEmojiCount < 0}
+ */
+ @Nullable
+ @CheckResult
+ public CharSequence process(@Nullable final CharSequence charSequence,
+ @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
+ @IntRange(from = 0) final int maxEmojiCount) {
+ return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT);
+ }
+
+ /**
+ * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+ *
+ *
+ * - If no emojis are found, {@code charSequence} given as the input is returned without
+ * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
+ * returned.
+ * - If the given input is not a Spannable (such as String), and at least one emoji is found
+ * a new {@link android.text.Spannable} instance is returned.
+ * - If the given input is a Spannable, the same instance is returned.
+ *
+ * When used on devices running API 18 or below, returns the given {@code charSequence} without
+ * processing it.
+ *
+ * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
+ * @param start start index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code 0}, also less than or equal to {@code charSequence.length()}
+ * @param end end index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code start} parameter, also less than or equal to
+ * {@code charSequence.length()}
+ * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
+ * than or equal to {@code 0}
+ * @param replaceStrategy whether to replace all emoji with {@link EmojiSpan}s, should be one of
+ * {@link #REPLACE_STRATEGY_DEFAULT},
+ * {@link #REPLACE_STRATEGY_NON_EXISTENT},
+ * {@link #REPLACE_STRATEGY_ALL}
+ *
+ * @throws IllegalStateException if not initialized yet
+ * @throws IllegalArgumentException in the following cases:
+ * {@code start < 0}, {@code end < 0}, {@code end < start},
+ * {@code start > charSequence.length()},
+ * {@code end > charSequence.length()}
+ * {@code maxEmojiCount < 0}
+ */
+ @Nullable
+ @CheckResult
+ public CharSequence process(@Nullable final CharSequence charSequence,
+ @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
+ @IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) {
+ Preconditions.checkState(isInitialized(), "Not initialized yet");
+ Preconditions.checkArgumentNonnegative(start, "start cannot be negative");
+ Preconditions.checkArgumentNonnegative(end, "end cannot be negative");
+ Preconditions.checkArgumentNonnegative(maxEmojiCount, "maxEmojiCount cannot be negative");
+ Preconditions.checkArgument(start <= end, "start should be <= than end");
+
+ // early return since there is nothing to do
+ if (charSequence == null) {
+ return null;
+ }
+
+ Preconditions.checkArgument(start <= charSequence.length(),
+ "start should be < than charSequence length");
+ Preconditions.checkArgument(end <= charSequence.length(),
+ "end should be < than charSequence length");
+
+ // early return since there is nothing to do
+ if (charSequence.length() == 0 || start == end) {
+ return charSequence;
+ }
+
+ final boolean replaceAll;
+ switch (replaceStrategy) {
+ case REPLACE_STRATEGY_ALL:
+ replaceAll = true;
+ break;
+ case REPLACE_STRATEGY_NON_EXISTENT:
+ replaceAll = false;
+ break;
+ case REPLACE_STRATEGY_DEFAULT:
+ default:
+ replaceAll = mReplaceAll;
+ break;
+ }
+
+ return mHelper.process(charSequence, start, end, maxEmojiCount, replaceAll);
+ }
+
+ /**
+ * Returns signature for the currently loaded emoji assets. The signature is a SHA that is
+ * constructed using emoji assets. Can be used to detect if currently loaded asset is different
+ * then previous executions. When used on devices running API 18 or below, returns empty string.
+ *
+ * @throws IllegalStateException if not initialized yet
+ */
+ @NonNull
+ public String getAssetSignature() {
+ Preconditions.checkState(isInitialized(), "Not initialized yet");
+ return mHelper.getAssetSignature();
+ }
+
+ /**
+ * Updates the EditorInfo attributes in order to communicate information to Keyboards. When
+ * used on devices running API 18 or below, does not update EditorInfo attributes.
+ *
+ * This is called from EditText integrations that use EmojiEditTextHelper. Custom
+ * widgets that allow IME not subclassing EditText should call this method when creating an
+ * input connection.
+ *
+ * When EmojiCompat is not in {@link #LOAD_STATE_SUCCEEDED}, this method has no effect.
+ *
+ * Calling this method on API levels below API 19 will have no effect, as EmojiCompat may
+ * never be configured. However, it is always safe to call, even on older API levels.
+ *
+ * @param outAttrs EditorInfo instance passed to
+ * {@link android.widget.TextView#onCreateInputConnection(EditorInfo)}
+ *
+ * @see #EDITOR_INFO_METAVERSION_KEY
+ * @see #EDITOR_INFO_REPLACE_ALL_KEY
+ */
+ public void updateEditorInfo(@NonNull final EditorInfo outAttrs) {
+ //noinspection ConstantConditions
+ if (!isInitialized() || outAttrs == null) {
+ return;
+ }
+ if (outAttrs.extras == null) {
+ outAttrs.extras = new Bundle();
+ }
+ mHelper.updateEditorInfoAttrs(outAttrs);
+ }
+
+ /**
+ * Factory class that creates the EmojiSpans.
+ *
+ * By default it creates {@link TypefaceEmojiSpan}.
+ *
+ * Apps should use this only if they want to control the drawing of EmojiSpans for non-standard
+ * emoji display (for example, resizing or repositioning emoji).
+ */
+ public interface SpanFactory {
+ /**
+ * Create EmojiSpan instance.
+ *
+ * @param rasterizer TypefaceEmojiRasterizer instance, which can draw the emoji onto a
+ * Canvas.
+ *
+ * @return EmojiSpan instance that can use TypefaceEmojiRasterizer to draw emoji.
+ */
+ @RequiresApi(19)
+ @NonNull
+ EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer rasterizer);
+ }
+
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static class DefaultSpanFactory implements SpanFactory {
+
+ /**
+ * Returns a TypefaceEmojiSpan.
+ *
+ * @param rasterizer TypefaceEmojiRasterizer instance, which can draw the emoji onto a
+ * Canvas.
+ *
+ * @return {@link TypefaceEmojiSpan}
+ */
+ @RequiresApi(19)
+ @NonNull
+ @Override
+ public EmojiSpan createSpan(@NonNull TypefaceEmojiRasterizer rasterizer) {
+ return new TypefaceEmojiSpan(rasterizer);
+ }
+ }
+
+ /**
+ * Listener class for the initialization of the EmojiCompat.
+ */
+ public abstract static class InitCallback {
+ /**
+ * Called when EmojiCompat is initialized and the emoji data is loaded. When used on devices
+ * running API 18 or below, this function is always called.
+ */
+ public void onInitialized() {
+ }
+
+ /**
+ * Called when an unrecoverable error occurs during EmojiCompat initialization. When used on
+ * devices running API 18 or below, this function is never called.
+ */
+ public void onFailed(@SuppressWarnings("unused") @Nullable Throwable throwable) {
+ }
+ }
+
+ /**
+ * Interface to load emoji metadata.
+ */
+ public interface MetadataRepoLoader {
+ /**
+ * Start loading the metadata. When the loading operation is finished {@link
+ * MetadataRepoLoaderCallback#onLoaded(MetadataRepo)} or
+ * {@link MetadataRepoLoaderCallback#onFailed(Throwable)} should be called. When used on
+ * devices running API 18 or below, this function is never called.
+ *
+ * @param loaderCallback callback to signal the loading state
+ */
+ @SuppressWarnings("ExecutorRegistration")
+ void load(@NonNull MetadataRepoLoaderCallback loaderCallback);
+ }
+
+ /**
+ * Interface to check if a given emoji exists on the system.
+ */
+ public interface GlyphChecker {
+ /**
+ * Return {@code true} if the emoji that is in {@code charSequence} between
+ * {@code start}(inclusive) and {@code end}(exclusive) can be rendered on the system
+ * using the default Typeface.
+ *
+ * This function is called after an emoji is identified in the given {@code charSequence}
+ * and EmojiCompat wants to know if that emoji can be rendered on the system. The result
+ * of this call will be cached and the same emoji sequence won't be asked for the same
+ * EmojiCompat instance.
+ *
+ *
When the function returns {@code true}, it will mean that the system can render the
+ * emoji. In that case if {@link Config#setReplaceAll} is set to {@code false}, then no
+ * {@link EmojiSpan} will be added in the final emoji processing result.
+ *
+ *
When the function returns {@code false}, it will mean that the system cannot render
+ * the given emoji, therefore an {@link EmojiSpan} will be added to the final emoji
+ * processing result.
+ *
+ *
The default implementation of this class uses
+ * {@link androidx.core.graphics.PaintCompat#hasGlyph(Paint, String)} function to check
+ * if the emoji can be rendered on the system. This is required even if EmojiCompat
+ * knows about the SDK Version that the emoji was added on AOSP. Just the {@code sdkAdded}
+ * information is not enough to reliably decide if emoji can be rendered since this
+ * information may not be consistent across all the OEMs and all the Android versions.
+ *
+ *
With this interface you can apply your own heuristics to check if the emoji can be
+ * rendered on the system. For example, if you'd like to rely on the {@code sdkAdded}
+ * information, and some predefined OEMs, it is possible to write the following code
+ * snippet.
+ *
+ * {@sample frameworks/support/samples/SupportEmojiDemos/src/main/java/com/example/android/support/text/emoji/sample/GlyphCheckerSample.java glyphchecker}
+ *
+ * @param charSequence the CharSequence that is being processed
+ * @param start the inclusive starting offset for the emoji in the {@code charSequence}
+ * @param end the exclusive end offset for the emoji in the {@code charSequence}
+ * @param sdkAdded the API version that the emoji was added in AOSP
+ *
+ * @return true if the given sequence can be rendered as a single glyph, otherwise false.
+ */
+ boolean hasGlyph(
+ @NonNull CharSequence charSequence,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @IntRange(from = 0) int sdkAdded
+ );
+ }
+
+ /**
+ * Callback to inform EmojiCompat about the state of the metadata load. Passed to
+ * MetadataRepoLoader during {@link MetadataRepoLoader#load(MetadataRepoLoaderCallback)} call.
+ */
+ public abstract static class MetadataRepoLoaderCallback {
+ /**
+ * Called by {@link MetadataRepoLoader} when metadata is loaded successfully.
+ *
+ * @param metadataRepo MetadataRepo instance, cannot be {@code null}
+ */
+ public abstract void onLoaded(@NonNull MetadataRepo metadataRepo);
+
+ /**
+ * Called by {@link MetadataRepoLoader} if an error occurs while loading the metadata.
+ *
+ * @param throwable the exception that caused the failure, {@code nullable}
+ */
+ public abstract void onFailed(@Nullable Throwable throwable);
+ }
+
+ /**
+ * Configuration class for EmojiCompat. Changes to the values will be ignored after
+ * {@link #init(Config)} is called.
+ *
+ * @see #init(EmojiCompat.Config)
+ */
+ public abstract static class Config {
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull
+ final MetadataRepoLoader mMetadataLoader;
+
+ /**
+ * Used to create new EmojiSpans.
+ *
+ * May be set by developer using config to fully customize emoji display.
+ */
+ SpanFactory mSpanFactory;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mReplaceAll;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mUseEmojiAsDefaultStyle;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @Nullable
+ int[] mEmojiAsDefaultStyleExceptions;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @Nullable
+ Set mInitCallbacks;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean mEmojiSpanIndicatorEnabled;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ int mEmojiSpanIndicatorColor = Color.GREEN;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @LoadStrategy int mMetadataLoadStrategy = LOAD_STRATEGY_DEFAULT;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @NonNull
+ GlyphChecker mGlyphChecker = new DefaultGlyphChecker();
+
+ /**
+ * Default constructor.
+ *
+ * @param metadataLoader MetadataRepoLoader instance, cannot be {@code null}
+ */
+ protected Config(@NonNull final MetadataRepoLoader metadataLoader) {
+ Preconditions.checkNotNull(metadataLoader, "metadataLoader cannot be null.");
+ mMetadataLoader = metadataLoader;
+ }
+
+ /**
+ * Registers an initialization callback.
+ *
+ * @param initCallback the initialization callback to register, cannot be {@code null}
+ *
+ * @return EmojiCompat.Config instance
+ */
+ @SuppressWarnings("ExecutorRegistration")
+ @NonNull
+ public Config registerInitCallback(@NonNull InitCallback initCallback) {
+ Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
+ if (mInitCallbacks == null) {
+ mInitCallbacks = new ArraySet<>();
+ }
+
+ mInitCallbacks.add(initCallback);
+
+ return this;
+ }
+
+ /**
+ * Unregisters a callback that was added before.
+ *
+ * @param initCallback the initialization callback to be removed, cannot be {@code null}
+ *
+ * @return EmojiCompat.Config instance
+ */
+ @NonNull
+ public Config unregisterInitCallback(@NonNull InitCallback initCallback) {
+ Preconditions.checkNotNull(initCallback, "initCallback cannot be null");
+ if (mInitCallbacks != null) {
+ mInitCallbacks.remove(initCallback);
+ }
+ return this;
+ }
+
+ /**
+ * Determines whether EmojiCompat should replace all the emojis it finds with the
+ * EmojiSpans. By default EmojiCompat tries its best to understand if the system already
+ * can render an emoji and do not replace those emojis.
+ *
+ * @param replaceAll replace all emojis found with EmojiSpans
+ *
+ * @return EmojiCompat.Config instance
+ */
+ @NonNull
+ public Config setReplaceAll(final boolean replaceAll) {
+ mReplaceAll = replaceAll;
+ return this;
+ }
+
+ /**
+ * Determines whether EmojiCompat should use the emoji presentation style for emojis
+ * that have text style as default. By default, the text style would be used, unless these
+ * are followed by the U+FE0F variation selector.
+ * Details about emoji presentation and text presentation styles can be found here:
+ * http://unicode.org/reports/tr51/#Presentation_Style
+ * If useEmojiAsDefaultStyle is true, the emoji presentation style will be used for all
+ * emojis, including potentially unexpected ones (such as digits or other keycap emojis). If
+ * this is not the expected behaviour, method
+ * {@link #setUseEmojiAsDefaultStyle(boolean, List)} can be used to specify the
+ * exception emojis that should be still presented as text style.
+ *
+ * @param useEmojiAsDefaultStyle whether to use the emoji style presentation for all emojis
+ * that would be presented as text style by default
+ */
+ @NonNull
+ public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {
+ return setUseEmojiAsDefaultStyle(useEmojiAsDefaultStyle,
+ null);
+ }
+
+ /**
+ * @see #setUseEmojiAsDefaultStyle(boolean)
+ *
+ * @param emojiAsDefaultStyleExceptions Contains the exception emojis which will be still
+ * presented as text style even if the
+ * useEmojiAsDefaultStyle flag is set to {@code true}.
+ * This list will be ignored if useEmojiAsDefaultStyle
+ * is {@code false}. Note that emojis with default
+ * emoji style presentation will remain emoji style
+ * regardless the value of useEmojiAsDefaultStyle or
+ * whether they are included in the exceptions list or
+ * not. When no exception is wanted, the method
+ * {@link #setUseEmojiAsDefaultStyle(boolean)} should
+ * be used instead.
+ */
+ @NonNull
+ public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
+ @Nullable final List emojiAsDefaultStyleExceptions) {
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ if (mUseEmojiAsDefaultStyle && emojiAsDefaultStyleExceptions != null) {
+ mEmojiAsDefaultStyleExceptions = new int[emojiAsDefaultStyleExceptions.size()];
+ int i = 0;
+ for (Integer exception : emojiAsDefaultStyleExceptions) {
+ mEmojiAsDefaultStyleExceptions[i++] = exception;
+ }
+ Arrays.sort(mEmojiAsDefaultStyleExceptions);
+ } else {
+ mEmojiAsDefaultStyleExceptions = null;
+ }
+ return this;
+ }
+
+ /**
+ * Determines whether a background will be drawn for the emojis that are found and
+ * replaced by EmojiCompat. Should be used only for debugging purposes. The indicator color
+ * can be set using {@link #setEmojiSpanIndicatorColor(int)}.
+ *
+ * @param emojiSpanIndicatorEnabled when {@code true} a background is drawn for each emoji
+ * that is replaced
+ */
+ @NonNull
+ public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {
+ mEmojiSpanIndicatorEnabled = emojiSpanIndicatorEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the color used as emoji span indicator. The default value is
+ * {@link Color#GREEN Color.GREEN}.
+ *
+ * @see #setEmojiSpanIndicatorEnabled(boolean)
+ */
+ @NonNull
+ public Config setEmojiSpanIndicatorColor(@ColorInt int color) {
+ mEmojiSpanIndicatorColor = color;
+ return this;
+ }
+
+ /**
+ * Determines the strategy to start loading the metadata. By default {@link EmojiCompat}
+ * will start loading the metadata during {@link EmojiCompat#init(Config)}. When set to
+ * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}, you should call {@link EmojiCompat#load()} to
+ * initiate metadata loading.
+ *
+ * Default implementations of {@link EmojiCompat.MetadataRepoLoader} start a thread
+ * during their {@link EmojiCompat.MetadataRepoLoader#load} functions. Just instantiating
+ * and starting a thread might take time especially in older devices. Since
+ * {@link EmojiCompat#init(Config)} has to be called before any EmojiCompat widgets are
+ * inflated, this results in time spent either on your Application.onCreate or Activity
+ * .onCreate. If you'd like to gain more control on when to start loading the metadata
+ * and be able to call {@link EmojiCompat#init(Config)} with absolute minimum time cost you
+ * can use {@link EmojiCompat#LOAD_STRATEGY_MANUAL}.
+ *
+ * When set to {@link EmojiCompat#LOAD_STRATEGY_MANUAL}, {@link EmojiCompat} will wait
+ * for {@link #load()} to be called by the developer in order to start loading metadata,
+ * therefore you should call {@link EmojiCompat#load()} to initiate metadata loading.
+ * {@link #load()} can be called from any thread.
+ *
+ * EmojiCompat.Config config = new FontRequestEmojiCompatConfig(context, fontRequest)
+ * .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+ *
+ * // EmojiCompat will not start loading metadata and MetadataRepoLoader#load(...)
+ * // will not be called
+ * EmojiCompat.init(config);
+ *
+ * // At any time (i.e. idle time or executorService is ready)
+ * // call EmojiCompat#load() to start loading metadata.
+ * executorService.execute(() -> EmojiCompat.get().load());
+ *
+ *
+ * @param strategy one of {@link EmojiCompat#LOAD_STRATEGY_DEFAULT},
+ * {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
+ *
+ */
+ @NonNull
+ public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
+ mMetadataLoadStrategy = strategy;
+ return this;
+ }
+
+ /**
+ * Set the span factory used to actually draw emoji replacements.
+ *
+ * @param factory custum span factory that can draw the emoji replacements
+ * @return this
+ */
+ @NonNull
+ public Config setSpanFactory(@NonNull SpanFactory factory) {
+ mSpanFactory = factory;
+ return this;
+ }
+
+ /**
+ * The interface that is used by EmojiCompat in order to check if a given emoji can be
+ * rendered by the system.
+ *
+ * @param glyphChecker {@link GlyphChecker} instance to be used.
+ */
+ @NonNull
+ public Config setGlyphChecker(@NonNull GlyphChecker glyphChecker) {
+ Preconditions.checkNotNull(glyphChecker, "GlyphChecker cannot be null");
+ mGlyphChecker = glyphChecker;
+ return this;
+ }
+
+ /**
+ * Returns the {@link MetadataRepoLoader}.
+ */
+ @NonNull
+ protected final MetadataRepoLoader getMetadataRepoLoader() {
+ return mMetadataLoader;
+ }
+ }
+
+ /**
+ * Runnable to call success/failure case for the listeners.
+ */
+ private static class ListenerDispatcher implements Runnable {
+ private final List mInitCallbacks;
+ private final Throwable mThrowable;
+ private final int mLoadState;
+
+ @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+ ListenerDispatcher(@NonNull final InitCallback initCallback,
+ @LoadState final int loadState) {
+ this(Arrays.asList(Preconditions.checkNotNull(initCallback,
+ "initCallback cannot be null")), loadState, null);
+ }
+
+ ListenerDispatcher(@NonNull final Collection initCallbacks,
+ @LoadState final int loadState) {
+ this(initCallbacks, loadState, null);
+ }
+
+ ListenerDispatcher(@NonNull final Collection initCallbacks,
+ @LoadState final int loadState,
+ @Nullable final Throwable throwable) {
+ Preconditions.checkNotNull(initCallbacks, "initCallbacks cannot be null");
+ mInitCallbacks = new ArrayList<>(initCallbacks);
+ mLoadState = loadState;
+ mThrowable = throwable;
+ }
+
+ @Override
+ public void run() {
+ final int size = mInitCallbacks.size();
+ switch (mLoadState) {
+ case LOAD_STATE_SUCCEEDED:
+ for (int i = 0; i < size; i++) {
+ mInitCallbacks.get(i).onInitialized();
+ }
+ break;
+ case LOAD_STATE_FAILED:
+ default:
+ for (int i = 0; i < size; i++) {
+ mInitCallbacks.get(i).onFailed(mThrowable);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Internal helper class to behave no-op for certain functions.
+ */
+ private static class CompatInternal {
+ final EmojiCompat mEmojiCompat;
+
+ CompatInternal(EmojiCompat emojiCompat) {
+ mEmojiCompat = emojiCompat;
+ }
+
+ void loadMetadata() {
+ // Moves into LOAD_STATE_SUCCESS state immediately.
+ mEmojiCompat.onMetadataLoadSuccess();
+ }
+
+ boolean hasEmojiGlyph(@NonNull final CharSequence sequence) {
+ // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
+ return false;
+ }
+
+ boolean hasEmojiGlyph(@NonNull final CharSequence sequence, final int metadataVersion) {
+ // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis.
+ return false;
+ }
+
+ int getEmojiStart(@NonNull final CharSequence cs, @IntRange(from = 0) final int offset) {
+ // Since no metadata is loaded, EmojiCompat cannot detect any emojis.
+ return -1;
+ }
+
+ int getEmojiEnd(@NonNull final CharSequence cs, @IntRange(from = 0) final int offset) {
+ // Since no metadata is loaded, EmojiCompat cannot detect any emojis.
+ return -1;
+ }
+
+ CharSequence process(@NonNull final CharSequence charSequence,
+ @IntRange(from = 0) final int start, @IntRange(from = 0) final int end,
+ @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) {
+ // Returns the given charSequence as it is.
+ return charSequence;
+ }
+
+ void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
+ // Does not add any EditorInfo attributes.
+ }
+
+ String getAssetSignature() {
+ return "";
+ }
+
+ @CodepointSequenceMatchResult
+ public int getEmojiMatch(CharSequence sequence, int metadataVersion) {
+ return EMOJI_UNSUPPORTED;
+ }
+ }
+
+ @RequiresApi(19)
+ private static final class CompatInternal19 extends CompatInternal {
+ /**
+ * Responsible to process a CharSequence and add the spans. @{code Null} until the time the
+ * metadata is loaded.
+ */
+ private volatile EmojiProcessor mProcessor;
+
+ /**
+ * Keeps the information about emojis. Null until the time the data is loaded.
+ */
+ private volatile MetadataRepo mMetadataRepo;
+
+
+ CompatInternal19(EmojiCompat emojiCompat) {
+ super(emojiCompat);
+ }
+
+ @Override
+ void loadMetadata() {
+ try {
+ final MetadataRepoLoaderCallback callback = new MetadataRepoLoaderCallback() {
+ @Override
+ public void onLoaded(@NonNull MetadataRepo metadataRepo) {
+ onMetadataLoadSuccess(metadataRepo);
+ }
+
+ @Override
+ public void onFailed(@Nullable Throwable throwable) {
+ mEmojiCompat.onMetadataLoadFailed(throwable);
+ }
+ };
+ mEmojiCompat.mMetadataLoader.load(callback);
+ } catch (Throwable t) {
+ mEmojiCompat.onMetadataLoadFailed(t);
+ }
+ }
+
+ @SuppressWarnings("SyntheticAccessor")
+ void onMetadataLoadSuccess(@NonNull final MetadataRepo metadataRepo) {
+ //noinspection ConstantConditions
+ if (metadataRepo == null) {
+ mEmojiCompat.onMetadataLoadFailed(
+ new IllegalArgumentException("metadataRepo cannot be null"));
+ return;
+ }
+
+ mMetadataRepo = metadataRepo;
+ mProcessor = new EmojiProcessor(
+ mMetadataRepo,
+ mEmojiCompat.mSpanFactory,
+ mEmojiCompat.mGlyphChecker,
+ mEmojiCompat.mUseEmojiAsDefaultStyle,
+ mEmojiCompat.mEmojiAsDefaultStyleExceptions,
+ EmojiExclusions.getEmojiExclusions()
+ );
+
+ mEmojiCompat.onMetadataLoadSuccess();
+ }
+
+ @Override
+ boolean hasEmojiGlyph(@NonNull CharSequence sequence) {
+ return mProcessor.getEmojiMatch(sequence) == EMOJI_SUPPORTED;
+ }
+
+ @Override
+ boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) {
+ int emojiMatch = mProcessor.getEmojiMatch(sequence, metadataVersion);
+ return emojiMatch == EMOJI_SUPPORTED;
+ }
+
+ @Override
+ public int getEmojiMatch(CharSequence sequence, int metadataVersion) {
+ return mProcessor.getEmojiMatch(sequence, metadataVersion);
+ }
+
+ @Override
+ int getEmojiStart(@NonNull final CharSequence sequence, final int offset) {
+ return mProcessor.getEmojiStart(sequence, offset);
+ }
+
+ @Override
+ int getEmojiEnd(@NonNull final CharSequence sequence, final int offset) {
+ return mProcessor.getEmojiEnd(sequence, offset);
+ }
+
+ @Override
+ CharSequence process(@NonNull CharSequence charSequence, int start, int end,
+ int maxEmojiCount, boolean replaceAll) {
+ return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll);
+ }
+
+ @Override
+ void updateEditorInfoAttrs(@NonNull EditorInfo outAttrs) {
+ outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion());
+ outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll);
+ }
+
+ @Override
+ String getAssetSignature() {
+ final String sha = mMetadataRepo.getMetadataList().sourceSha();
+ return sha == null ? "" : sha;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/EmojiCompatInitializer.java b/app/src/main/java/androidx/emoji2/text/EmojiCompatInitializer.java
new file mode 100644
index 0000000000..f4823bf095
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/EmojiCompatInitializer.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
+import androidx.core.os.TraceCompat;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.ProcessLifecycleInitializer;
+import androidx.startup.AppInitializer;
+import androidx.startup.Initializer;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * Initializer for configuring EmojiCompat with the system installed downloadable font provider.
+ *
+ * This initializer will initialize EmojiCompat immediately then defer loading the font for a
+ * short delay to avoid delaying application startup. Typically, the font will be loaded shortly
+ * after the first screen of your application loads, which means users may see system emoji
+ * briefly prior to the compat font loading.
+ *
+ * This is the recommended configuration for all apps that don't need specialized configuration,
+ * and don't need to control the background thread that initialization runs on. For more information
+ * see {@link androidx.emoji2.text.DefaultEmojiCompatConfig}.
+ *
+ * In addition to the reasons listed in {@code DefaultEmojiCompatConfig} you may wish to disable
+ * this automatic configuration if you intend to call initialization from an existing background
+ * thread pool in your application.
+ *
+ * This is enabled by default by including the {@code :emoji2:emoji2} gradle artifact. To
+ * disable the default configuration (and allow manual configuration) add this to your manifest:
+ *
+ *
+ * <provider
+ * android:name="androidx.startup.InitializationProvider"
+ * android:authorities="${applicationId}.androidx-startup"
+ * android:exported="false"
+ * tools:node="merge">
+ * <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
+ * tools:node="remove" />
+ * </provider>
+ *
+ *
+ * This initializer depends on {@link ProcessLifecycleInitializer}.
+ *
+ * @see androidx.emoji2.text.DefaultEmojiCompatConfig
+ */
+public class EmojiCompatInitializer implements Initializer {
+ private static final long STARTUP_THREAD_CREATION_DELAY_MS = 500L;
+ private static final String S_INITIALIZER_THREAD_NAME = "EmojiCompatInitializer";
+
+ /**
+ * Initialize EmojiCompat with the app's context.
+ *
+ * @param context application context
+ * @return result of default init
+ */
+ @SuppressWarnings("AutoBoxing")
+ @NonNull
+ @Override
+ public Boolean create(@NonNull Context context) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ EmojiCompat.init(new BackgroundDefaultConfig(context));
+ delayUntilFirstResume(context);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Wait until the first frame of the application to do anything.
+ *
+ * This allows startup code to run before the delay is scheduled.
+ */
+ @RequiresApi(19)
+ void delayUntilFirstResume(@NonNull Context context) {
+ // schedule delay after first Activity resumes
+ AppInitializer appInitializer = AppInitializer.getInstance(context);
+ LifecycleOwner lifecycleOwner = appInitializer
+ .initializeComponent(ProcessLifecycleInitializer.class);
+ Lifecycle lifecycle = lifecycleOwner.getLifecycle();
+ lifecycle.addObserver(new DefaultLifecycleObserver() {
+ @Override
+ public void onResume(@NonNull LifecycleOwner owner) {
+ loadEmojiCompatAfterDelay();
+ lifecycle.removeObserver(this);
+ }
+ });
+ }
+
+ @RequiresApi(19)
+ void loadEmojiCompatAfterDelay() {
+ final Handler mainHandler = ConcurrencyHelpers.mainHandlerAsync();
+ mainHandler.postDelayed(new LoadEmojiCompatRunnable(), STARTUP_THREAD_CREATION_DELAY_MS);
+ }
+
+ /**
+ * Dependes on ProcessLifecycleInitializer
+ */
+ @NonNull
+ @Override
+ public List>> dependencies() {
+ return Collections.singletonList(ProcessLifecycleInitializer.class);
+ }
+
+ static class LoadEmojiCompatRunnable implements Runnable {
+ @Override
+ public void run() {
+ try {
+ // this is main thread, so mark what we're doing (this trace includes thread
+ // start time in BackgroundLoadingLoader.load
+ TraceCompat.beginSection("EmojiCompat.EmojiCompatInitializer.run");
+ if (EmojiCompat.isConfigured()) {
+ EmojiCompat.get().load();
+ }
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+ }
+
+ @RequiresApi(19)
+ static class BackgroundDefaultConfig extends EmojiCompat.Config {
+ protected BackgroundDefaultConfig(Context context) {
+ super(new BackgroundDefaultLoader(context));
+ setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+ }
+ }
+
+ @RequiresApi(19)
+ static class BackgroundDefaultLoader implements EmojiCompat.MetadataRepoLoader {
+ private final Context mContext;
+
+ BackgroundDefaultLoader(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ ThreadPoolExecutor executor = ConcurrencyHelpers.createBackgroundPriorityExecutor(
+ S_INITIALIZER_THREAD_NAME);
+ executor.execute(() -> doLoad(loaderCallback, executor));
+ }
+
+ @WorkerThread
+ void doLoad(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback,
+ @NonNull ThreadPoolExecutor executor) {
+ try {
+ FontRequestEmojiCompatConfig config = DefaultEmojiCompatConfig.create(mContext);
+ if (config == null) {
+ throw new RuntimeException("EmojiCompat font provider not available on this "
+ + "device.");
+ }
+ config.setLoadingExecutor(executor);
+ config.getMetadataRepoLoader().load(new EmojiCompat.MetadataRepoLoaderCallback() {
+ @Override
+ public void onLoaded(@NonNull MetadataRepo metadataRepo) {
+ try {
+ // main thread is notified before returning, so we can quit now
+ loaderCallback.onLoaded(metadataRepo);
+ } finally {
+ executor.shutdown();
+ }
+ }
+
+ @Override
+ public void onFailed(@Nullable Throwable throwable) {
+ try {
+ // main thread is notified before returning, so we can quit now
+ loaderCallback.onFailed(throwable);
+ } finally {
+ executor.shutdown();
+ }
+ }
+ });
+ } catch (Throwable t) {
+ loaderCallback.onFailed(t);
+ executor.shutdown();
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/androidx/emoji2/text/EmojiDefaults.java b/app/src/main/java/androidx/emoji2/text/EmojiDefaults.java
new file mode 100644
index 0000000000..9818af1d8e
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/EmojiDefaults.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import androidx.annotation.RestrictTo;
+
+/**
+ * Defaults for emojicompat
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class EmojiDefaults {
+
+ private EmojiDefaults() {}
+
+ /**
+ * Default value for maxEmojiCount if not specified.
+ */
+ public static final int MAX_EMOJI_COUNT = Integer.MAX_VALUE;
+}
diff --git a/app/src/main/java/androidx/emoji2/text/EmojiExclusions.java b/app/src/main/java/androidx/emoji2/text/EmojiExclusions.java
new file mode 100644
index 0000000000..f93959c831
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/EmojiExclusions.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Set;
+
+class EmojiExclusions {
+ private EmojiExclusions() { /* cannot instantiate */ }
+
+ @NonNull
+ static Set getEmojiExclusions() {
+ if (Build.VERSION.SDK_INT >= 34) {
+ return EmojiExclusions_Api34.getExclusions();
+ } else {
+ return EmojiExclusions_Reflections.getExclusions();
+ }
+ }
+
+ @RequiresApi(34)
+ private static class EmojiExclusions_Api34 {
+ private EmojiExclusions_Api34() { /* cannot instantiate */ }
+
+ @NonNull
+ @DoNotInline
+ static Set getExclusions() {
+ // TODO: Call directly when API34 is published
+ return EmojiExclusions_Reflections.getExclusions();
+ }
+ }
+
+ private static class EmojiExclusions_Reflections {
+ private EmojiExclusions_Reflections() { /* cannot instantiate */ }
+
+ /**
+ * Attempt to reflectively call EmojiExclusion
+ *
+ * If anything goes wrong, return Collections.emptySet.
+ */
+ @SuppressWarnings("unchecked")
+ // will be checked after platform API for 34 published
+ @SuppressLint({ "BanUncheckedReflection" })
+ @NonNull
+ static Set getExclusions() {
+ try {
+ Class> clazz = Class.forName("android.text.EmojiConsistency");
+ Method method = clazz.getMethod("getEmojiConsistencySet");
+ Object result = method.invoke(null);
+ if (result == null) {
+ return Collections.emptySet();
+ }
+ // validate the result type before exposing it to caller
+ Set> resultList = (Set>) result;
+ for (Object item : resultList) {
+ if (!(item instanceof int[])) {
+ return Collections.emptySet();
+ }
+ }
+ return (Set) resultList;
+ } catch (Throwable ignore) {
+ return Collections.emptySet();
+
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/androidx/emoji2/text/EmojiProcessor.java b/app/src/main/java/androidx/emoji2/text/EmojiProcessor.java
new file mode 100644
index 0000000000..6efcd7a99b
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/EmojiProcessor.java
@@ -0,0 +1,1000 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.view.KeyEvent;
+import android.view.inputmethod.InputConnection;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Processes the CharSequence and adds the emojis.
+ *
+ * @hide
+ */
+@AnyThread
+@RestrictTo(LIBRARY)
+@RequiresApi(19)
+final class EmojiProcessor {
+
+ /**
+ * State transition commands.
+ */
+ @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface Action {
+ }
+
+ private interface EmojiProcessCallback {
+ /**
+ * Invoked on every emoji found during {@link #process}.
+ * Returning {@code false} can abort this {@link #process} loop.
+ */
+ boolean handleEmoji(@NonNull CharSequence charSequence, int start, int end,
+ TypefaceEmojiRasterizer metadata);
+
+ /**
+ * @return the result after process.
+ */
+ T getResult();
+ }
+
+ /**
+ * Advance the end pointer in CharSequence and reset the start to be the end.
+ */
+ private static final int ACTION_ADVANCE_BOTH = 1;
+
+ /**
+ * Advance end pointer in CharSequence.
+ */
+ private static final int ACTION_ADVANCE_END = 2;
+
+ /**
+ * Add a new emoji with the metadata in {@link ProcessorSm#getFlushMetadata()}. Advance end
+ * pointer in CharSequence and reset the start to be the end.
+ */
+ private static final int ACTION_FLUSH = 3;
+
+ /**
+ * The max number of characters look around in {@link #getEmojiStart(CharSequence, int)} and
+ * {@link #getEmojiEnd(CharSequence, int)}.
+ */
+ private static final int MAX_LOOK_AROUND_CHARACTER = 16;
+
+ /**
+ * Factory used to create EmojiSpans.
+ */
+ @NonNull
+ private final EmojiCompat.SpanFactory mSpanFactory;
+
+ /**
+ * Emoji metadata repository.
+ */
+ @NonNull
+ private final MetadataRepo mMetadataRepo;
+
+ /**
+ * Utility class that checks if the system can render a given glyph.
+ */
+ @NonNull
+ private EmojiCompat.GlyphChecker mGlyphChecker;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
+ */
+ private final boolean mUseEmojiAsDefaultStyle;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
+ */
+ @Nullable
+ private final int[] mEmojiAsDefaultStyleExceptions;
+
+ EmojiProcessor(
+ @NonNull final MetadataRepo metadataRepo,
+ @NonNull final EmojiCompat.SpanFactory spanFactory,
+ @NonNull final EmojiCompat.GlyphChecker glyphChecker,
+ final boolean useEmojiAsDefaultStyle,
+ @Nullable final int[] emojiAsDefaultStyleExceptions,
+ @NonNull Set emojiExclusions) {
+ mSpanFactory = spanFactory;
+ mMetadataRepo = metadataRepo;
+ mGlyphChecker = glyphChecker;
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
+ initExclusions(emojiExclusions);
+ }
+
+ private void initExclusions(@NonNull Set emojiExclusions) {
+ if (emojiExclusions.isEmpty()) {
+ return;
+ }
+ for (int[] codepoints : emojiExclusions) {
+ String emoji = new String(codepoints, 0, codepoints.length);
+ MarkExclusionCallback callback = new MarkExclusionCallback(emoji);
+ process(emoji, 0, emoji.length(), 1, true, callback);
+ }
+ }
+
+ @EmojiCompat.CodepointSequenceMatchResult
+ int getEmojiMatch(@NonNull final CharSequence charSequence) {
+ return getEmojiMatch(charSequence, mMetadataRepo.getMetadataVersion());
+ }
+
+ @EmojiCompat.CodepointSequenceMatchResult
+ int getEmojiMatch(@NonNull final CharSequence charSequence,
+ final int metadataVersion) {
+ final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+ mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
+ final int end = charSequence.length();
+ int currentOffset = 0;
+ int potentialSubsequenceMatch = 0;
+ int subsequenceMatch = 0;
+
+ while (currentOffset < end) {
+ final int codePoint = Character.codePointAt(charSequence, currentOffset);
+ final int action = sm.check(codePoint);
+ TypefaceEmojiRasterizer currentNode = sm.getCurrentMetadata();
+ switch (action) {
+ case ACTION_FLUSH: {
+ // this happens when matching new unknown ZWJ sequences that are comprised of
+ // known emoji
+ currentNode = sm.getFlushMetadata();
+ if (currentNode.getCompatAdded() <= metadataVersion) {
+ subsequenceMatch++;
+ }
+ break;
+ }
+ case ACTION_ADVANCE_BOTH: {
+ currentOffset += Character.charCount(codePoint);
+ // state machine decided to skip previous entries
+ potentialSubsequenceMatch = 0;
+ break;
+ } case ACTION_ADVANCE_END: {
+ currentOffset += Character.charCount(codePoint);
+ break;
+ }
+ }
+ if (currentNode != null && currentNode.getCompatAdded() <= metadataVersion) {
+ potentialSubsequenceMatch++;
+ }
+ }
+
+ if (subsequenceMatch != 0) {
+ // if we matched multiple emoji on the first pass, then the current emoji font
+ // doesn't know about the codepoint sequence, and will decompose when REPLACE_ALL = true
+ return EmojiCompat.EMOJI_FALLBACK;
+ }
+
+ if (sm.isInFlushableState()) {
+ // We matched exactly one emoji
+ // EmojiCompat can completely handle this sequence
+ TypefaceEmojiRasterizer exactMatch = sm.getCurrentMetadata();
+ if (exactMatch.getCompatAdded() <= metadataVersion) {
+ return EmojiCompat.EMOJI_SUPPORTED;
+ }
+ }
+ // if we get here than we definitely do not know the emoji, decide if we will decompose
+ if (potentialSubsequenceMatch == 0) {
+ return EmojiCompat.EMOJI_UNSUPPORTED;
+ } else {
+ return EmojiCompat.EMOJI_FALLBACK;
+ }
+ }
+
+
+ /**
+ * see {@link EmojiCompat#getEmojiStart(CharSequence, int)}.
+ */
+ int getEmojiStart(@NonNull final CharSequence charSequence, @IntRange(from = 0) int offset) {
+ if (offset < 0 || offset >= charSequence.length()) {
+ return -1;
+ }
+
+ if (charSequence instanceof Spanned) {
+ final Spanned spanned = (Spanned) charSequence;
+ final EmojiSpan[] spans = spanned.getSpans(offset, offset + 1, EmojiSpan.class);
+ if (spans.length > 0) {
+ return spanned.getSpanStart(spans[0]);
+ }
+ }
+
+ // TODO: come up with some heuristic logic to better determine the interval
+ final int start = Math.max(0, offset - MAX_LOOK_AROUND_CHARACTER);
+ final int end = Math.min(charSequence.length(), offset + MAX_LOOK_AROUND_CHARACTER);
+ return process(charSequence, start, end, EmojiCompat.EMOJI_COUNT_UNLIMITED, true,
+ new EmojiProcessLookupCallback(offset)).start;
+ }
+
+ /**
+ * see {@link EmojiCompat#getEmojiStart(CharSequence, int)}.
+ */
+ int getEmojiEnd(@NonNull final CharSequence charSequence, @IntRange(from = 0) int offset) {
+ if (offset < 0 || offset >= charSequence.length()) {
+ return -1;
+ }
+
+ if (charSequence instanceof Spanned) {
+ final Spanned spanned = (Spanned) charSequence;
+ final EmojiSpan[] spans = spanned.getSpans(offset, offset + 1, EmojiSpan.class);
+ if (spans.length > 0) {
+ return spanned.getSpanEnd(spans[0]);
+ }
+ }
+
+ // TODO: come up with some heuristic logic to better determine the interval
+ final int start = Math.max(0, offset - MAX_LOOK_AROUND_CHARACTER);
+ final int end = Math.min(charSequence.length(), offset + MAX_LOOK_AROUND_CHARACTER);
+ return process(charSequence, start, end, EmojiCompat.EMOJI_COUNT_UNLIMITED, true,
+ new EmojiProcessLookupCallback(offset)).end;
+ }
+
+ /**
+ * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+ *
+ *
+ * - If no emojis are found, {@code charSequence} given as the input is returned without
+ * any changes. i.e. charSequence is a String, and no emojis are found, the same String is
+ * returned.
+ * - If the given input is not a Spannable (such as String), and at least one emoji is found
+ * a new {@link android.text.Spannable} instance is returned.
+ * - If the given input is a Spannable, the same instance is returned.
+ *
+ *
+ * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null}
+ * @param start start index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code 0}, also less than {@code charSequence.length()}
+ * @param end end index in the charSequence to look for emojis, should be greater than or
+ * equal to {@code start} parameter, also less than {@code charSequence.length()}
+ * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater
+ * than or equal to {@code 0}
+ * @param replaceAll whether to replace all emoji with {@link EmojiSpan}s
+ */
+ CharSequence process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount,
+ final boolean replaceAll) {
+ final boolean isSpannableBuilder = charSequence instanceof SpannableBuilder;
+ if (isSpannableBuilder) {
+ ((SpannableBuilder) charSequence).beginBatchEdit();
+ }
+
+ try {
+ UnprecomputeTextOnModificationSpannable spannable = null;
+ // if it is a spannable already, use the same instance to add/remove EmojiSpans.
+ // otherwise wait until the first EmojiSpan found in order to change the result
+ // into a Spannable.
+ if (isSpannableBuilder || charSequence instanceof Spannable) {
+ spannable = new UnprecomputeTextOnModificationSpannable((Spannable) charSequence);
+ } else if (charSequence instanceof Spanned) {
+ // check if there are any EmojiSpans as cheap as possible
+ // start-1, end+1 will return emoji span that starts/ends at start/end indices
+ final int nextSpanTransition = ((Spanned) charSequence).nextSpanTransition(
+ start - 1, end + 1, EmojiSpan.class);
+
+ if (nextSpanTransition <= end) {
+ spannable = new UnprecomputeTextOnModificationSpannable(charSequence);
+ }
+ }
+
+ if (spannable != null) {
+ final EmojiSpan[] spans = spannable.getSpans(start, end, EmojiSpan.class);
+ if (spans != null && spans.length > 0) {
+ // remove existing spans, and realign the start, end according to spans
+ // if start or end is in the middle of an emoji they should be aligned
+ final int length = spans.length;
+ for (int index = 0; index < length; index++) {
+ final EmojiSpan span = spans[index];
+ final int spanStart = spannable.getSpanStart(span);
+ final int spanEnd = spannable.getSpanEnd(span);
+ // Remove span only when its spanStart is NOT equal to current end.
+ // During add operation an emoji at index 0 is added with 0-1 as start and
+ // end indices. Therefore if there are emoji spans at [0-1] and [1-2]
+ // and end is 1, the span between 0-1 should be deleted, not 1-2.
+ if (spanStart != end) {
+ spannable.removeSpan(span);
+ }
+ start = Math.min(spanStart, start);
+ end = Math.max(spanEnd, end);
+ }
+ }
+ }
+
+ if (start == end || start >= charSequence.length()) {
+ return charSequence;
+ }
+
+ // calculate max number of emojis that can be added. since getSpans call is a relatively
+ // expensive operation, do it only when maxEmojiCount is not unlimited.
+ if (maxEmojiCount != EmojiCompat.EMOJI_COUNT_UNLIMITED && spannable != null) {
+ maxEmojiCount -= spannable.getSpans(0, spannable.length(), EmojiSpan.class).length;
+ }
+
+ spannable = process(charSequence, start, end, maxEmojiCount, replaceAll,
+ new EmojiProcessAddSpanCallback(spannable, mSpanFactory));
+
+ // if nothing was written, always return the source
+ if (spannable != null) {
+ return spannable.getUnwrappedSpannable();
+ } else {
+ return charSequence;
+ }
+ } finally {
+ if (isSpannableBuilder) {
+ ((SpannableBuilder) charSequence).endBatchEdit();
+ }
+ }
+ }
+
+ private T process(@NonNull final CharSequence charSequence, @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end, @IntRange(from = 0) int maxEmojiCount,
+ final boolean processAll, final EmojiProcessCallback emojiProcessCallback) {
+ int addedCount = 0;
+ final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+ mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
+
+ int currentOffset = start;
+ int codePoint = Character.codePointAt(charSequence, currentOffset);
+ boolean keepProcessing = true;
+
+ while (currentOffset < end && addedCount < maxEmojiCount && keepProcessing) {
+ final int action = sm.check(codePoint);
+
+ switch (action) {
+ case ACTION_ADVANCE_BOTH:
+ start += Character.charCount(Character.codePointAt(charSequence, start));
+ currentOffset = start;
+ if (currentOffset < end) {
+ codePoint = Character.codePointAt(charSequence, currentOffset);
+ }
+ break;
+ case ACTION_ADVANCE_END:
+ currentOffset += Character.charCount(codePoint);
+ if (currentOffset < end) {
+ codePoint = Character.codePointAt(charSequence, currentOffset);
+ }
+ break;
+ case ACTION_FLUSH:
+ if (processAll || !hasGlyph(charSequence, start, currentOffset,
+ sm.getFlushMetadata())) {
+ keepProcessing = emojiProcessCallback.handleEmoji(charSequence, start,
+ currentOffset, sm.getFlushMetadata());
+ addedCount++;
+ }
+ start = currentOffset;
+ break;
+ }
+ }
+
+ // After the last codepoint is consumed the state machine might be in a state where it
+ // identified an emoji before. i.e. abc[women-emoji] when the last codepoint is consumed
+ // state machine is waiting to see if there is an emoji sequence (i.e. ZWJ).
+ // Need to check if it is in such a state.
+ if (sm.isInFlushableState() && addedCount < maxEmojiCount && keepProcessing) {
+ if (processAll || !hasGlyph(charSequence, start, currentOffset,
+ sm.getCurrentMetadata())) {
+ emojiProcessCallback.handleEmoji(charSequence, start,
+ currentOffset, sm.getCurrentMetadata());
+ addedCount++;
+ }
+ }
+
+ return emojiProcessCallback.getResult();
+ }
+
+ /**
+ * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of
+ * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an
+ * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
+ * deleted with the characters it covers.
+ *
+ * If there is a selection where selection start is not equal to selection end, does not
+ * delete.
+ *
+ * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View,
+ * Editable, int, KeyEvent)}
+ * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
+ * int, KeyEvent)}
+ * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable,
+ * int, KeyEvent)}
+ *
+ * @return {@code true} if an {@link EmojiSpan} is deleted
+ */
+ static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode,
+ @NonNull final KeyEvent event) {
+ final boolean handled;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DEL:
+ handled = delete(editable, event, false /*forwardDelete*/);
+ break;
+ case KeyEvent.KEYCODE_FORWARD_DEL:
+ handled = delete(editable, event, true /*forwardDelete*/);
+ break;
+ default:
+ handled = false;
+ break;
+ }
+
+ if (handled) {
+ MetaKeyKeyListener.adjustMetaAfterKeypress(editable);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static boolean delete(@NonNull final Editable content, @NonNull final KeyEvent event,
+ final boolean forwardDelete) {
+ if (hasModifiers(event)) {
+ return false;
+ }
+
+ final int start = Selection.getSelectionStart(content);
+ final int end = Selection.getSelectionEnd(content);
+ if (hasInvalidSelection(start, end)) {
+ return false;
+ }
+
+ final EmojiSpan[] spans = content.getSpans(start, end, EmojiSpan.class);
+ if (spans != null && spans.length > 0) {
+ final int length = spans.length;
+ for (int index = 0; index < length; index++) {
+ final EmojiSpan span = spans[index];
+ final int spanStart = content.getSpanStart(span);
+ final int spanEnd = content.getSpanEnd(span);
+ if ((forwardDelete && spanStart == start)
+ || (!forwardDelete && spanEnd == start)
+ || (start > spanStart && start < spanEnd)) {
+ content.delete(spanStart, spanEnd);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an
+ * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is
+ * deleted.
+ *
+ * If there is a selection where selection start is not equal to selection end, does not
+ * delete.
+ *
+ * @param inputConnection InputConnection instance
+ * @param editable TextView.Editable instance
+ * @param beforeLength the number of characters before the cursor to be deleted
+ * @param afterLength the number of characters after the cursor to be deleted
+ * @param inCodePoints {@code true} if length parameters are in codepoints
+ *
+ * @return {@code true} if an {@link EmojiSpan} is deleted
+ */
+ static boolean handleDeleteSurroundingText(@NonNull final InputConnection inputConnection,
+ @NonNull final Editable editable, @IntRange(from = 0) final int beforeLength,
+ @IntRange(from = 0) final int afterLength, final boolean inCodePoints) {
+ //noinspection ConstantConditions
+ if (editable == null || inputConnection == null) {
+ return false;
+ }
+
+ if (beforeLength < 0 || afterLength < 0) {
+ return false;
+ }
+
+ final int selectionStart = Selection.getSelectionStart(editable);
+ final int selectionEnd = Selection.getSelectionEnd(editable);
+
+ if (hasInvalidSelection(selectionStart, selectionEnd)) {
+ return false;
+ }
+
+ int start;
+ int end;
+ if (inCodePoints) {
+ // go backwards in terms of codepoints
+ start = CodepointIndexFinder.findIndexBackward(editable, selectionStart,
+ Math.max(beforeLength, 0));
+ end = CodepointIndexFinder.findIndexForward(editable, selectionEnd,
+ Math.max(afterLength, 0));
+
+ if (start == CodepointIndexFinder.INVALID_INDEX
+ || end == CodepointIndexFinder.INVALID_INDEX) {
+ return false;
+ }
+ } else {
+ start = Math.max(selectionStart - beforeLength, 0);
+ end = Math.min(selectionEnd + afterLength, editable.length());
+ }
+
+ final EmojiSpan[] spans = editable.getSpans(start, end, EmojiSpan.class);
+ if (spans != null && spans.length > 0) {
+ final int length = spans.length;
+ for (int index = 0; index < length; index++) {
+ final EmojiSpan span = spans[index];
+ int spanStart = editable.getSpanStart(span);
+ int spanEnd = editable.getSpanEnd(span);
+ start = Math.min(spanStart, start);
+ end = Math.max(spanEnd, end);
+ }
+
+ start = Math.max(start, 0);
+ end = Math.min(end, editable.length());
+
+ inputConnection.beginBatchEdit();
+ editable.delete(start, end);
+ inputConnection.endBatchEdit();
+ return true;
+ }
+
+ return false;
+ }
+
+ private static boolean hasInvalidSelection(final int start, final int end) {
+ return start == -1 || end == -1 || start != end;
+ }
+
+ private static boolean hasModifiers(@NonNull KeyEvent event) {
+ return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
+ }
+
+ /**
+ * Checks whether the current OS can render a given emoji. Used by the system to decide if an
+ * emoji span should be added. If the system cannot render it, an emoji span will be added.
+ * Used only for the case where replaceAll is set to {@code false}.
+ *
+ * @param charSequence the CharSequence that the emoji is in
+ * @param start start index of the emoji in the CharSequence
+ * @param end end index of the emoji in the CharSequence
+ * @param rasterizer TypefaceEmojiRasterizer instance for the emoji
+ *
+ * @return {@code true} if the OS can render emoji, {@code false} otherwise
+ */
+ private boolean hasGlyph(final CharSequence charSequence, int start, final int end,
+ final TypefaceEmojiRasterizer rasterizer) {
+ // if the existence is not calculated yet
+ if (rasterizer.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_UNKNOWN) {
+ final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end,
+ rasterizer.getSdkAdded());
+ rasterizer.setHasGlyph(hasGlyph);
+ }
+
+ return rasterizer.getHasGlyph() == TypefaceEmojiRasterizer.HAS_GLYPH_EXISTS;
+ }
+
+ /**
+ * State machine for walking over the metadata trie.
+ */
+ static final class ProcessorSm {
+
+ private static final int STATE_DEFAULT = 1;
+ private static final int STATE_WALKING = 2;
+
+ private int mState = STATE_DEFAULT;
+
+ /**
+ * Root of the trie
+ */
+ private final MetadataRepo.Node mRootNode;
+
+ /**
+ * Pointer to the node after last codepoint.
+ */
+ private MetadataRepo.Node mCurrentNode;
+
+ /**
+ * The node where ACTION_FLUSH is called. Required since after flush action is
+ * returned mCurrentNode is reset to be the root.
+ */
+ private MetadataRepo.Node mFlushNode;
+
+ /**
+ * The code point that was checked.
+ */
+ private int mLastCodepoint;
+
+ /**
+ * Level for mCurrentNode. Root is 0.
+ */
+ private int mCurrentDepth;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
+ */
+ private final boolean mUseEmojiAsDefaultStyle;
+
+ /**
+ * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
+ */
+ private final int[] mEmojiAsDefaultStyleExceptions;
+
+ ProcessorSm(MetadataRepo.Node rootNode, boolean useEmojiAsDefaultStyle,
+ int[] emojiAsDefaultStyleExceptions) {
+ mRootNode = rootNode;
+ mCurrentNode = rootNode;
+ mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+ mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
+ }
+
+ @Action
+ int check(final int codePoint) {
+ final int action;
+ MetadataRepo.Node node = mCurrentNode.get(codePoint);
+ switch (mState) {
+ case STATE_WALKING:
+ if (node != null) {
+ mCurrentNode = node;
+ mCurrentDepth += 1;
+ action = ACTION_ADVANCE_END;
+ } else {
+ if (isTextStyle(codePoint)) {
+ action = reset();
+ } else if (isEmojiStyle(codePoint)) {
+ action = ACTION_ADVANCE_END;
+ } else if (mCurrentNode.getData() != null) {
+ if (mCurrentDepth == 1) {
+ if (shouldUseEmojiPresentationStyleForSingleCodepoint()) {
+ mFlushNode = mCurrentNode;
+ action = ACTION_FLUSH;
+ reset();
+ } else {
+ action = reset();
+ }
+ } else {
+ mFlushNode = mCurrentNode;
+ action = ACTION_FLUSH;
+ reset();
+ }
+ } else {
+ action = reset();
+ }
+ }
+ break;
+ case STATE_DEFAULT:
+ default:
+ if (node == null) {
+ action = reset();
+ } else {
+ mState = STATE_WALKING;
+ mCurrentNode = node;
+ mCurrentDepth = 1;
+ action = ACTION_ADVANCE_END;
+ }
+ break;
+ }
+
+ mLastCodepoint = codePoint;
+ return action;
+ }
+
+ @Action
+ private int reset() {
+ mState = STATE_DEFAULT;
+ mCurrentNode = mRootNode;
+ mCurrentDepth = 0;
+ return ACTION_ADVANCE_BOTH;
+ }
+
+ /**
+ * @return the metadata node when ACTION_FLUSH is returned
+ */
+ TypefaceEmojiRasterizer getFlushMetadata() {
+ return mFlushNode.getData();
+ }
+
+ /**
+ * @return current pointer to the metadata node in the trie
+ */
+ TypefaceEmojiRasterizer getCurrentMetadata() {
+ return mCurrentNode.getData();
+ }
+
+ /**
+ * Need for the case where input is consumed, but action_flush was not called. For example
+ * when the char sequence has single codepoint character which is a default emoji. State
+ * machine will wait for the next.
+ *
+ * @return whether the current state requires an emoji to be added
+ */
+ boolean isInFlushableState() {
+ return mState == STATE_WALKING && mCurrentNode.getData() != null
+ && (mCurrentDepth > 1 || shouldUseEmojiPresentationStyleForSingleCodepoint());
+ }
+
+ private boolean shouldUseEmojiPresentationStyleForSingleCodepoint() {
+ if (mCurrentNode.getData().isDefaultEmoji()) {
+ // The codepoint is emoji style by default.
+ return true;
+ }
+ if (isEmojiStyle(mLastCodepoint)) {
+ // The codepoint was followed by the emoji style variation selector.
+ return true;
+ }
+ if (mUseEmojiAsDefaultStyle) {
+ // Emoji presentation style for text style default emojis is enabled. We have
+ // to check that the current codepoint is not an exception.
+ if (mEmojiAsDefaultStyleExceptions == null) {
+ return true;
+ }
+ final int codepoint = mCurrentNode.getData().getCodepointAt(0);
+ final int index = Arrays.binarySearch(mEmojiAsDefaultStyleExceptions, codepoint);
+ if (index < 0) {
+ // Index is negative, so the codepoint was not found in the array of exceptions.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param codePoint CodePoint to check
+ *
+ * @return {@code true} if the codepoint is a emoji style standardized variation selector
+ */
+ private static boolean isEmojiStyle(int codePoint) {
+ return codePoint == 0xFE0F;
+ }
+
+ /**
+ * @param codePoint CodePoint to check
+ *
+ * @return {@code true} if the codepoint is a text style standardized variation selector
+ */
+ private static boolean isTextStyle(int codePoint) {
+ return codePoint == 0xFE0E;
+ }
+ }
+
+ /**
+ * Copy of BaseInputConnection findIndexBackward and findIndexForward functions.
+ */
+ @RequiresApi(19)
+ private static final class CodepointIndexFinder {
+ private static final int INVALID_INDEX = -1;
+
+ private CodepointIndexFinder() {}
+
+ /**
+ * Find start index of the character in {@code cs} that is {@code numCodePoints} behind
+ * starting from {@code from}.
+ *
+ * @param cs CharSequence to work on
+ * @param from the index to start going backwards
+ * @param numCodePoints the number of codepoints
+ *
+ * @return start index of the character
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static int findIndexBackward(final CharSequence cs, final int from,
+ final int numCodePoints) {
+ int currentIndex = from;
+ boolean waitingHighSurrogate = false;
+ final int length = cs.length();
+ if (currentIndex < 0 || length < currentIndex) {
+ return INVALID_INDEX; // The starting point is out of range.
+ }
+ if (numCodePoints < 0) {
+ return INVALID_INDEX; // Basically this should not happen.
+ }
+ int remainingCodePoints = numCodePoints;
+ while (true) {
+ if (remainingCodePoints == 0) {
+ return currentIndex; // Reached to the requested length in code points.
+ }
+
+ --currentIndex;
+ if (currentIndex < 0) {
+ if (waitingHighSurrogate) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ return 0; // Reached to the R of the text w/o any invalid surrogate
+ // pair.
+ }
+ final char c = cs.charAt(currentIndex);
+ if (waitingHighSurrogate) {
+ if (!Character.isHighSurrogate(c)) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ waitingHighSurrogate = false;
+ --remainingCodePoints;
+ continue;
+ }
+ if (!Character.isSurrogate(c)) {
+ --remainingCodePoints;
+ continue;
+ }
+ if (Character.isHighSurrogate(c)) {
+ return INVALID_INDEX; // A invalid surrogate pair is found.
+ }
+ waitingHighSurrogate = true;
+ }
+ }
+
+ /**
+ * Find start index of the character in {@code cs} that is {@code numCodePoints} ahead
+ * starting from {@code from}.
+ *
+ * @param cs CharSequence to work on
+ * @param from the index to start going forward
+ * @param numCodePoints the number of codepoints
+ *
+ * @return start index of the character
+ */
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static int findIndexForward(final CharSequence cs, final int from,
+ final int numCodePoints) {
+ int currentIndex = from;
+ boolean waitingLowSurrogate = false;
+ final int length = cs.length();
+ if (currentIndex < 0 || length < currentIndex) {
+ return INVALID_INDEX; // The starting point is out of range.
+ }
+ if (numCodePoints < 0) {
+ return INVALID_INDEX; // Basically this should not happen.
+ }
+ int remainingCodePoints = numCodePoints;
+
+ while (true) {
+ if (remainingCodePoints == 0) {
+ return currentIndex; // Reached to the requested length in code points.
+ }
+
+ if (currentIndex >= length) {
+ if (waitingLowSurrogate) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ return length; // Reached to the end of the text w/o any invalid surrogate
+ // pair.
+ }
+ final char c = cs.charAt(currentIndex);
+ if (waitingLowSurrogate) {
+ if (!Character.isLowSurrogate(c)) {
+ return INVALID_INDEX; // An invalid surrogate pair is found.
+ }
+ --remainingCodePoints;
+ waitingLowSurrogate = false;
+ ++currentIndex;
+ continue;
+ }
+ if (!Character.isSurrogate(c)) {
+ --remainingCodePoints;
+ ++currentIndex;
+ continue;
+ }
+ if (Character.isLowSurrogate(c)) {
+ return INVALID_INDEX; // A invalid surrogate pair is found.
+ }
+ waitingLowSurrogate = true;
+ ++currentIndex;
+ }
+ }
+ }
+
+ private static class EmojiProcessAddSpanCallback
+ implements EmojiProcessCallback {
+ @Nullable
+ public UnprecomputeTextOnModificationSpannable spannable;
+ private final EmojiCompat.SpanFactory mSpanFactory;
+
+ EmojiProcessAddSpanCallback(@Nullable UnprecomputeTextOnModificationSpannable spannable,
+ EmojiCompat.SpanFactory spanFactory) {
+ this.spannable = spannable;
+ this.mSpanFactory = spanFactory;
+ }
+
+ @Override
+ public boolean handleEmoji(@NonNull CharSequence charSequence, int start, int end,
+ TypefaceEmojiRasterizer metadata) {
+ if (metadata.isPreferredSystemRender()) {
+ return true;
+ }
+ if (spannable == null) {
+ spannable = new UnprecomputeTextOnModificationSpannable(
+ charSequence instanceof Spannable
+ ? (Spannable) charSequence
+ : new SpannableString(charSequence));
+ }
+ final EmojiSpan span = mSpanFactory.createSpan(metadata);
+ spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return true;
+ }
+
+ @Override
+ public UnprecomputeTextOnModificationSpannable getResult() {
+ return spannable;
+ }
+ }
+
+ private static class EmojiProcessLookupCallback
+ implements EmojiProcessCallback {
+ private final int mOffset;
+
+ public int start = -1;
+
+ public int end = -1;
+
+ EmojiProcessLookupCallback(int offset) {
+ this.mOffset = offset;
+ }
+
+ @Override
+ public boolean handleEmoji(@NonNull CharSequence charSequence, int start, int end,
+ TypefaceEmojiRasterizer metadata) {
+ if (start <= mOffset && mOffset < end) {
+ this.start = start;
+ this.end = end;
+ return false;
+ }
+
+ return end <= mOffset;
+ }
+
+ @Override
+ public EmojiProcessLookupCallback getResult() {
+ return this;
+ }
+ }
+
+ /**
+ * Mark exclusinos for any emoji matched by this callback
+ */
+ private static class MarkExclusionCallback
+ implements EmojiProcessCallback {
+
+ private final String mExclusion;
+
+ MarkExclusionCallback(String emoji) {
+ mExclusion = emoji;
+ }
+
+ @Override
+ public boolean handleEmoji(@NonNull CharSequence charSequence, int start, int end,
+ TypefaceEmojiRasterizer metadata) {
+ if (TextUtils.equals(charSequence.subSequence(start, end), mExclusion)) {
+ metadata.setExclusion(true);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public MarkExclusionCallback getResult() {
+ return this;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/EmojiSpan.java b/app/src/main/java/androidx/emoji2/text/EmojiSpan.java
new file mode 100644
index 0000000000..7c334c6057
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.TESTS;
+
+import android.annotation.SuppressLint;
+import android.graphics.Paint;
+import android.text.style.ReplacementSpan;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+/**
+ * Base span class for the emoji replacement. When an emoji is found and needs to be replaced in a
+ * CharSequence, an instance of this class is added to the CharSequence.
+ */
+@RequiresApi(19)
+public abstract class EmojiSpan extends ReplacementSpan {
+
+ /**
+ * Temporary object to calculate the size of the span.
+ */
+ private final Paint.FontMetricsInt mTmpFontMetrics = new Paint.FontMetricsInt();
+
+ /**
+ * Information about emoji. This is not parcelled since we do not want multiple objects
+ * representing same emoji to be in memory. When unparcelled, EmojiSpan tries to set it back
+ * using the singleton EmojiCompat instance.
+ */
+ @NonNull
+ private final TypefaceEmojiRasterizer mRasterizer;
+
+ /**
+ * Cached width of the span. Width is calculated according to the font metrics.
+ */
+ private short mWidth = -1;
+
+ /**
+ * Cached height of the span. Height is calculated according to the font metrics.
+ */
+ private short mHeight = -1;
+
+ /**
+ * Cached ratio of current font height to emoji image height.
+ */
+ private float mRatio = 1.0f;
+
+ /**
+ * Default constructor.
+ *
+ * @param rasterizer information about the emoji, cannot be {@code null}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ EmojiSpan(@NonNull final TypefaceEmojiRasterizer rasterizer) {
+ Preconditions.checkNotNull(rasterizer, "rasterizer cannot be null");
+ mRasterizer = rasterizer;
+ }
+
+ @Override
+ public int getSize(@NonNull final Paint paint,
+ @SuppressLint("UnknownNullness") @SuppressWarnings("MissingNullability")
+ final CharSequence text,
+ final int start,
+ final int end,
+ @Nullable final Paint.FontMetricsInt fm) {
+ paint.getFontMetricsInt(mTmpFontMetrics);
+ final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
+
+ mRatio = fontHeight * 1.0f / mRasterizer.getHeight();
+ mHeight = (short) (mRasterizer.getHeight() * mRatio);
+ mWidth = (short) (mRasterizer.getWidth() * mRatio);
+
+ if (fm != null) {
+ fm.ascent = mTmpFontMetrics.ascent;
+ fm.descent = mTmpFontMetrics.descent;
+ fm.top = mTmpFontMetrics.top;
+ fm.bottom = mTmpFontMetrics.bottom;
+ }
+
+ return mWidth;
+ }
+
+ /**
+ * Get the rasterizer that draws this emoji.
+ *
+ * @return rasterizer to draw emoji
+ */
+ @NonNull
+ public final TypefaceEmojiRasterizer getTypefaceRasterizer() {
+ return mRasterizer;
+ }
+
+ /**
+ * @return width of the span
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ final int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * @return height of the span
+ *
+ * @hide
+ */
+ @RestrictTo(TESTS)
+ public final int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ final float getRatio() {
+ return mRatio;
+ }
+
+ /**
+ * @return unique id for the emoji that this EmojiSpan is used for
+ *
+ * @hide
+ */
+ @RestrictTo(TESTS)
+ public final int getId() {
+ return getTypefaceRasterizer().getId();
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java b/app/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
new file mode 100644
index 0000000000..143c06f1b0
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.core.graphics.TypefaceCompatUtil;
+import androidx.core.os.TraceCompat;
+import androidx.core.provider.FontRequest;
+import androidx.core.provider.FontsContractCompat;
+import androidx.core.provider.FontsContractCompat.FontFamilyResult;
+import androidx.core.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * {@link EmojiCompat.Config} implementation that asynchronously fetches the required font and the
+ * metadata using a {@link FontRequest}. FontRequest should be constructed to fetch an EmojiCompat
+ * compatible emoji font.
+ *
+ */
+public class FontRequestEmojiCompatConfig extends EmojiCompat.Config {
+
+ /**
+ * Retry policy used when the font provider is not ready to give the font file.
+ *
+ * To control the thread the retries are handled on, see
+ * {@link FontRequestEmojiCompatConfig#setLoadingExecutor}.
+ */
+ public abstract static class RetryPolicy {
+ /**
+ * Called each time the metadata loading fails.
+ *
+ * This is primarily due to a pending download of the font.
+ * If a value larger than zero is returned, metadata loader will retry after the given
+ * milliseconds.
+ *
+ * If {@code zero} is returned, metadata loader will retry immediately.
+ *
+ * If a value less than 0 is returned, the metadata loader will stop retrying and
+ * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
+ *
+ * Note that the retry may happen earlier than you specified if the font provider notifies
+ * that the download is completed.
+ *
+ * @return long milliseconds to wait until next retry
+ */
+ public abstract long getRetryDelay();
+ }
+
+ /**
+ * A retry policy implementation that doubles the amount of time in between retries.
+ *
+ * If downloading hasn't finish within given amount of time, this policy give up and the
+ * EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
+ */
+ public static class ExponentialBackoffRetryPolicy extends RetryPolicy {
+ private final long mTotalMs;
+ private long mRetryOrigin;
+
+ /**
+ * @param totalMs A total amount of time to wait in milliseconds.
+ */
+ public ExponentialBackoffRetryPolicy(long totalMs) {
+ mTotalMs = totalMs;
+ }
+
+ @Override
+ public long getRetryDelay() {
+ if (mRetryOrigin == 0) {
+ mRetryOrigin = SystemClock.uptimeMillis();
+ // Since download may be completed after getting query result and before registering
+ // observer, requesting later at the same time.
+ return 0;
+ } else {
+ // Retry periodically since we can't trust notify change event. Some font provider
+ // may not notify us.
+ final long elapsedMillis = SystemClock.uptimeMillis() - mRetryOrigin;
+ if (elapsedMillis > mTotalMs) {
+ return -1; // Give up since download hasn't finished in 10 min.
+ }
+ // Wait until the same amount of the time from the first scheduled time, but adjust
+ // the minimum request interval is 1 sec and never exceeds 10 min in total.
+ return Math.min(Math.max(elapsedMillis, 1000), mTotalMs - elapsedMillis);
+ }
+ }
+ }
+
+ /**
+ * @param context Context instance, cannot be {@code null}
+ * @param request {@link FontRequest} to fetch the font asynchronously, cannot be {@code null}
+ */
+ public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request) {
+ super(new FontRequestMetadataLoader(context, request, DEFAULT_FONTS_CONTRACT));
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
+ @NonNull FontProviderHelper fontProviderHelper) {
+ super(new FontRequestMetadataLoader(context, request, fontProviderHelper));
+ }
+
+ /**
+ * Sets the custom executor to be used for initialization.
+ *
+ * Since font loading is too slow for the main thread, the metadata loader will fetch the fonts
+ * on a background thread. By default, FontRequestEmojiCompatConfig will create its own
+ * single threaded Executor, which causes a thread to be created.
+ *
+ * You can pass your own executor to control which thread the font is loaded on, and avoid an
+ * extra thread creation.
+ *
+ * @param executor background executor for performing font load
+ */
+ @NonNull
+ public FontRequestEmojiCompatConfig setLoadingExecutor(@NonNull Executor executor) {
+ ((FontRequestMetadataLoader) getMetadataRepoLoader()).setExecutor(executor);
+ return this;
+ }
+
+ /**
+ * Please us {@link #setLoadingExecutor(Executor)} instead to set background loading thread.
+ *
+ * This was deprecated in emoji2 1.0.0-alpha04.
+ *
+ * If migrating from androidx.emoji please prefer to use an existing background executor for
+ * setLoadingExecutor.
+ *
+ * Note: This method will no longer have any effect if passed null, which is a breaking
+ * change from androidx.emoji.
+ *
+ * @deprecated please call setLoadingExecutor instead
+ *
+ * @param handler background thread handler to wrap in an Executor, if null this method will
+ * do nothing
+ */
+ @Deprecated
+ @NonNull
+ @SuppressWarnings("deprecation")
+ public FontRequestEmojiCompatConfig setHandler(@Nullable Handler handler) {
+ if (handler == null) {
+ // this is a breaking behavior change from androidx.emoji, we no longer support
+ // clearing executors
+ return this;
+ }
+ setLoadingExecutor(ConcurrencyHelpers.convertHandlerToExecutor(handler));
+ return this;
+ }
+
+ /**
+ * Sets the retry policy.
+ *
+ * {@see RetryPolicy}
+ * @param policy The policy to be used when the font provider is not ready to give the font
+ * file. Can be {@code null}. In case of {@code null}, the metadata loader never
+ * retries.
+ */
+ @NonNull
+ public FontRequestEmojiCompatConfig setRetryPolicy(@Nullable RetryPolicy policy) {
+ ((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
+ return this;
+ }
+
+ /**
+ * MetadataRepoLoader implementation that uses FontsContractCompat and TypefaceCompat to load a
+ * given FontRequest.
+ */
+ private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
+ private static final String S_TRACE_BUILD_TYPEFACE =
+ "EmojiCompat.FontRequestEmojiCompatConfig.buildTypeface";
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final FontRequest mRequest;
+ @NonNull
+ private final FontProviderHelper mFontProviderHelper;
+ @NonNull
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Handler mMainHandler;
+ @GuardedBy("mLock")
+ @Nullable
+ private Executor mExecutor;
+ @GuardedBy("mLock")
+ @Nullable
+ private ThreadPoolExecutor mMyThreadPoolExecutor;
+ @GuardedBy("mLock")
+ @Nullable
+ private RetryPolicy mRetryPolicy;
+
+ @GuardedBy("mLock")
+ @Nullable
+ EmojiCompat.MetadataRepoLoaderCallback mCallback;
+ @GuardedBy("mLock")
+ @Nullable
+ private ContentObserver mObserver;
+ @GuardedBy("mLock")
+ @Nullable
+ private Runnable mMainHandlerLoadCallback;
+
+ FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
+ @NonNull FontProviderHelper fontProviderHelper) {
+ Preconditions.checkNotNull(context, "Context cannot be null");
+ Preconditions.checkNotNull(request, "FontRequest cannot be null");
+ mContext = context.getApplicationContext();
+ mRequest = request;
+ mFontProviderHelper = fontProviderHelper;
+ }
+
+ public void setExecutor(@NonNull Executor executor) {
+ synchronized (mLock) {
+ mExecutor = executor;
+ }
+ }
+
+ public void setRetryPolicy(@Nullable RetryPolicy policy) {
+ synchronized (mLock) {
+ mRetryPolicy = policy;
+ }
+ }
+
+ @Override
+ @RequiresApi(19)
+ public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ Preconditions.checkNotNull(loaderCallback, "LoaderCallback cannot be null");
+ synchronized (mLock) {
+ mCallback = loaderCallback;
+ }
+ loadInternal();
+ }
+
+ @RequiresApi(19)
+ void loadInternal() {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ // do nothing; loading is already complete
+ return;
+ }
+ if (mExecutor == null) {
+ mMyThreadPoolExecutor = ConcurrencyHelpers.createBackgroundPriorityExecutor(
+ "emojiCompat");
+ mExecutor = mMyThreadPoolExecutor;
+ }
+ mExecutor.execute(this::createMetadata);
+ }
+ }
+
+ @WorkerThread
+ private FontsContractCompat.FontInfo retrieveFontInfo() {
+ final FontsContractCompat.FontFamilyResult result;
+ try {
+ result = mFontProviderHelper.fetchFonts(mContext, mRequest);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException("provider not found", e);
+ }
+ if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
+ throw new RuntimeException("fetchFonts failed (" + result.getStatusCode() + ")");
+ }
+ final FontsContractCompat.FontInfo[] fonts = result.getFonts();
+ if (fonts == null || fonts.length == 0) {
+ throw new RuntimeException("fetchFonts failed (empty result)");
+ }
+ return fonts[0]; // Assuming the GMS Core provides only one font file.
+ }
+
+ @RequiresApi(19)
+ @WorkerThread
+ private void scheduleRetry(Uri uri, long waitMs) {
+ synchronized (mLock) {
+ Handler handler = mMainHandler;
+ if (handler == null) {
+ handler = ConcurrencyHelpers.mainHandlerAsync();
+ mMainHandler = handler;
+ }
+ if (mObserver == null) {
+ mObserver = new ContentObserver(handler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ loadInternal();
+ }
+ };
+ mFontProviderHelper.registerObserver(mContext, uri, mObserver);
+ }
+ if (mMainHandlerLoadCallback == null) {
+ mMainHandlerLoadCallback = this::loadInternal;
+ }
+ handler.postDelayed(mMainHandlerLoadCallback, waitMs);
+ }
+ }
+
+ // Must be called on the mHandler.
+ private void cleanUp() {
+ synchronized (mLock) {
+ mCallback = null;
+ if (mObserver != null) {
+ mFontProviderHelper.unregisterObserver(mContext, mObserver);
+ mObserver = null;
+ }
+ if (mMainHandler != null) {
+ mMainHandler.removeCallbacks(mMainHandlerLoadCallback);
+ }
+ mMainHandler = null;
+ if (mMyThreadPoolExecutor != null) {
+ // if we made the executor, shut it down
+ mMyThreadPoolExecutor.shutdown();
+ }
+ mExecutor = null;
+ mMyThreadPoolExecutor = null;
+ }
+ }
+
+ // Must be called on the mHandler.
+ @RequiresApi(19)
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ @WorkerThread
+ void createMetadata() {
+ synchronized (mLock) {
+ if (mCallback == null) {
+ return; // Already handled or cancelled. Do nothing.
+ }
+ }
+ try {
+ final FontsContractCompat.FontInfo font = retrieveFontInfo();
+
+ final int resultCode = font.getResultCode();
+ if (resultCode == FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE) {
+ // The font provider is now downloading. Ask RetryPolicy for when to retry next.
+ synchronized (mLock) {
+ if (mRetryPolicy != null) {
+ final long delayMs = mRetryPolicy.getRetryDelay();
+ if (delayMs >= 0) {
+ scheduleRetry(font.getUri(), delayMs);
+ return;
+ }
+ }
+ }
+ }
+
+ if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
+ throw new RuntimeException("fetchFonts result is not OK. (" + resultCode + ")");
+ }
+
+ final MetadataRepo metadataRepo;
+ try {
+ TraceCompat.beginSection(S_TRACE_BUILD_TYPEFACE);
+ // TODO: Good to add new API to create Typeface from FD not to open FD twice.
+ final Typeface typeface = mFontProviderHelper.buildTypeface(mContext, font);
+ final ByteBuffer buffer = TypefaceCompatUtil.mmap(mContext, null,
+ font.getUri());
+ if (buffer == null || typeface == null) {
+ throw new RuntimeException("Unable to open file.");
+ }
+ metadataRepo = MetadataRepo.create(typeface, buffer);
+ } finally {
+ TraceCompat.endSection();
+ }
+ synchronized (mLock) {
+ if (mCallback != null) {
+ mCallback.onLoaded(metadataRepo);
+ }
+ }
+ cleanUp();
+ } catch (Throwable t) {
+ synchronized (mLock) {
+ if (mCallback != null) {
+ mCallback.onFailed(t);
+ }
+ }
+ cleanUp();
+ }
+ }
+ }
+
+ /**
+ * Delegate class for mocking FontsContractCompat.fetchFonts.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static class FontProviderHelper {
+ /** Calls FontsContractCompat.fetchFonts. */
+ @NonNull
+ public FontFamilyResult fetchFonts(@NonNull Context context,
+ @NonNull FontRequest request) throws NameNotFoundException {
+ return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
+ }
+
+ /** Calls FontsContractCompat.buildTypeface. */
+ @Nullable
+ public Typeface buildTypeface(@NonNull Context context,
+ @NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
+ return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
+ new FontsContractCompat.FontInfo[] { font });
+ }
+
+ /** Calls Context.getContentObserver().registerObserver */
+ public void registerObserver(@NonNull Context context, @NonNull Uri uri,
+ @NonNull ContentObserver observer) {
+ context.getContentResolver().registerContentObserver(
+ uri, false /* notifyForDescendants */, observer);
+
+ }
+ /** Calls Context.getContentObserver().unregisterObserver */
+ public void unregisterObserver(@NonNull Context context,
+ @NonNull ContentObserver observer) {
+ context.getContentResolver().unregisterContentObserver(observer);
+ }
+ }
+
+ private static final FontProviderHelper DEFAULT_FONTS_CONTRACT = new FontProviderHelper();
+
+}
diff --git a/app/src/main/java/androidx/emoji2/text/MetadataListReader.java b/app/src/main/java/androidx/emoji2/text/MetadataListReader.java
new file mode 100644
index 0000000000..2ed42e0700
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/MetadataListReader.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import android.content.res.AssetManager;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.flatbuffer.MetadataList;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Reads the emoji metadata from a given InputStream or ByteBuffer.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@AnyThread
+@RequiresApi(19)
+class MetadataListReader {
+
+ /**
+ * Meta tag for emoji metadata. This string is used by the font update script to insert the
+ * emoji meta into the font. This meta table contains the list of all emojis which are stored in
+ * binary format using FlatBuffers. This flat list is later converted by the system into a trie.
+ * {@code int} representation for "Emji"
+ *
+ * @see MetadataRepo
+ */
+ private static final int EMJI_TAG = 'E' << 24 | 'm' << 16 | 'j' << 8 | 'i';
+
+ /**
+ * Deprecated meta tag name. Do not use, kept for compatibility reasons, will be removed soon.
+ */
+ private static final int EMJI_TAG_DEPRECATED = 'e' << 24 | 'm' << 16 | 'j' << 8 | 'i';
+
+ /**
+ * The name of the meta table in the font. int representation for "meta"
+ */
+ private static final int META_TABLE_NAME = 'm' << 24 | 'e' << 16 | 't' << 8 | 'a';
+
+ /**
+ * Construct MetadataList from an input stream. Does not close the given InputStream, therefore
+ * it is caller's responsibility to properly close the stream.
+ *
+ * @param inputStream InputStream to read emoji metadata from
+ */
+ static MetadataList read(InputStream inputStream) throws IOException {
+ final OpenTypeReader openTypeReader = new InputStreamOpenTypeReader(inputStream);
+ final OffsetInfo offsetInfo = findOffsetInfo(openTypeReader);
+ // skip to where metadata is
+ openTypeReader.skip((int) (offsetInfo.getStartOffset() - openTypeReader.getPosition()));
+ // allocate a ByteBuffer and read into it since FlatBuffers can read only from a ByteBuffer
+ final ByteBuffer buffer = ByteBuffer.allocate((int) offsetInfo.getLength());
+ final int numRead = inputStream.read(buffer.array());
+ if (numRead != offsetInfo.getLength()) {
+ throw new IOException("Needed " + offsetInfo.getLength() + " bytes, got " + numRead);
+ }
+
+ return MetadataList.getRootAsMetadataList(buffer);
+ }
+
+ /**
+ * Construct MetadataList from a byte buffer.
+ *
+ * @param byteBuffer ByteBuffer to read emoji metadata from
+ */
+ static MetadataList read(final ByteBuffer byteBuffer) throws IOException {
+ final ByteBuffer newBuffer = byteBuffer.duplicate();
+ final OpenTypeReader reader = new ByteBufferReader(newBuffer);
+ final OffsetInfo offsetInfo = findOffsetInfo(reader);
+ // skip to where metadata is
+ newBuffer.position((int) offsetInfo.getStartOffset());
+ return MetadataList.getRootAsMetadataList(newBuffer);
+ }
+
+ /**
+ * Construct MetadataList from an asset.
+ *
+ * @param assetManager AssetManager instance
+ * @param assetPath asset manager path of the file that the Typeface and metadata will be
+ * created from
+ */
+ static MetadataList read(AssetManager assetManager, String assetPath)
+ throws IOException {
+ try (InputStream inputStream = assetManager.open(assetPath)) {
+ return read(inputStream);
+ }
+ }
+
+ /**
+ * Finds the start offset and length of the emoji metadata in the font.
+ *
+ * @return OffsetInfo which contains start offset and length of the emoji metadata in the font
+ *
+ * @throws IOException
+ */
+ private static OffsetInfo findOffsetInfo(OpenTypeReader reader) throws IOException {
+ // skip sfnt version
+ reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
+ // start of Table Count
+ final int tableCount = reader.readUnsignedShort();
+ if (tableCount > 100) {
+ //something is wrong quit
+ throw new IOException("Cannot read metadata.");
+ }
+ //skip to beginning of tables data
+ reader.skip(OpenTypeReader.UINT16_BYTE_COUNT * 3);
+
+ long metaOffset = -1;
+ for (int i = 0; i < tableCount; i++) {
+ final int tag = reader.readTag();
+ // skip checksum
+ reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
+ final long offset = reader.readUnsignedInt();
+ // skip mLength
+ reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
+ if (META_TABLE_NAME == tag) {
+ metaOffset = offset;
+ break;
+ }
+ }
+
+ if (metaOffset != -1) {
+ // skip to the beginning of meta tables.
+ reader.skip((int) (metaOffset - reader.getPosition()));
+ // skip minorVersion, majorVersion, flags, reserved,
+ reader.skip(
+ OpenTypeReader.UINT16_BYTE_COUNT * 2 + OpenTypeReader.UINT32_BYTE_COUNT * 2);
+ final long mapsCount = reader.readUnsignedInt();
+ for (int i = 0; i < mapsCount; i++) {
+ final int tag = reader.readTag();
+ final long dataOffset = reader.readUnsignedInt();
+ final long dataLength = reader.readUnsignedInt();
+ if (EMJI_TAG == tag || EMJI_TAG_DEPRECATED == tag) {
+ return new OffsetInfo(dataOffset + metaOffset, dataLength);
+ }
+ }
+ }
+
+ throw new IOException("Cannot read metadata.");
+ }
+
+ /**
+ * Start offset and length of the emoji metadata in the font.
+ */
+ private static class OffsetInfo {
+ private final long mStartOffset;
+ private final long mLength;
+
+ OffsetInfo(long startOffset, long length) {
+ mStartOffset = startOffset;
+ mLength = length;
+ }
+
+ long getStartOffset() {
+ return mStartOffset;
+ }
+
+ long getLength() {
+ return mLength;
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static int toUnsignedShort(final short value) {
+ return value & 0xFFFF;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static long toUnsignedInt(final int value) {
+ return value & 0xFFFFFFFFL;
+ }
+
+ private interface OpenTypeReader {
+ int UINT16_BYTE_COUNT = 2;
+ int UINT32_BYTE_COUNT = 4;
+
+ /**
+ * Reads an {@code OpenType uint16}.
+ *
+ * @throws IOException
+ */
+ int readUnsignedShort() throws IOException;
+
+ /**
+ * Reads an {@code OpenType uint32}.
+ *
+ * @throws IOException
+ */
+ long readUnsignedInt() throws IOException;
+
+ /**
+ * Reads an {@code OpenType Tag}.
+ *
+ * @throws IOException
+ */
+ int readTag() throws IOException;
+
+ /**
+ * Skip the given amount of numOfBytes
+ *
+ * @throws IOException
+ */
+ void skip(int numOfBytes) throws IOException;
+
+ /**
+ * @return the position of the reader
+ */
+ long getPosition();
+ }
+
+ /**
+ * Reads {@code OpenType} data from an {@link InputStream}.
+ */
+ private static class InputStreamOpenTypeReader implements OpenTypeReader {
+
+ private final @NonNull byte[] mByteArray;
+ private final @NonNull ByteBuffer mByteBuffer;
+ private final @NonNull InputStream mInputStream;
+ private long mPosition = 0;
+
+ /**
+ * Constructs the reader with the given InputStream. Does not close the InputStream, it is
+ * caller's responsibility to close it.
+ *
+ * @param inputStream InputStream to read from
+ */
+ InputStreamOpenTypeReader(@NonNull final InputStream inputStream) {
+ mInputStream = inputStream;
+ mByteArray = new byte[UINT32_BYTE_COUNT];
+ mByteBuffer = ByteBuffer.wrap(mByteArray);
+ mByteBuffer.order(ByteOrder.BIG_ENDIAN);
+ }
+
+ @Override
+ public int readUnsignedShort() throws IOException {
+ mByteBuffer.position(0);
+ read(UINT16_BYTE_COUNT);
+ return toUnsignedShort(mByteBuffer.getShort());
+ }
+
+ @Override
+ public long readUnsignedInt() throws IOException {
+ mByteBuffer.position(0);
+ read(UINT32_BYTE_COUNT);
+ return toUnsignedInt(mByteBuffer.getInt());
+ }
+
+ @Override
+ public int readTag() throws IOException {
+ mByteBuffer.position(0);
+ read(UINT32_BYTE_COUNT);
+ return mByteBuffer.getInt();
+ }
+
+ @Override
+ public void skip(int numOfBytes) throws IOException {
+ while (numOfBytes > 0) {
+ int skipped = (int) mInputStream.skip(numOfBytes);
+ if (skipped < 1) {
+ throw new IOException("Skip didn't move at least 1 byte forward");
+ }
+ numOfBytes -= skipped;
+ mPosition += skipped;
+ }
+ }
+
+ @Override
+ public long getPosition() {
+ return mPosition;
+ }
+
+ private void read(@IntRange(from = 0, to = UINT32_BYTE_COUNT) final int numOfBytes)
+ throws IOException {
+ if (mInputStream.read(mByteArray, 0, numOfBytes) != numOfBytes) {
+ throw new IOException("read failed");
+ }
+ mPosition += numOfBytes;
+ }
+ }
+
+ /**
+ * Reads OpenType data from a ByteBuffer.
+ */
+ private static class ByteBufferReader implements OpenTypeReader {
+
+ private final @NonNull ByteBuffer mByteBuffer;
+
+ /**
+ * Constructs the reader with the given ByteBuffer.
+ *
+ * @param byteBuffer ByteBuffer to read from
+ */
+ ByteBufferReader(@NonNull final ByteBuffer byteBuffer) {
+ mByteBuffer = byteBuffer;
+ mByteBuffer.order(ByteOrder.BIG_ENDIAN);
+ }
+
+ @Override
+ public int readUnsignedShort() throws IOException {
+ return toUnsignedShort(mByteBuffer.getShort());
+ }
+
+ @Override
+ public long readUnsignedInt() throws IOException {
+ return toUnsignedInt(mByteBuffer.getInt());
+ }
+
+ @Override
+ public int readTag() throws IOException {
+ return mByteBuffer.getInt();
+ }
+
+ @Override
+ public void skip(final int numOfBytes) throws IOException {
+ mByteBuffer.position(mByteBuffer.position() + numOfBytes);
+ }
+
+ @Override
+ public long getPosition() {
+ return mByteBuffer.position();
+ }
+ }
+
+ private MetadataListReader() {
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/MetadataRepo.java b/app/src/main/java/androidx/emoji2/text/MetadataRepo.java
new file mode 100644
index 0000000000..f08ae06653
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import android.content.res.AssetManager;
+import android.graphics.Typeface;
+import android.util.SparseArray;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.os.TraceCompat;
+import androidx.core.util.Preconditions;
+import androidx.emoji2.text.flatbuffer.MetadataList;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+/**
+ * Class to hold the emoji metadata required to process and draw emojis.
+ */
+@AnyThread
+@RequiresApi(19)
+public final class MetadataRepo {
+ /**
+ * The default children size of the root node.
+ */
+ private static final int DEFAULT_ROOT_SIZE = 1024;
+ private static final String S_TRACE_CREATE_REPO = "EmojiCompat.MetadataRepo.create";
+
+ /**
+ * MetadataList that contains the emoji metadata.
+ */
+ private final @NonNull MetadataList mMetadataList;
+
+ /**
+ * char presentation of all TypefaceEmojiRasterizer's in a single array. All emojis we have are
+ * mapped to Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2
+ * chars.
+ */
+ private final @NonNull char[] mEmojiCharArray;
+
+ /**
+ * Empty root node of the trie.
+ */
+ private final @NonNull Node mRootNode;
+
+ /**
+ * Typeface to be used to render emojis.
+ */
+ private final @NonNull Typeface mTypeface;
+
+ /**
+ * Private constructor that is called by one of {@code create} methods.
+ *
+ * @param typeface Typeface to be used to render emojis
+ * @param metadataList MetadataList that contains the emoji metadata
+ */
+ private MetadataRepo(@NonNull final Typeface typeface,
+ @NonNull final MetadataList metadataList) {
+ mTypeface = typeface;
+ mMetadataList = metadataList;
+ mRootNode = new Node(DEFAULT_ROOT_SIZE);
+ mEmojiCharArray = new char[mMetadataList.listLength() * 2];
+ constructIndex(mMetadataList);
+ }
+
+ /**
+ * Construct MetadataRepo with empty metadata.
+ *
+ * This should only be used from tests.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.TESTS)
+ public static MetadataRepo create(@NonNull final Typeface typeface) {
+ try {
+ TraceCompat.beginSection(S_TRACE_CREATE_REPO);
+ return new MetadataRepo(typeface, new MetadataList());
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+
+ /**
+ * Construct MetadataRepo from an input stream. The library does not close the given
+ * InputStream, therefore it is caller's responsibility to properly close the stream.
+ *
+ * @param typeface Typeface to be used to render emojis
+ * @param inputStream InputStream to read emoji metadata from
+ */
+ @NonNull
+ public static MetadataRepo create(@NonNull final Typeface typeface,
+ @NonNull final InputStream inputStream) throws IOException {
+ try {
+ TraceCompat.beginSection(S_TRACE_CREATE_REPO);
+ return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+
+ /**
+ * Construct MetadataRepo from a byte buffer. The position of the ByteBuffer will change, it is
+ * caller's responsibility to reposition the buffer if required.
+ *
+ * @param typeface Typeface to be used to render emojis
+ * @param byteBuffer ByteBuffer to read emoji metadata from
+ */
+ @NonNull
+ public static MetadataRepo create(@NonNull final Typeface typeface,
+ @NonNull final ByteBuffer byteBuffer) throws IOException {
+ try {
+ TraceCompat.beginSection(S_TRACE_CREATE_REPO);
+ return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+
+ /**
+ * Construct MetadataRepo from an asset.
+ *
+ * @param assetManager AssetManager instance
+ * @param assetPath asset manager path of the file that the Typeface and metadata will be
+ * created from
+ */
+ @NonNull
+ public static MetadataRepo create(@NonNull final AssetManager assetManager,
+ @NonNull final String assetPath) throws IOException {
+ try {
+ TraceCompat.beginSection(S_TRACE_CREATE_REPO);
+ final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
+ return new MetadataRepo(typeface,
+ MetadataListReader.read(assetManager, assetPath));
+ } finally {
+ TraceCompat.endSection();
+ }
+ }
+
+ /**
+ * Read emoji metadata list and construct the trie.
+ */
+ private void constructIndex(final MetadataList metadataList) {
+ int length = metadataList.listLength();
+ for (int i = 0; i < length; i++) {
+ final TypefaceEmojiRasterizer metadata = new TypefaceEmojiRasterizer(this, i);
+ //since all emojis are mapped to a single codepoint in Private Use Area A they are 2
+ //chars wide
+ //noinspection ResultOfMethodCallIgnored
+ Character.toChars(metadata.getId(), mEmojiCharArray, i * 2);
+ put(metadata);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ Typeface getTypeface() {
+ return mTypeface;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ int getMetadataVersion() {
+ return mMetadataList.version();
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ Node getRootNode() {
+ return mRootNode;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public char[] getEmojiCharArray() {
+ return mEmojiCharArray;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public MetadataList getMetadataList() {
+ return mMetadataList;
+ }
+
+ /**
+ * Add a TypefaceEmojiRasterizer to the index.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @VisibleForTesting
+ void put(@NonNull final TypefaceEmojiRasterizer data) {
+ Preconditions.checkNotNull(data, "emoji metadata cannot be null");
+ Preconditions.checkArgument(data.getCodepointsLength() > 0,
+ "invalid metadata codepoint length");
+
+ mRootNode.put(data, 0, data.getCodepointsLength() - 1);
+ }
+
+ /**
+ * Trie node that holds mapping from emoji codepoint(s) to TypefaceEmojiRasterizer.
+ *
+ * A single codepoint emoji is represented by a child of the root node.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static class Node {
+ private final SparseArray mChildren;
+ private TypefaceEmojiRasterizer mData;
+
+ private Node() {
+ this(1);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ Node(final int defaultChildrenSize) {
+ mChildren = new SparseArray<>(defaultChildrenSize);
+ }
+
+ Node get(final int key) {
+ return mChildren == null ? null : mChildren.get(key);
+ }
+
+ final TypefaceEmojiRasterizer getData() {
+ return mData;
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ void put(@NonNull final TypefaceEmojiRasterizer data, final int start, final int end) {
+ Node node = get(data.getCodepointAt(start));
+ if (node == null) {
+ node = new Node();
+ mChildren.put(data.getCodepointAt(start), node);
+ }
+
+ if (end > start) {
+ node.put(data, start + 1, end);
+ } else {
+ node.mData = data;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/SpannableBuilder.java b/app/src/main/java/androidx/emoji2/text/SpannableBuilder.java
new file mode 100644
index 0000000000..89116c7d3d
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/SpannableBuilder.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.text.Editable;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextWatcher;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance
+ * of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject
+ * (WatcherWrapper) that implements the same interfaces.
+ *
+ * During a span change event WatcherWrapper’s functions are fired, it checks if the span is an
+ * EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs
+ * ChangeWatcher only once at the end of the edit. Important point is, the block operation is
+ * applied only for EmojiSpans. Therefore any other span change operation works the same way as in
+ * the framework.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class SpannableBuilder extends SpannableStringBuilder {
+ /**
+ * DynamicLayout$ChangeWatcher class.
+ */
+ private final @NonNull Class> mWatcherClass;
+
+ /**
+ * All WatcherWrappers.
+ */
+ private final @NonNull List mWatchers = new ArrayList<>();
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ SpannableBuilder(@NonNull Class> watcherClass) {
+ Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
+ mWatcherClass = watcherClass;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ SpannableBuilder(@NonNull Class> watcherClass, @NonNull CharSequence text) {
+ super(text);
+ Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
+ mWatcherClass = watcherClass;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ SpannableBuilder(@NonNull Class> watcherClass, @NonNull CharSequence text, int start,
+ int end) {
+ super(text, start, end);
+ Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
+ mWatcherClass = watcherClass;
+ }
+
+ /**
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static SpannableBuilder create(@NonNull Class> clazz, @NonNull CharSequence text) {
+ return new SpannableBuilder(clazz, text);
+ }
+
+ /**
+ * Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher.
+ *
+ * @param object mObject to be checked
+ *
+ * @return true if mObject is instance of the DynamicLayout$ChangeWatcher.
+ */
+ private boolean isWatcher(@Nullable Object object) {
+ return object != null && isWatcher(object.getClass());
+ }
+
+ /**
+ * Checks whether the class is DynamicLayout$ChangeWatcher.
+ *
+ * @param clazz class to be checked
+ *
+ * @return true if class is DynamicLayout$ChangeWatcher.
+ */
+ private boolean isWatcher(@NonNull Class> clazz) {
+ return mWatcherClass == clazz;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return new SpannableBuilder(mWatcherClass, this, start, end);
+ }
+
+ /**
+ * If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in
+ * another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set
+ * this new mObject as the span.
+ */
+ @Override
+ public void setSpan(@Nullable Object what, int start, int end, int flags) {
+ if (isWatcher(what)) {
+ final WatcherWrapper span = new WatcherWrapper(what);
+ mWatchers.add(span);
+ what = span;
+ }
+
+ super.setSpan(what, start, end, flags);
+ }
+
+ /**
+ * If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
+ * correct Object that the client has set.
+ */
+ @SuppressLint("UnknownNullness")
+ @SuppressWarnings("unchecked")
+ @Override
+ public T[] getSpans(int queryStart, int queryEnd, @NonNull Class kind) {
+ if (isWatcher(kind)) {
+ final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
+ WatcherWrapper.class);
+ final T[] result = (T[]) Array.newInstance(kind, spans.length);
+ for (int i = 0; i < spans.length; i++) {
+ result[i] = (T) spans[i].mObject;
+ }
+ return result;
+ }
+ return super.getSpans(queryStart, queryEnd, kind);
+ }
+
+ /**
+ * If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper
+ * instead.
+ */
+ @Override
+ public void removeSpan(@Nullable Object what) {
+ final WatcherWrapper watcher;
+ if (isWatcher(what)) {
+ watcher = getWatcherFor(what);
+ if (watcher != null) {
+ what = watcher;
+ }
+ } else {
+ watcher = null;
+ }
+
+ super.removeSpan(what);
+
+ if (watcher != null) {
+ mWatchers.remove(watcher);
+ }
+ }
+
+ /**
+ * Return the correct start for the DynamicLayout$ChangeWatcher span.
+ */
+ @Override
+ public int getSpanStart(@Nullable Object tag) {
+ if (isWatcher(tag)) {
+ final WatcherWrapper watcher = getWatcherFor(tag);
+ if (watcher != null) {
+ tag = watcher;
+ }
+ }
+ return super.getSpanStart(tag);
+ }
+
+ /**
+ * Return the correct end for the DynamicLayout$ChangeWatcher span.
+ */
+ @Override
+ public int getSpanEnd(@Nullable Object tag) {
+ if (isWatcher(tag)) {
+ final WatcherWrapper watcher = getWatcherFor(tag);
+ if (watcher != null) {
+ tag = watcher;
+ }
+ }
+ return super.getSpanEnd(tag);
+ }
+
+ /**
+ * Return the correct flags for the DynamicLayout$ChangeWatcher span.
+ */
+ @Override
+ public int getSpanFlags(@Nullable Object tag) {
+ if (isWatcher(tag)) {
+ final WatcherWrapper watcher = getWatcherFor(tag);
+ if (watcher != null) {
+ tag = watcher;
+ }
+ }
+ return super.getSpanFlags(tag);
+ }
+
+ /**
+ * Return the correct transition for the DynamicLayout$ChangeWatcher span.
+ */
+ @Override
+ public int nextSpanTransition(int start, int limit, @Nullable Class type) {
+ if (type == null || isWatcher(type)) {
+ type = WatcherWrapper.class;
+ }
+ return super.nextSpanTransition(start, limit, type);
+ }
+
+ /**
+ * Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher.
+ *
+ * @param object DynamicLayout$ChangeWatcher mObject
+ *
+ * @return WatcherWrapper that wraps the mObject.
+ */
+ private WatcherWrapper getWatcherFor(Object object) {
+ for (int i = 0; i < mWatchers.size(); i++) {
+ WatcherWrapper watcher = mWatchers.get(i);
+ if (watcher.mObject == object) {
+ return watcher;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public void beginBatchEdit() {
+ blockWatchers();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public void endBatchEdit() {
+ unblockwatchers();
+ fireWatchers();
+ }
+
+ /**
+ * Block all watcher wrapper events.
+ */
+ private void blockWatchers() {
+ for (int i = 0; i < mWatchers.size(); i++) {
+ mWatchers.get(i).blockCalls();
+ }
+ }
+
+ /**
+ * Unblock all watcher wrapper events.
+ */
+ private void unblockwatchers() {
+ for (int i = 0; i < mWatchers.size(); i++) {
+ mWatchers.get(i).unblockCalls();
+ }
+ }
+
+ /**
+ * Unblock all watcher wrapper events. Called by editing operations, namely
+ * {@link SpannableStringBuilder#replace(int, int, CharSequence)}.
+ */
+ private void fireWatchers() {
+ for (int i = 0; i < mWatchers.size(); i++) {
+ mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length());
+ }
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
+ blockWatchers();
+ super.replace(start, end, tb);
+ unblockwatchers();
+ return this;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
+ int tbend) {
+ blockWatchers();
+ super.replace(start, end, tb, tbstart, tbend);
+ unblockwatchers();
+ return this;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder insert(int where, CharSequence tb) {
+ super.insert(where, tb);
+ return this;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
+ super.insert(where, tb, start, end);
+ return this;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder delete(int start, int end) {
+ super.delete(start, end);
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
+ super.append(text);
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public SpannableStringBuilder append(char text) {
+ super.append(text);
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
+ int start,
+ int end) {
+ super.append(text, start, end);
+ return this;
+ }
+
+ @SuppressLint("UnknownNullness")
+ @Override
+ public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
+ super.append(text, what, flags);
+ return this;
+ }
+
+ /**
+ * Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout.
+ */
+ private static class WatcherWrapper implements TextWatcher, SpanWatcher {
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final Object mObject;
+ private final AtomicInteger mBlockCalls = new AtomicInteger(0);
+
+ WatcherWrapper(Object object) {
+ this.mObject = object;
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ ((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ ((TextWatcher) mObject).onTextChanged(s, start, before, count);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ ((TextWatcher) mObject).afterTextChanged(s);
+ }
+
+ /**
+ * Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation
+ * (mBlockCalls is set) and the span that is added is an EmojiSpan.
+ */
+ @Override
+ public void onSpanAdded(Spannable text, Object what, int start, int end) {
+ if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
+ return;
+ }
+ ((SpanWatcher) mObject).onSpanAdded(text, what, start, end);
+ }
+
+ /**
+ * Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation
+ * (mBlockCalls is set) and the span that is added is an EmojiSpan.
+ */
+ @Override
+ public void onSpanRemoved(Spannable text, Object what, int start, int end) {
+ if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
+ return;
+ }
+ ((SpanWatcher) mObject).onSpanRemoved(text, what, start, end);
+ }
+
+ /**
+ * Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation
+ * (mBlockCalls is set) and the span that is added is an EmojiSpan.
+ */
+ @Override
+ public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
+ int nend) {
+ if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
+ return;
+ }
+ // workaround for platform bug fixed in Android P
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ // b/67926915 start cannot be determined, fallback to reflow from start instead
+ // of causing an exception.
+
+ // emoji2 bug b/216891011
+ if (ostart > oend) {
+ ostart = 0;
+ }
+ if (nstart > nend) {
+ nstart = 0;
+ }
+ }
+ ((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
+ }
+
+ final void blockCalls() {
+ mBlockCalls.incrementAndGet();
+ }
+
+ final void unblockCalls() {
+ mBlockCalls.decrementAndGet();
+ }
+
+ private boolean isEmojiSpan(final Object span) {
+ return span instanceof EmojiSpan;
+ }
+ }
+
+}
diff --git a/app/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java b/app/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java
new file mode 100644
index 0000000000..f120b3cb23
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/TypefaceEmojiRasterizer.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static androidx.annotation.RestrictTo.Scope.TESTS;
+
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.flatbuffer.MetadataItem;
+import androidx.emoji2.text.flatbuffer.MetadataList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Information about a single emoji.
+ *
+ * To draw this emoji on a canvas using use draw.
+ *
+ * To draw this emoji using custom code, use getCodepointAt and getTypeface to access the
+ * underlying emoji font and look up the glyph.
+ *
+ * @see TypefaceEmojiRasterizer#draw
+ * @see TypefaceEmojiRasterizer#getCodepointAt
+ * @see TypefaceEmojiRasterizer#getTypeface
+ *
+ */
+@AnyThread
+@RequiresApi(19)
+public class TypefaceEmojiRasterizer {
+ /**
+ * Defines whether the system can render the emoji.
+ * @hide
+ */
+ @IntDef({HAS_GLYPH_UNKNOWN, HAS_GLYPH_ABSENT, HAS_GLYPH_EXISTS})
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(LIBRARY_GROUP)
+ public @interface HasGlyph {
+ }
+
+ /**
+ * Not calculated on device yet.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ static final int HAS_GLYPH_UNKNOWN = 0;
+
+ /**
+ * Device cannot render the emoji.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ static final int HAS_GLYPH_ABSENT = 1;
+
+ /**
+ * Device can render the emoji.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ static final int HAS_GLYPH_EXISTS = 2;
+
+ /**
+ * @see #getMetadataItem()
+ */
+ private static final ThreadLocal sMetadataItem = new ThreadLocal<>();
+
+ /**
+ * Index of the TypefaceEmojiRasterizer in {@link MetadataList}.
+ */
+ private final int mIndex;
+
+ /**
+ * MetadataRepo that holds this instance.
+ */
+ @NonNull
+ private final MetadataRepo mMetadataRepo;
+
+ /**
+ * Stores hasGlyph as well as exclusion values
+ *
+ * mCache & 0b0011 is hasGlyph result
+ * mCache & 0b0100 is exclusion value
+ */
+ private volatile int mCache = 0;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ TypefaceEmojiRasterizer(@NonNull final MetadataRepo metadataRepo,
+ @IntRange(from = 0) final int index) {
+ mMetadataRepo = metadataRepo;
+ mIndex = index;
+ }
+
+ /**
+ * Draws the emoji onto a canvas with origin at (x,y), using the specified paint.
+ *
+ * @param canvas Canvas to be drawn
+ * @param x x-coordinate of the origin of the emoji being drawn
+ * @param y y-coordinate of the baseline of the emoji being drawn
+ * @param paint Paint used for the text (e.g. color, size, style)
+ */
+ public void draw(@NonNull final Canvas canvas, final float x, final float y,
+ @NonNull final Paint paint) {
+ final Typeface typeface = mMetadataRepo.getTypeface();
+ final Typeface oldTypeface = paint.getTypeface();
+ paint.setTypeface(typeface);
+ // MetadataRepo.getEmojiCharArray() is a continuous array of chars that is used to store the
+ // chars for emojis. since all emojis are mapped to a single codepoint, and since it is 2
+ // chars wide, we assume that the start index of the current emoji is mIndex * 2, and it is
+ // 2 chars long.
+ final int charArrayStartIndex = mIndex * 2;
+ canvas.drawText(mMetadataRepo.getEmojiCharArray(), charArrayStartIndex, 2, x, y, paint);
+ paint.setTypeface(oldTypeface);
+ }
+
+ /**
+ * @return return typeface to be used to render
+ */
+ @NonNull
+ public Typeface getTypeface() {
+ return mMetadataRepo.getTypeface();
+ }
+
+ /**
+ * @return a ThreadLocal instance of MetadataItem for this TypefaceEmojiRasterizer
+ */
+ private MetadataItem getMetadataItem() {
+ MetadataItem result = sMetadataItem.get();
+ if (result == null) {
+ result = new MetadataItem();
+ sMetadataItem.set(result);
+ }
+ // MetadataList is a wrapper around the metadata ByteBuffer. MetadataItem is a wrapper with
+ // an index (pointer) on this ByteBuffer that represents a single emoji. Both are FlatBuffer
+ // classes that wraps a ByteBuffer and gives access to the information in it. In order not
+ // to create a wrapper class for each TypefaceEmojiRasterizer, we use mIndex as the index
+ // of the MetadataItem in the ByteBuffer. We need to reiniitalize the current thread
+ // local instance by executing the statement below. All the statement does is to set an
+ // int index in MetadataItem. the same instance is used by all TypefaceEmojiRasterizer
+ // classes in the same thread.
+ mMetadataRepo.getMetadataList().list(result, mIndex);
+ return result;
+ }
+
+ /**
+ * Unique id for the emoji, as loaded from the font file.
+ *
+ * @return unique id for the emoji
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public int getId() {
+ return getMetadataItem().id();
+ }
+
+ /**
+ * @return width of the emoji image
+ */
+ public int getWidth() {
+ return getMetadataItem().width();
+ }
+
+ /**
+ * @return height of the emoji image
+ */
+ public int getHeight() {
+ return getMetadataItem().height();
+ }
+
+ /**
+ * @return in which metadata version the emoji was added
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public short getCompatAdded() {
+ return getMetadataItem().compatAdded();
+ }
+
+ /**
+ * @return first SDK that the support for this emoji was added
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public short getSdkAdded() {
+ return getMetadataItem().sdkAdded();
+ }
+
+ /**
+ * Returns the value set by setHasGlyph
+ *
+ * This is intended to be used as a cache on this emoji to avoid repeatedly calling
+ * PaintCompat#hasGlyph on the same codepoint sequence, which is expensive.
+ *
+ * @see TypefaceEmojiRasterizer#setHasGlyph
+ * @return the set value of hasGlyph for this metadata item
+ * @hide
+ */
+ @HasGlyph
+ @SuppressLint("KotlinPropertyAccess")
+ @RestrictTo(LIBRARY)
+ public int getHasGlyph() {
+ return (int) (mCache & 0b0011);
+ }
+
+ /**
+ * Reset any cached values of hasGlyph on this metadata.
+ *
+ * This is only useful for testing, and will make the next display of this emoji slower.
+ *
+ * @hide
+ */
+ @RestrictTo(TESTS)
+ public void resetHasGlyphCache() {
+ boolean willExclude = isPreferredSystemRender();
+ if (willExclude) {
+ mCache = 0b0100;
+ } else {
+ mCache = 0b0000;
+ }
+ }
+
+ /**
+ * Set whether the system can render the emoji.
+ *
+ * @see PaintCompat#hasGlyph
+ * @param hasGlyph {@code true} if system can render the emoji
+ * @hide
+ */
+ @SuppressLint("KotlinPropertyAccess")
+ @RestrictTo(LIBRARY)
+ public void setHasGlyph(boolean hasGlyph) {
+ int newValue = mCache & 0b0100; /* keep the exclusion bit */
+ if (hasGlyph) {
+ newValue |= 0b0010;
+ } else {
+ newValue |= 0b0001;
+ }
+ mCache = newValue;
+ }
+
+ /**
+ * If this emoji is excluded due to CodepointExclusions.getExcludedCodpoints()
+ *
+ * @param exclude if the emoji should never be rendered by emojicompat
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public void setExclusion(boolean exclude) {
+ int hasGlyphBits = getHasGlyph();
+ if (exclude) {
+ mCache = hasGlyphBits | 0b0100;
+ } else {
+ mCache = hasGlyphBits;
+ }
+ }
+
+ /**
+ * If the platform requested that this emoji not be rendered using emojicompat.
+ *
+ * @return true if this emoji should be drawn by the system instead of this renderer
+ */
+ public boolean isPreferredSystemRender() {
+ return (mCache & 0b0100) > 0;
+ }
+
+ /**
+ * @return whether the emoji is in Emoji Presentation by default (without emoji
+ * style selector 0xFE0F)
+ */
+ public boolean isDefaultEmoji() {
+ return getMetadataItem().emojiStyle();
+ }
+
+ /**
+ * @param index index of the codepoint
+ *
+ * @return the codepoint at index
+ */
+ public int getCodepointAt(int index) {
+ return getMetadataItem().codepoints(index);
+ }
+
+ /**
+ * @return the length of the codepoints for this emoji
+ */
+ public int getCodepointsLength() {
+ return getMetadataItem().codepointsLength();
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(super.toString());
+ builder.append(", id:");
+ builder.append(Integer.toHexString(getId()));
+ builder.append(", codepoints:");
+ final int codepointsLength = getCodepointsLength();
+ for (int i = 0; i < codepointsLength; i++) {
+ builder.append(Integer.toHexString(getCodepointAt(i)));
+ builder.append(" ");
+ }
+ return builder.toString();
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java b/app/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
new file mode 100644
index 0000000000..3b23474cc9
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.emoji2.text;
+
+import android.annotation.SuppressLint;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * EmojiSpan subclass used to render emojis using Typeface.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@RequiresApi(19)
+public final class TypefaceEmojiSpan extends EmojiSpan {
+
+ /**
+ * Paint object used to draw a background in debug mode.
+ */
+ private static @Nullable Paint sDebugPaint;
+ @Nullable
+ private TextPaint mWorkingPaint;
+
+ /**
+ * Default constructor.
+ *
+ * @param metadata metadata representing the emoji that this span will draw
+ */
+ public TypefaceEmojiSpan(final @NonNull TypefaceEmojiRasterizer metadata) {
+ super(metadata);
+ }
+
+ @Override
+ public void draw(@NonNull final Canvas canvas,
+ @SuppressLint("UnknownNullness") final CharSequence text,
+ @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x,
+ final int top, final int y, final int bottom, @NonNull final Paint paint) {
+ @Nullable TextPaint textPaint = applyCharacterSpanStyles(text, start, end, paint);
+ if (textPaint != null && textPaint.bgColor != 0) {
+ drawBackground(canvas, textPaint, x, x + getWidth(), top, bottom);
+ }
+ if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
+ canvas.drawRect(x, top , x + getWidth(), bottom, getDebugPaint());
+ }
+ getTypefaceRasterizer().draw(canvas, x, y, textPaint != null ? textPaint : paint);
+ }
+
+ // compat behavior with TextLine.java#handleText background drawing
+ void drawBackground(Canvas c, TextPaint textPaint, float leftX, float rightX, float top,
+ float bottom) {
+ int previousColor = textPaint.getColor();
+ Paint.Style previousStyle = textPaint.getStyle();
+
+ textPaint.setColor(textPaint.bgColor);
+ textPaint.setStyle(Paint.Style.FILL);
+ c.drawRect(leftX, top, rightX, bottom, textPaint);
+
+ textPaint.setStyle(previousStyle);
+ textPaint.setColor(previousColor);
+ }
+
+ /**
+ * This applies the CharacterSpanStyles that _would_ have been applied to this character by
+ * StaticLayout.
+ *
+ * StaticLayout applies CharacterSpanStyles _after_ calling ReplacementSpan.draw, which means
+ * BackgroundSpan will not be applied before draw is called.
+ *
+ * If any CharacterSpanStyles would impact _this_ location, apply them to a TextPaint to
+ * determine if a background needs draw prior to the emoji.
+ *
+ * @param text text that this span is part of
+ * @param start start position to replace
+ * @param end end position to replace
+ * @param paint paint (from TextLine)
+ * @return TextPaint configured
+ */
+ @Nullable
+ private TextPaint applyCharacterSpanStyles(@Nullable CharSequence text, int start, int end,
+ Paint paint) {
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+ CharacterStyle[] spans = spanned.getSpans(start, end, CharacterStyle.class);
+ if (spans.length == 0 || (spans.length == 1 && spans[0] == this)) {
+ if (paint instanceof TextPaint) {
+ // happy path goes here, retain color and bgColor from caller
+ return (TextPaint) paint;
+ } else {
+ return null;
+ }
+ }
+ // there are some non-TypefaceEmojiSpan character styles to apply, update a working
+ // paint to apply each span style, like TextLine would have.
+ TextPaint wp = mWorkingPaint;
+ if (wp == null) {
+ wp = new TextPaint();
+ mWorkingPaint = wp;
+ }
+ wp.set(paint);
+ //noinspection ForLoopReplaceableByForEach
+ for (int pos = 0; pos < spans.length; pos++) {
+ spans[pos].updateDrawState(wp);
+ }
+ return wp;
+ } else {
+ if (paint instanceof TextPaint) {
+ // retain any color and bgColor from caller
+ return (TextPaint) paint;
+ } else {
+ return null;
+ }
+ }
+
+ }
+
+ @NonNull
+ private static Paint getDebugPaint() {
+ if (sDebugPaint == null) {
+ sDebugPaint = new TextPaint();
+ sDebugPaint.setColor(EmojiCompat.get().getEmojiSpanIndicatorColor());
+ sDebugPaint.setStyle(Paint.Style.FILL);
+ }
+ return sDebugPaint;
+ }
+
+
+}
diff --git a/app/src/main/java/androidx/emoji2/text/UnprecomputeTextOnModificationSpannable.java b/app/src/main/java/androidx/emoji2/text/UnprecomputeTextOnModificationSpannable.java
new file mode 100644
index 0000000000..25f44329e2
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/UnprecomputeTextOnModificationSpannable.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.text;
+
+import android.os.Build;
+import android.text.PrecomputedText;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.text.PrecomputedTextCompat;
+
+import java.util.stream.IntStream;
+
+/**
+ * Spannable that will delegate to a passed spannable for all query operations without allocation.
+ *
+ * If delegating to a PrecomputedText, the delegate will be swapped for a SpannableString prior
+ * to any modifications.
+ */
+class UnprecomputeTextOnModificationSpannable implements Spannable {
+
+
+ /**
+ * True when mDelegate is safe to write, otherwise mDelegate will need wrapped before mutation
+ */
+ private boolean mSafeToWrite = false;
+
+ @NonNull
+ private Spannable mDelegate;
+
+ UnprecomputeTextOnModificationSpannable(@NonNull Spannable delegate) {
+ mDelegate = delegate;
+ }
+
+ UnprecomputeTextOnModificationSpannable(@NonNull Spanned delegate) {
+ mDelegate = new SpannableString(delegate);
+ }
+
+ UnprecomputeTextOnModificationSpannable(@NonNull CharSequence delegate) {
+ mDelegate = new SpannableString(delegate);
+ }
+
+ private void ensureSafeWrites() {
+ Spannable old = mDelegate;
+ if (!mSafeToWrite && precomputedTextDetector().isPrecomputedText(old)) {
+ mDelegate = new SpannableString(old);
+ }
+ mSafeToWrite = true;
+ }
+
+ Spannable getUnwrappedSpannable() {
+ return mDelegate;
+ }
+
+ @Override
+ public void setSpan(Object o, int i, int i1, int i2) {
+ ensureSafeWrites();
+ mDelegate.setSpan(o, i, i1, i2);
+ }
+
+ @Override
+ public void removeSpan(Object o) {
+ ensureSafeWrites();
+ mDelegate.removeSpan(o);
+ }
+
+ @Override
+ public T[] getSpans(int i, int i1, Class aClass) {
+ return mDelegate.getSpans(i, i1, aClass);
+ }
+
+ @Override
+ public int getSpanStart(Object o) {
+ return mDelegate.getSpanStart(o);
+ }
+
+ @Override
+ public int getSpanEnd(Object o) {
+ return mDelegate.getSpanEnd(o);
+ }
+
+ @Override
+ public int getSpanFlags(Object o) {
+ return mDelegate.getSpanFlags(o);
+ }
+
+ @Override
+ public int nextSpanTransition(int i, int i1, Class aClass) {
+ return mDelegate.nextSpanTransition(i, i1, aClass);
+ }
+
+ @Override
+ public int length() {
+ return mDelegate.length();
+ }
+
+ @Override
+ public char charAt(int i) {
+ return mDelegate.charAt(i);
+ }
+
+ @NonNull
+ @Override
+ public CharSequence subSequence(int i, int i1) {
+ return mDelegate.subSequence(i, i1);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return mDelegate.toString();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ @Override
+ public IntStream chars() {
+ return CharSequenceHelper_API24.chars(mDelegate);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ @Override
+ public IntStream codePoints() {
+ return CharSequenceHelper_API24.codePoints(mDelegate);
+ }
+
+ @RequiresApi(24)
+ private static class CharSequenceHelper_API24 {
+ private CharSequenceHelper_API24() {
+ // not constructable
+ }
+
+ static IntStream codePoints(CharSequence charSequence) {
+ return charSequence.codePoints();
+ }
+
+ static IntStream chars(CharSequence charSequence) {
+ return charSequence.chars();
+ }
+ }
+
+ static PrecomputedTextDetector precomputedTextDetector() {
+ return (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
+ ? new PrecomputedTextDetector() : new PrecomputedTextDetector_28();
+ }
+
+ static class PrecomputedTextDetector {
+
+ boolean isPrecomputedText(CharSequence text) {
+ return text instanceof PrecomputedTextCompat;
+ }
+ }
+
+ @RequiresApi(28)
+ static class PrecomputedTextDetector_28 extends PrecomputedTextDetector {
+
+ @Override
+ boolean isPrecomputedText(CharSequence text) {
+ return text instanceof PrecomputedText || text instanceof PrecomputedTextCompat;
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataItem.java b/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataItem.java
new file mode 100644
index 0000000000..f6d87b97cf
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataItem.java
@@ -0,0 +1,79 @@
+// CHECKSTYLE:OFF Generated code
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// automatically generated by the FlatBuffers compiler, do not modify
+package androidx.emoji2.text.flatbuffer;
+import java.nio.*;
+import java.lang.*;
+import java.util.*;
+import com.google.flatbuffers.*;
+@SuppressWarnings("unused")
+public final class MetadataItem extends Table {
+ public static void ValidateVersion() { Constants.FLATBUFFERS_2_0_0(); }
+ public static MetadataItem getRootAsMetadataItem(ByteBuffer _bb) { return getRootAsMetadataItem(_bb, new MetadataItem()); }
+ public static MetadataItem getRootAsMetadataItem(ByteBuffer _bb, MetadataItem obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
+ public MetadataItem __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+ public int id() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
+ public boolean emojiStyle() { int o = __offset(6); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
+ public short sdkAdded() { int o = __offset(8); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
+ public short compatAdded() { int o = __offset(10); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
+ public short width() { int o = __offset(12); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
+ public short height() { int o = __offset(14); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
+ public int codepoints(int j) { int o = __offset(16); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; }
+ public int codepointsLength() { int o = __offset(16); return o != 0 ? __vector_len(o) : 0; }
+ public IntVector codepointsVector() { return codepointsVector(new IntVector()); }
+ public IntVector codepointsVector(IntVector obj) { int o = __offset(16); return o != 0 ? obj.__assign(__vector(o), bb) : null; }
+ public ByteBuffer codepointsAsByteBuffer() { return __vector_as_bytebuffer(16, 4); }
+ public ByteBuffer codepointsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 16, 4); }
+ public static int createMetadataItem(FlatBufferBuilder builder,
+ int id,
+ boolean emojiStyle,
+ short sdkAdded,
+ short compatAdded,
+ short width,
+ short height,
+ int codepointsOffset) {
+ builder.startTable(7);
+ MetadataItem.addCodepoints(builder, codepointsOffset);
+ MetadataItem.addId(builder, id);
+ MetadataItem.addHeight(builder, height);
+ MetadataItem.addWidth(builder, width);
+ MetadataItem.addCompatAdded(builder, compatAdded);
+ MetadataItem.addSdkAdded(builder, sdkAdded);
+ MetadataItem.addEmojiStyle(builder, emojiStyle);
+ return MetadataItem.endMetadataItem(builder);
+ }
+ public static void startMetadataItem(FlatBufferBuilder builder) { builder.startTable(7); }
+ public static void addId(FlatBufferBuilder builder, int id) { builder.addInt(0, id, 0); }
+ public static void addEmojiStyle(FlatBufferBuilder builder, boolean emojiStyle) { builder.addBoolean(1, emojiStyle, false); }
+ public static void addSdkAdded(FlatBufferBuilder builder, short sdkAdded) { builder.addShort(2, sdkAdded, 0); }
+ public static void addCompatAdded(FlatBufferBuilder builder, short compatAdded) { builder.addShort(3, compatAdded, 0); }
+ public static void addWidth(FlatBufferBuilder builder, short width) { builder.addShort(4, width, 0); }
+ public static void addHeight(FlatBufferBuilder builder, short height) { builder.addShort(5, height, 0); }
+ public static void addCodepoints(FlatBufferBuilder builder, int codepointsOffset) { builder.addOffset(6, codepointsOffset, 0); }
+ public static int createCodepointsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
+ public static void startCodepointsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endMetadataItem(FlatBufferBuilder builder) {
+ int o = builder.endTable();
+ return o;
+ }
+ public static final class Vector extends BaseVector {
+ public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
+ public MetadataItem get(int j) { return get(new MetadataItem(), j); }
+ public MetadataItem get(MetadataItem obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
+ }
+}
diff --git a/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataList.java b/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataList.java
new file mode 100644
index 0000000000..7c7a5aae30
--- /dev/null
+++ b/app/src/main/java/androidx/emoji2/text/flatbuffer/MetadataList.java
@@ -0,0 +1,66 @@
+// CHECKSTYLE:OFF Generated code
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// automatically generated by the FlatBuffers compiler, do not modify
+package androidx.emoji2.text.flatbuffer;
+import java.nio.*;
+import java.lang.*;
+import java.util.*;
+import com.google.flatbuffers.*;
+@SuppressWarnings("unused")
+public final class MetadataList extends Table {
+ public static void ValidateVersion() { Constants.FLATBUFFERS_2_0_0(); }
+ public static MetadataList getRootAsMetadataList(ByteBuffer _bb) { return getRootAsMetadataList(_bb, new MetadataList()); }
+ public static MetadataList getRootAsMetadataList(ByteBuffer _bb, MetadataList obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
+ public MetadataList __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+ public int version() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
+ public androidx.emoji2.text.flatbuffer.MetadataItem list(int j) { return list(new androidx.emoji2.text.flatbuffer.MetadataItem(), j); }
+ public androidx.emoji2.text.flatbuffer.MetadataItem list(androidx.emoji2.text.flatbuffer.MetadataItem obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int listLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
+ public androidx.emoji2.text.flatbuffer.MetadataItem.Vector listVector() { return listVector(new androidx.emoji2.text.flatbuffer.MetadataItem.Vector()); }
+ public androidx.emoji2.text.flatbuffer.MetadataItem.Vector listVector(androidx.emoji2.text.flatbuffer.MetadataItem.Vector obj) { int o = __offset(6); return o != 0 ? obj.__assign(__vector(o), 4, bb) : null; }
+ public String sourceSha() { int o = __offset(8); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer sourceShaAsByteBuffer() { return __vector_as_bytebuffer(8, 1); }
+ public ByteBuffer sourceShaInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); }
+ public static int createMetadataList(FlatBufferBuilder builder,
+ int version,
+ int listOffset,
+ int sourceShaOffset) {
+ builder.startTable(3);
+ MetadataList.addSourceSha(builder, sourceShaOffset);
+ MetadataList.addList(builder, listOffset);
+ MetadataList.addVersion(builder, version);
+ return MetadataList.endMetadataList(builder);
+ }
+ public static void startMetadataList(FlatBufferBuilder builder) { builder.startTable(3); }
+ public static void addVersion(FlatBufferBuilder builder, int version) { builder.addInt(0, version, 0); }
+ public static void addList(FlatBufferBuilder builder, int listOffset) { builder.addOffset(1, listOffset, 0); }
+ public static int createListVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startListVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static void addSourceSha(FlatBufferBuilder builder, int sourceShaOffset) { builder.addOffset(2, sourceShaOffset, 0); }
+ public static int endMetadataList(FlatBufferBuilder builder) {
+ int o = builder.endTable();
+ return o;
+ }
+ public static void finishMetadataListBuffer(FlatBufferBuilder builder, int offset) { builder.finish(offset); }
+ public static void finishSizePrefixedMetadataListBuffer(FlatBufferBuilder builder, int offset) { builder.finishSizePrefixed(offset); }
+ public static final class Vector extends BaseVector {
+ public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
+ public MetadataList get(int j) { return get(new MetadataList(), j); }
+ public MetadataList get(MetadataList obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
+ }
+}