[go: nahoru, domu]

Create new emoji2 feature

Moved core and bundled features to folders that match their feature name

Bug: 178035684

Test: Refactoring, ensured build

Relnote: N/A

Change-Id: Ibfbc1e9fb09276a73cc273176c9b868f65a0ed2f
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
index 5e24cda..d22e5be 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryGroups.kt
@@ -48,6 +48,7 @@
     val DRAWERLAYOUT = LibraryGroup("androidx.drawerlayout", LibraryVersions.DRAWERLAYOUT)
     val DYNAMICANIMATION = LibraryGroup("androidx.dynamicanimation", null)
     val EMOJI = LibraryGroup("androidx.emoji", null)
+    val EMOJI2 = LibraryGroup("androidx.emoji2", LibraryVersions.EMOJI2)
     val ENTERPRISE = LibraryGroup("androidx.enterprise", LibraryVersions.ENTERPRISE)
     val EXIFINTERFACE = LibraryGroup("androidx.exifinterface", LibraryVersions.EXIFINTERFACE)
     val FRAGMENT = LibraryGroup("androidx.fragment", LibraryVersions.FRAGMENT)
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 360643f..2647951 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -62,6 +62,7 @@
     val DYNAMICANIMATION = Version("1.1.0-alpha04")
     val DYNAMICANIMATION_KTX = Version("1.0.0-alpha04")
     val EMOJI = Version("1.2.0-alpha03")
+    val EMOJI2 = Version("1.0.0-alpha01")
     val ENTERPRISE = Version("1.1.0-rc01")
     val EXIFINTERFACE = Version("1.4.0-alpha01")
     val FRAGMENT = Version("1.4.0-alpha01")
diff --git a/emoji2/OWNERS b/emoji2/OWNERS
new file mode 100644
index 0000000..fe41080
--- /dev/null
+++ b/emoji2/OWNERS
@@ -0,0 +1,3 @@
+clarabayarri@google.com
+siyamed@google.com
+seanmcq@google.com
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
new file mode 100644
index 0000000..5b6e687
--- /dev/null
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -0,0 +1,42 @@
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+import androidx.build.RunApiTasks
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+ext {
+    fontDir = project(':noto-emoji-compat').projectDir
+}
+
+android {
+    sourceSets {
+        main.assets.srcDirs new File(fontDir, "font").getAbsolutePath()
+    }
+}
+
+dependencies {
+    api(project(":emoji"))
+}
+
+androidx {
+    name = "Android Emoji2 Compat"
+    publish = Publish.NONE
+    mavenVersion = LibraryVersions.EMOJI2
+    mavenGroup = LibraryGroups.EMOJI2
+    inceptionYear = "2017"
+    description = "Library bundled with assets to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
+
+    license {
+        name = "SIL Open Font License, Version 1.1"
+        url = "http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web"
+    }
+
+    license {
+        name = "Unicode, Inc. License"
+        url = "http://www.unicode.org/copyright.html#License"
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-bundled/lint-baseline.xml b/emoji2/emoji2-bundled/lint-baseline.xml
new file mode 100644
index 0000000..27e26a8
--- /dev/null
+++ b/emoji2/emoji2-bundled/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
+
+</issues>
diff --git a/emoji2/emoji2-bundled/src/main/AndroidManifest.xml b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..56ac589
--- /dev/null
+++ b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest package="androidx.emoji.bundled"/>
\ No newline at end of file
diff --git a/emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java b/emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java
new file mode 100644
index 0000000..03f5567
--- /dev/null
+++ b/emoji2/emoji2-bundled/src/main/java/androidx/emoji/bundled/BundledEmojiCompatConfig.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+package androidx.emoji.bundled;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+import androidx.emoji.text.EmojiCompat;
+import androidx.emoji.text.MetadataRepo;
+
+/**
+ * {@link EmojiCompat.Config} implementation that loads the metadata using AssetManager and
+ * bundled resources.
+ * <p/>
+ * <pre><code>EmojiCompat.init(new BundledEmojiCompatConfig(context));</code></pre>
+ *
+ * @see EmojiCompat
+ */
+public class BundledEmojiCompatConfig extends EmojiCompat.Config {
+
+    /**
+     * Default constructor.
+     *
+     * @param context Context instance
+     */
+    public BundledEmojiCompatConfig(@NonNull Context context) {
+        super(new BundledMetadataLoader(context));
+    }
+
+    private static class BundledMetadataLoader implements EmojiCompat.MetadataRepoLoader {
+        private final Context mContext;
+
+        BundledMetadataLoader(@NonNull Context context) {
+            mContext = context.getApplicationContext();
+        }
+
+        @Override
+        @RequiresApi(19)
+        public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            Preconditions.checkNotNull(loaderCallback, "loaderCallback cannot be null");
+            final InitRunnable runnable = new InitRunnable(mContext, loaderCallback);
+            final Thread thread = new Thread(runnable);
+            thread.setDaemon(false);
+            thread.start();
+        }
+    }
+
+    @RequiresApi(19)
+    private static class InitRunnable implements Runnable {
+        private static final String FONT_NAME = "NotoColorEmojiCompat.ttf";
+        private final EmojiCompat.MetadataRepoLoaderCallback mLoaderCallback;
+        private final Context mContext;
+
+        InitRunnable(Context context, EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            mContext = context;
+            mLoaderCallback = loaderCallback;
+        }
+
+        @Override
+        public void run() {
+            try {
+                final AssetManager assetManager = mContext.getAssets();
+                final MetadataRepo resourceIndex = MetadataRepo.create(assetManager, FONT_NAME);
+                mLoaderCallback.onLoaded(resourceIndex);
+            } catch (Throwable t) {
+                mLoaderCallback.onFailed(t);
+            }
+        }
+    }
+}
diff --git a/emoji2/emoji2/AndroidManifest.xml b/emoji2/emoji2/AndroidManifest.xml
new file mode 100644
index 0000000..524cb3c
--- /dev/null
+++ b/emoji2/emoji2/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest package="androidx.emoji2"/>
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
new file mode 100644
index 0000000..ec4a8ea
--- /dev/null
+++ b/emoji2/emoji2/build.gradle
@@ -0,0 +1,79 @@
+import androidx.build.BundleInsideHelper
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.Publish
+import androidx.build.RunApiTasks
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("com.github.johnrengelman.shadow")
+}
+
+ext {
+    fontDir = project(':noto-emoji-compat').projectDir
+}
+
+BundleInsideHelper.forInsideAar(
+    project,
+    /* from = */ "com.google.flatbuffers",
+    /* to =   */ "androidx.text.emoji.flatbuffer"
+)
+
+dependencies {
+    bundleInside(project(":noto-emoji-compat"))
+
+    api("androidx.core:core:1.3.0-rc01")
+    implementation("androidx.collection:collection:1.1.0")
+
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+    androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-runtime')
+}
+
+android {
+    sourceSets {
+        main {
+            // We use a non-standard manifest path.
+            manifest.srcFile 'AndroidManifest.xml'
+            res.srcDirs += 'src/main/res-public'
+            resources {
+                srcDirs += [fontDir.getAbsolutePath()]
+                includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
+            }
+        }
+
+        androidTest {
+            assets {
+                srcDirs = [new File(fontDir, "font").getAbsolutePath(),
+                           new File(fontDir, "supported-emojis").getAbsolutePath()]
+            }
+        }
+    }
+}
+
+androidx {
+    name = "Android Emoji2 Compat"
+    publish = Publish.NONE
+    mavenVersion = LibraryVersions.EMOJI2
+    mavenGroup = LibraryGroups.EMOJI2
+    inceptionYear = "2017"
+    description = "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
+
+    license {
+        name = "SIL Open Font License, Version 1.1"
+        url = "http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web"
+    }
+
+    license {
+        name = "Unicode, Inc. License"
+        url = "http://www.unicode.org/copyright.html#License"
+    }
+}
diff --git a/emoji2/emoji2/lint-baseline.xml b/emoji2/emoji2/lint-baseline.xml
new file mode 100644
index 0000000..d6d91ea
--- /dev/null
+++ b/emoji2/emoji2/lint-baseline.xml
@@ -0,0 +1,1416 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="5" by="lint 4.2.0-beta04" client="gradle" variant="debug" version="4.2.0-beta04">
+
+    <issue
+        id="PrivateConstructorForUtilityClass"
+        message="Utility class with non private constructor"
+        errorLine1="    private static final class CodepointIndexFinder {"
+        errorLine2="                               ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
+            line="654"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiButton is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="58"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="65"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractEditText is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="74"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiExtractTextLayout is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="99"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 19, the call containing class androidx.emoji2.widget.EmojiInputFilter.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
+        errorLine1="            if (textView != null &amp;&amp; textView.isAttachedToWindow()) {"
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiInputFilter.java"
+            line="110"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
+        errorLine1="                if (!Character.isSurrogate(c)) {"
+        errorLine2="                               ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
+            line="702"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 19, the call containing class androidx.emoji2.text.EmojiProcessor.CodepointIndexFinder is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
+        errorLine1="                if (!Character.isSurrogate(c)) {"
+        errorLine2="                               ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiProcessor.java"
+            line="759"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.EmojiTextView is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="58"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 19, the call containing class androidx.emoji2.widget.EmojiTextWatcher.InitCallbackImpl is not annotated with @RequiresApi(x) where x is at least 19. Either annotate the containing class with at least @RequiresApi(19) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(19)."
+        errorLine1="            if (editText != null &amp;&amp; editText.isAttachedToWindow()) {"
+        errorLine2="                                             ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java"
+            line="121"
+            column="46"/>
+    </issue>
+
+    <issue
+        id="UnsafeNewApiCall"
+        message="This call is to a method from API 21, the call containing class androidx.emoji2.widget.ExtractButtonCompat is not annotated with @RequiresApi(x) where x is at least 21. Either annotate the containing class with at least @RequiresApi(21) or move the call to a static method in a wrapper class annotated with at least @RequiresApi(21)."
+        errorLine1="        super(context, attrs, defStyleAttr, defStyleRes);"
+        errorLine2="        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="53"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="KotlinPropertyAccess"
+        message="The getter return type (`int`) and setter parameter type (`boolean`) getter and setter methods for property `hasGlyph` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
+        errorLine1="    public int getHasGlyph() {"
+        errorLine2="               ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
+            line="184"
+            column="16"/>
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
+            line="193"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,"
+        errorLine2="                                                       ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java"
+            line="40"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context) {"
+        errorLine2="                       ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="41"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs) {"
+        errorLine2="                       ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="46"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs) {"
+        errorLine2="                                        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="46"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                       ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="51"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="51"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                       ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="57"
+            column="24"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                                        ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="57"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setFilters(InputFilter[] filters) {"
+        errorLine2="                           ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="70"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiButton.java"
+            line="92"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static EmojiCompat init(@NonNull final Config config) {"
+        errorLine2="                  ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="302"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static EmojiCompat reset(@NonNull final Config config) {"
+        errorLine2="                  ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="322"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
+        errorLine2="                  ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="337"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static EmojiCompat reset(final EmojiCompat emojiCompat) {"
+        errorLine2="                                          ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="337"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static EmojiCompat get() {"
+        errorLine2="                  ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="352"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="            final KeyEvent event) {"
+        errorLine2="                  ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="541"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence) {"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="625"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="662"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="698"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public CharSequence process(@NonNull final CharSequence charSequence,"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="739"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config registerInitCallback(@NonNull InitCallback initCallback) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="982"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config unregisterInitCallback(@NonNull InitCallback initCallback) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1000"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setReplaceAll(final boolean replaceAll) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1017"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1037"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1057"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1081"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setEmojiSpanIndicatorColor(@ColorInt int color) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1092"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {"
+        errorLine2="               ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1133"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        protected final MetadataRepoLoader getMetadataRepoLoader() {"
+        errorLine2="                        ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiCompat.java"
+            line="1154"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="48"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="53"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="53"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="58"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="58"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="64"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="64"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
+        errorLine2="           ~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="88"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
+        errorLine2="                                                   ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="88"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiEditText.java"
+            line="133"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context) {"
+        errorLine2="                                ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="56"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                                ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="61"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs) {"
+        errorLine2="                                                 ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="61"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="66"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                                 ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="66"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
+        errorLine2="                                ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="72"
+            column="33"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,"
+        errorLine2="                                                 ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="72"
+            column="50"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
+        errorLine2="           ~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="97"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {"
+        errorLine2="                                                   ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="97"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java"
+            line="161"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractTextLayout(Context context) {"
+        errorLine2="                                  ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="79"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractTextLayout(Context context,"
+        errorLine2="                                  ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="84"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractTextLayout(Context context,"
+        errorLine2="                                  ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="90"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
+        errorLine2="                                  ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="97"
+            column="35"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiExtractTextLayout(Context context, AttributeSet attrs,"
+        errorLine2="                                                   ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="97"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
+        errorLine2="                                        ~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="163"
+            column="41"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {"
+        errorLine2="                                                                               ~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java"
+            line="163"
+            column="80"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public Typeface getTypeface() {"
+        errorLine2="           ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiMetadata.java"
+            line="120"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,"
+        errorLine2="                                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
+            line="77"
+            column="58"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="            final int end, final Paint.FontMetricsInt fm) {"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/EmojiSpan.java"
+            line="78"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="41"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="46"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="46"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="51"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="51"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                         ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="57"
+            column="26"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {"
+        errorLine2="                                          ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="57"
+            column="43"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setFilters(InputFilter[] filters) {"
+        errorLine2="                           ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="70"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/EmojiTextView.java"
+            line="92"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context) {"
+        errorLine2="                               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="38"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs) {"
+        errorLine2="                               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="42"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs) {"
+        errorLine2="                                                ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="42"
+            column="49"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="46"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {"
+        errorLine2="                                                ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="46"
+            column="49"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
+        errorLine2="                               ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="51"
+            column="32"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,"
+        errorLine2="                                                ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="51"
+            column="49"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {"
+        errorLine2="                                                     ~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java"
+            line="70"
+            column="54"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="143"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public FontRequestEmojiCompatConfig setHandler(Handler handler) {"
+        errorLine2="                                                   ~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="143"
+            column="52"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="156"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public FontRequestEmojiCompatConfig setRetryPolicy(RetryPolicy policy) {"
+        errorLine2="                                                       ~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="156"
+            column="56"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public FontFamilyResult fetchFonts(@NonNull Context context,"
+        errorLine2="               ~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="335"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="        public Typeface buildTypeface(@NonNull Context context,"
+        errorLine2="               ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java"
+            line="341"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static MetadataRepo create(@NonNull final Typeface typeface,"
+        errorLine2="                  ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="103"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static MetadataRepo create(@NonNull final Typeface typeface,"
+        errorLine2="                  ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="115"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public static MetadataRepo create(@NonNull final AssetManager assetManager,"
+        errorLine2="                  ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="127"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="            final String assetPath) throws IOException {"
+        errorLine2="                  ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="128"
+            column="19"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public char[] getEmojiCharArray() {"
+        errorLine2="           ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="176"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public MetadataList getMetadataList() {"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/MetadataRepo.java"
+            line="184"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public CharSequence subSequence(int start, int end) {"
+        errorLine2="           ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="124"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void setSpan(Object what, int start, int end, int flags) {"
+        errorLine2="                        ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="134"
+            column="25"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public &lt;T> T[] getSpans(int queryStart, int queryEnd, Class&lt;T> kind) {"
+        errorLine2="               ~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="149"
+            column="16"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public &lt;T> T[] getSpans(int queryStart, int queryEnd, Class&lt;T> kind) {"
+        errorLine2="                                                          ~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="149"
+            column="59"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void removeSpan(Object what) {"
+        errorLine2="                           ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="167"
+            column="28"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public int getSpanStart(Object tag) {"
+        errorLine2="                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="189"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public int getSpanEnd(Object tag) {"
+        errorLine2="                          ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="203"
+            column="27"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public int getSpanFlags(Object tag) {"
+        errorLine2="                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="217"
+            column="29"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public int nextSpanTransition(int start, int limit, Class type) {"
+        errorLine2="                                                        ~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="231"
+            column="57"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="301"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {"
+        errorLine2="                                                              ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="301"
+            column="63"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="309"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,"
+        errorLine2="                                                              ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="309"
+            column="63"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="318"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb) {"
+        errorLine2="                                                    ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="318"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="324"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {"
+        errorLine2="                                                    ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="324"
+            column="53"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder delete(int start, int end) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="330"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="336"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text) {"
+        errorLine2="                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="336"
+            column="42"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(char text) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="342"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text, int start, int end) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="348"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text, int start, int end) {"
+        errorLine2="                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="348"
+            column="42"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
+        errorLine2="           ~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="354"
+            column="12"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
+        errorLine2="                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="354"
+            column="42"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {"
+        errorLine2="                                                            ~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/widget/SpannableBuilder.java"
+            line="354"
+            column="61"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public TypefaceEmojiSpan(final EmojiMetadata metadata) {"
+        errorLine2="                                   ~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
+            line="48"
+            column="36"/>
+    </issue>
+
+    <issue
+        id="UnknownNullness"
+        message="Unknown nullability; explicitly declare as `@Nullable` or `@NonNull` to improve Kotlin interoperability; see https://android.github.io/kotlin-guides/interop.html#nullability-annotations"
+        errorLine1="    public void draw(@NonNull final Canvas canvas, final CharSequence text,"
+        errorLine2="                                                         ~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java"
+            line="53"
+            column="58"/>
+    </issue>
+
+</issues>
diff --git a/emoji2/emoji2/src/androidTest/AndroidManifest.xml b/emoji2/emoji2/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..0b2cc8b
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="androidx.emoji2.text">
+
+    <application>
+        <activity android:name=".TestActivity"/>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
new file mode 100644
index 0000000..6e5a680
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.Paint;
+import android.text.Spanned;
+
+import androidx.core.graphics.PaintCompat;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Reads raw/allemojis.txt which includes all the emojis known to human kind and tests that
+ * EmojiCompat creates EmojiSpans for each one of them.
+ */
+@LargeTest
+@RunWith(Parameterized.class)
+@SdkSuppress(minSdkVersion = 19)
+public class AllEmojisTest {
+
+    /**
+     * String representation for a single emoji
+     */
+    private String mString;
+
+    /**
+     * Codepoints of emoji for better assert error message.
+     */
+    private String mCodepoints;
+
+    /**
+     * Paint object used to check if Typeface can render the given emoji.
+     */
+    private Paint mPaint;
+
+    @BeforeClass
+    public static void setup() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() throws IOException {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final InputStream inputStream = context.getAssets().open("emojis.txt");
+        try {
+            final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+            final Collection<Object[]> data = new ArrayList<>();
+            final StringBuilder stringBuilder = new StringBuilder();
+            final StringBuilder codePointsBuilder = new StringBuilder();
+
+            String s;
+            while ((s = reader.readLine()) != null) {
+                s = s.trim();
+                // pass comments
+                if (s.isEmpty() || s.startsWith("#")) continue;
+
+                stringBuilder.setLength(0);
+                codePointsBuilder.setLength(0);
+
+                // emoji codepoints are space separated: i.e. 0x1f1e6 0x1f1e8
+                final String[] split = s.split(" ");
+
+                for (int index = 0; index < split.length; index++) {
+                    final String part = split[index].trim();
+                    codePointsBuilder.append(part);
+                    codePointsBuilder.append(",");
+                    stringBuilder.append(Character.toChars(Integer.parseInt(part, 16)));
+                }
+                data.add(new Object[]{stringBuilder.toString(), codePointsBuilder.toString()});
+            }
+
+            return data;
+        } finally {
+            inputStream.close();
+        }
+
+    }
+
+    public AllEmojisTest(String string, String codepoints) {
+        mString = string;
+        mCodepoints = codepoints;
+        mPaint = new Paint();
+    }
+
+    @Test
+    public void testEmoji() {
+        assertTrue("EmojiCompat should have emoji: " + mCodepoints,
+                EmojiCompat.get().hasEmojiGlyph(mString));
+        assertEmojiCompatAddsEmoji(mString);
+        assertSpanCanRenderEmoji(mString);
+    }
+
+    private void assertSpanCanRenderEmoji(final String str) {
+        final Spanned spanned = (Spanned) EmojiCompat.get().process(new TestString(str).toString());
+        final EmojiSpan[] spans = spanned.getSpans(0, spanned.length(), EmojiSpan.class);
+        final EmojiMetadata metadata = spans[0].getMetadata();
+        mPaint.setTypeface(metadata.getTypeface());
+
+        final String codepoint = String.valueOf(Character.toChars(metadata.getId()));
+        assertTrue(metadata.toString() + " should be rendered",
+                PaintCompat.hasGlyph(mPaint, codepoint));
+    }
+
+    private void assertEmojiCompatAddsEmoji(final String str) {
+        TestString string = new TestString(str);
+        CharSequence sequence = EmojiCompat.get().process(string.toString());
+        assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
+        assertThat(sequence,
+                EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
+
+        // case where Emoji is in the middle of string
+        string = new TestString(str).withPrefix().withSuffix();
+        sequence = EmojiCompat.get().process(string.toString());
+        assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
+        assertThat(sequence,
+                EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
+
+        // case where Emoji is at the end of string
+        string = new TestString(str).withSuffix();
+        sequence = EmojiCompat.get().process(string.toString());
+        assertThat(sequence, EmojiMatcher.hasEmojiCount(1));
+        assertThat(sequence,
+                EmojiMatcher.hasEmojiAt(string.emojiStartIndex(), string.emojiEndIndex()));
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
new file mode 100644
index 0000000..34525b1
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ConfigTest {
+
+    Context mContext;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructor_throwsExceptionIfMetadataLoaderNull() {
+        new TestConfigBuilder.TestConfig(null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testInitCallback_throwsExceptionIfNull() {
+        new ValidTestConfig().registerInitCallback(null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testUnregisterInitCallback_throwsExceptionIfNull() {
+        new ValidTestConfig().unregisterInitCallback(null);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testBuild_withDefaultValues() {
+        final EmojiCompat.Config config = new ValidTestConfig().setReplaceAll(true);
+
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+
+        final CharSequence processed = emojiCompat.process(new TestString(
+                Emoji.EMOJI_SINGLE_CODEPOINT)
+                .toString());
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed, EmojiMatcher.hasEmoji(Emoji.EMOJI_SINGLE_CODEPOINT));
+    }
+
+    @Test
+    public void testInitCallback_callsSuccessCallback() {
+        final EmojiCompat.InitCallback initCallback1 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.InitCallback initCallback2 = mock(EmojiCompat.InitCallback.class);
+
+        final EmojiCompat.Config config = new ValidTestConfig().registerInitCallback(initCallback1)
+                .registerInitCallback(initCallback2);
+        EmojiCompat.reset(config);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback1, times(1)).onInitialized();
+        verify(initCallback2, times(1)).onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19) //Fail callback never called for pre 19
+    public void testInitCallback_callsFailCallback() {
+        final EmojiCompat.InitCallback initCallback1 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.InitCallback initCallback2 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        doThrow(new RuntimeException("")).when(loader)
+                .load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .registerInitCallback(initCallback1)
+                .registerInitCallback(initCallback2);
+        EmojiCompat.reset(config);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback1, times(1)).onFailed(any(Throwable.class));
+        verify(initCallback2, times(1)).onFailed(any(Throwable.class));
+    }
+
+    @Test
+    public void testBuild_withEmojiSpanIndicator() {
+        EmojiCompat.Config config = new ValidTestConfig();
+        EmojiCompat emojiCompat = EmojiCompat.reset(config);
+
+        assertFalse(emojiCompat.isEmojiSpanIndicatorEnabled());
+
+        config = new ValidTestConfig().setEmojiSpanIndicatorEnabled(true);
+        emojiCompat = EmojiCompat.reset(config);
+
+        assertTrue(emojiCompat.isEmojiSpanIndicatorEnabled());
+    }
+
+    @Test
+    public void testBuild_withEmojiSpanIndicatorColor() {
+        EmojiCompat.Config config = new ValidTestConfig();
+        EmojiCompat emojiCompat = EmojiCompat.reset(config);
+
+        assertEquals(Color.GREEN, emojiCompat.getEmojiSpanIndicatorColor());
+
+        config = new ValidTestConfig().setEmojiSpanIndicatorColor(Color.RED);
+        emojiCompat = EmojiCompat.reset(config);
+
+        assertEquals(Color.RED, emojiCompat.getEmojiSpanIndicatorColor());
+    }
+
+    @Test
+    public void testBuild_defaultEmojiSpanIndicatorColor() {
+        final EmojiCompat.Config config = new ValidTestConfig().setEmojiSpanIndicatorEnabled(true);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+
+        assertTrue(emojiCompat.isEmojiSpanIndicatorEnabled());
+    }
+
+    @Test
+    public void testBuild_manualLoadStrategy_doesNotCallMetadataLoaderLoad() {
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        final EmojiCompat.Config config = new ValidTestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testGlyphCheckerInstance_EmojiSpan_isNotAdded_whenHasGlyph_returnsTrue() {
+        final EmojiCompat.GlyphChecker glyphChecker = mock(EmojiCompat.GlyphChecker.class);
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(true);
+
+        final EmojiCompat.Config config = TestConfigBuilder.freshConfig()
+                .setReplaceAll(false)
+                .setGlyphChecker(glyphChecker);
+        EmojiCompat.reset(config);
+
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+        CharSequence processed = EmojiCompat.get().process(original, 0, original.length());
+
+        verify(glyphChecker, times(1))
+                .hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt());
+        assertThat(processed, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testGlyphCheckerInstance_EmojiSpan_isAdded_whenHasGlyph_returnsFalse() {
+        final EmojiCompat.GlyphChecker glyphChecker = mock(EmojiCompat.GlyphChecker.class);
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(false);
+
+        final EmojiCompat.Config config = TestConfigBuilder.freshConfig()
+                .setReplaceAll(false)
+                .setGlyphChecker(glyphChecker);
+        EmojiCompat.reset(config);
+
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+
+        CharSequence processed = EmojiCompat.get().process(original, 0, original.length());
+
+        verify(glyphChecker, times(1))
+                .hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt());
+        assertThat(processed, EmojiMatcher.hasEmoji());
+    }
+
+    private static class ValidTestConfig extends EmojiCompat.Config {
+        ValidTestConfig() {
+            super(new TestConfigBuilder.TestEmojiDataLoader());
+        }
+
+        ValidTestConfig(EmojiCompat.MetadataRepoLoader loader) {
+            super(loader);
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
new file mode 100644
index 0000000..46d51bc
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
@@ -0,0 +1,947 @@
+/*
+ * 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 org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.SuppressLint;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.SpannedString;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.KeyboardUtil;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiCompatTest {
+
+    @Before
+    public void setup() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGet_throwsException() {
+        EmojiCompat.reset((EmojiCompat) null);
+        EmojiCompat.get();
+    }
+
+    @Test
+    public void testProcess_doesNothing_withNullCharSequence() {
+        assertNull(EmojiCompat.get().process(null));
+    }
+
+    @Test
+    public void testProcess_returnsEmptySpanned_withEmptyString() {
+        final CharSequence charSequence = EmojiCompat.get().process("");
+        assertNotNull(charSequence);
+        assertEquals(0, charSequence.length());
+        assertThat(charSequence, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @SuppressLint("Range")
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_withNegativeStartValue() {
+        EmojiCompat.get().process("a", -1, 1);
+    }
+
+    @SuppressLint("Range")
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_withNegativeEndValue() {
+        EmojiCompat.get().process("a", 1, -1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_withStartSmallerThanEndValue() {
+        EmojiCompat.get().process("aa", 1, 0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_withStartGreaterThanLength() {
+        EmojiCompat.get().process("a", 2, 2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_withEndGreaterThanLength() {
+        EmojiCompat.get().process("a", 0, 2);
+    }
+
+    @Test
+    public void testProcessWithStartEnd_withNoOpValues() {
+        final Spannable spannable = new SpannableString(new TestString('a')
+                .withPrefix().withSuffix().toString());
+        // early return check
+        assertSame(spannable, EmojiCompat.get().process(spannable, 0, 0));
+        assertSame(spannable, EmojiCompat.get().process(spannable, 1, 1));
+        assertSame(spannable, EmojiCompat.get().process(spannable, spannable.length(),
+                spannable.length()));
+    }
+
+    @Test
+    public void testProcess_doesNotAddEmojiSpan() {
+        final String string = "abc";
+        final CharSequence charSequence = EmojiCompat.get().process(string);
+        assertNotNull(charSequence);
+        assertEquals(string, charSequence.toString());
+        assertThat(charSequence, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testProcess_returnsSameCharSequence_pre19() {
+        assertNull(EmojiCompat.get().process(null));
+
+        CharSequence testString = "abc";
+        assertSame(testString, EmojiCompat.get().process(testString));
+
+        testString = new SpannableString("abc");
+        assertSame(testString, EmojiCompat.get().process(testString));
+
+        testString = new TestString(new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE}).toString();
+        assertSame(testString, EmojiCompat.get().process(testString));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsSingleCodePointEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_SINGLE_CODEPOINT);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsFlagEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_FLAG);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsUnknownFlagEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_UNKNOWN_FLAG);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsRegionalIndicatorSymbol() {
+        assertCodePointMatch(Emoji.EMOJI_REGIONAL_SYMBOL);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsKeyCapEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_DIGIT_KEYCAP);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_doesNotAddEmojiForNumbers() {
+        assertCodePointDoesNotMatch(new int[] {Emoji.CHAR_DIGIT});
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_doesNotAddEmojiForNumbers_1() {
+        final TestString string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).append('1', 'f');
+        CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsVariantSelectorEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_DIGIT_ES);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_doesNotAddVariantSelectorTextStyle() {
+        assertCodePointDoesNotMatch(new int[]{Emoji.CHAR_DIGIT, Emoji.CHAR_VS_TEXT});
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsVariantSelectorAndKeyCapEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_DIGIT_ES_KEYCAP);
+    }
+
+    @Test
+    public void testProcess_doesNotAddEmoji_forVariantBaseWithoutSelector() {
+        assertCodePointDoesNotMatch(new int[]{Emoji.CHAR_DIGIT});
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsAsteriskKeyCapEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_ASTERISK_KEYCAP);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsSkinModifierEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_SKIN_MODIFIER);
+        assertCodePointMatch(Emoji.EMOJI_SKIN_MODIFIER_TYPE_ONE);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsSkinModifierEmoji_withVariantSelector() {
+        assertCodePointMatch(Emoji.EMOJI_SKIN_MODIFIER_WITH_VS);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsSkinModifierEmoji_270c_withVariantSelector() {
+        // 0x270c is a Standardized Variant Base, Emoji Modifier Base and also Emoji
+        // therefore it is different than i.e. 0x1f3c3. The code actually failed for this test
+        // at first.
+        assertCodePointMatch(0xF0734,
+                new int[]{0x270C, Emoji.CHAR_VS_EMOJI, Emoji.CHAR_FITZPATRICK});
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_defaultStyleDoesNotAddSpan() {
+        assertCodePointDoesNotMatch(new int[]{Emoji.CHAR_DEFAULT_TEXT_STYLE});
+        assertCodePointMatch(Emoji.DEFAULT_TEXT_STYLE);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_defaultEmojiStyle_withTextStyleVs() {
+        assertCodePointMatch(Emoji.EMOJI_SINGLE_CODEPOINT.id(),
+                new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE, Emoji.CHAR_VS_EMOJI});
+        assertCodePointDoesNotMatch(new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE, Emoji.CHAR_VS_TEXT});
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_genderEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_GENDER);
+        assertCodePointMatch(Emoji.EMOJI_GENDER_WITHOUT_VS);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_standardizedVariantEmojiExceptions() {
+        final int[][] exceptions = new int[][]{
+                {0x2600, 0xF034D},
+                {0x2601, 0xF0167},
+                {0x260E, 0xF034E},
+                {0x261D, 0xF0227},
+                {0x263A, 0xF02A6},
+                {0x2660, 0xF0350},
+                {0x2663, 0xF033F},
+                {0x2665, 0xF033B},
+                {0x2666, 0xF033E},
+                {0x270C, 0xF0079},
+                {0x2744, 0xF0342},
+                {0x2764, 0xF0362}
+        };
+
+        for (int i = 0; i < exceptions.length; i++) {
+            final int[] codepoints = new int[]{exceptions[i][0]};
+            assertCodePointMatch(exceptions[i][1], codepoints);
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsZwjEmoji() {
+        assertCodePointMatch(Emoji.EMOJI_WITH_ZWJ);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_doesNotAddEmojiForNumbersAfterZwjEmo() {
+        TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).append(0x20, 0x2B, 0x31)
+                .withSuffix().withPrefix();
+        CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence,
+                EmojiMatcher.hasEmojiAt(Emoji.EMOJI_WITH_ZWJ, string.emojiStartIndex(),
+                        string.emojiEndIndex() - 3));
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+
+        string = new TestString(Emoji.EMOJI_WITH_ZWJ).withSuffix().withPrefix();
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_addsEmojiThatFollowsDigit() {
+        TestString string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).prepend('N', '5');
+        CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, EmojiMatcher.hasEmojiAt(
+                Emoji.EMOJI_SINGLE_CODEPOINT, string.emojiStartIndex() + 2,
+                string.emojiEndIndex()));
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+
+        string = new TestString(Emoji.EMOJI_WITH_ZWJ).prepend('N', '5');
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, EmojiMatcher.hasEmojiAt(
+                Emoji.EMOJI_WITH_ZWJ, string.emojiStartIndex() + 2,
+                string.emojiEndIndex()));
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withAppend() {
+        final Editable editable = new SpannableStringBuilder(new TestString('a').withPrefix()
+                .withSuffix().toString());
+        final int start = 1;
+        final int end = start + Emoji.EMOJI_SINGLE_CODEPOINT.charCount();
+        editable.insert(start, new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString());
+        EmojiCompat.get().process(editable, start, end);
+        assertThat(editable, EmojiMatcher.hasEmojiCount(1));
+        assertThat(editable, EmojiMatcher.hasEmojiAt(Emoji.EMOJI_SINGLE_CODEPOINT, start, end));
+    }
+
+    @Test
+    public void testProcess_doesNotCreateSpannable_ifNoEmoji() {
+        CharSequence processed = EmojiCompat.get().process("abc");
+        assertNotNull(processed);
+        assertThat(processed, instanceOf(String.class));
+
+        processed = EmojiCompat.get().process(new SpannedString("abc"));
+        assertNotNull(processed);
+        assertThat(processed, instanceOf(SpannedString.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_reprocess() {
+        final String string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .append(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .append(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .withPrefix().withSuffix().toString();
+
+        Spannable processed = (Spannable) EmojiCompat.get().process(string);
+        assertThat(processed, EmojiMatcher.hasEmojiCount(3));
+
+        final EmojiSpan[] spans = processed.getSpans(0, processed.length(), EmojiSpan.class);
+        final Set<EmojiSpan> spanSet = new HashSet<>();
+        Collections.addAll(spanSet, spans);
+
+        processed = (Spannable) EmojiCompat.get().process(processed);
+        assertThat(processed, EmojiMatcher.hasEmojiCount(3));
+        // new spans should be new instances
+        final EmojiSpan[] newSpans = processed.getSpans(0, processed.length(), EmojiSpan.class);
+        for (int i = 0; i < newSpans.length; i++) {
+            assertFalse(spanSet.contains(newSpans[i]));
+        }
+    }
+
+    @SuppressLint("Range")
+    @Test(expected = IllegalArgumentException.class)
+    public void testProcess_throwsException_withMaxEmojiSetToNegative() {
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                -1 /*maxEmojiCount*/);
+
+        assertThat(processed, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    public void testProcess_withMaxEmojiSetToZero() {
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                0 /*maxEmojiCount*/);
+
+        assertThat(processed, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withMaxEmojiSetToOne() {
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                1 /*maxEmojiCount*/);
+
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed, EmojiMatcher.hasEmoji(Emoji.EMOJI_SINGLE_CODEPOINT));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withMaxEmojiSetToLessThenExistingSpanCount() {
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .append(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .append(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .toString();
+
+        // add 2 spans
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(), 2);
+
+        assertThat(processed, EmojiMatcher.hasEmojiCount(2));
+
+        // use the Spannable with 2 spans, but use maxEmojiCount=1, start from the beginning of
+        // last (3rd) emoji
+        EmojiCompat.get().process(processed,
+                original.length() - Emoji.EMOJI_SINGLE_CODEPOINT.charCount(),
+                original.length(),
+                1 /*maxEmojiCount*/);
+
+        // expectation: there are still 2 emojis
+        assertThat(processed, EmojiMatcher.hasEmojiCount(2));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withMaxEmojiSet_withExistingEmojis() {
+        // test string with two emoji characters
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .append(Emoji.EMOJI_FLAG).toString();
+
+        // process and add 1 EmojiSpan, maxEmojiCount=1
+        CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                1 /*maxEmojiCount*/);
+
+        // assert that there is a single emoji
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed,
+                EmojiMatcher.hasEmojiAt(
+                        Emoji.EMOJI_SINGLE_CODEPOINT, 0, Emoji.EMOJI_SINGLE_CODEPOINT.charCount()));
+
+        // call process again with the charSequence that already has 1 span
+        processed = EmojiCompat.get().process(processed, Emoji.EMOJI_SINGLE_CODEPOINT.charCount(),
+                processed.length(), 1 /*maxEmojiCount*/);
+
+        // assert that there is still a single emoji
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed,
+                EmojiMatcher.hasEmojiAt(
+                        Emoji.EMOJI_SINGLE_CODEPOINT, 0, Emoji.EMOJI_SINGLE_CODEPOINT.charCount()));
+
+        // make the same call, this time with maxEmojiCount=2
+        processed = EmojiCompat.get().process(processed, Emoji.EMOJI_SINGLE_CODEPOINT.charCount(),
+                processed.length(), 2 /*maxEmojiCount*/);
+
+        // assert that it contains 2 emojis
+        assertThat(processed, EmojiMatcher.hasEmojiCount(2));
+        assertThat(processed,
+                EmojiMatcher.hasEmojiAt(
+                        Emoji.EMOJI_SINGLE_CODEPOINT, 0, Emoji.EMOJI_SINGLE_CODEPOINT.charCount()));
+        assertThat(processed,
+                EmojiMatcher.hasEmojiAt(Emoji.EMOJI_FLAG, Emoji.EMOJI_SINGLE_CODEPOINT.charCount(),
+                        original.length()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withReplaceNonExistent_callsGlyphChecker() {
+        final EmojiCompat.GlyphChecker glyphChecker = mock(EmojiCompat.GlyphChecker.class);
+        final EmojiCompat.Config config = TestConfigBuilder.freshConfig()
+                .setReplaceAll(true)
+                .setGlyphChecker(glyphChecker);
+        EmojiCompat.reset(config);
+
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(true);
+
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+
+        CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                Integer.MAX_VALUE /*maxEmojiCount*/, EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT);
+
+        // when function overrides config level replaceAll, a call to GlyphChecker is expected.
+        verify(glyphChecker, times(1))
+                .hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt());
+
+        // since replaceAll is false, there should be no EmojiSpans
+        assertThat(processed, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withReplaceDefault_doesNotCallGlyphChecker() {
+        final EmojiCompat.GlyphChecker glyphChecker = mock(EmojiCompat.GlyphChecker.class);
+        final EmojiCompat.Config config = TestConfigBuilder.freshConfig()
+                .setReplaceAll(true)
+                .setGlyphChecker(glyphChecker);
+        EmojiCompat.reset(config);
+
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(true);
+
+        final String original = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+        // call without replaceAll, config value (true) should be used
+        final CharSequence processed = EmojiCompat.get().process(original, 0, original.length(),
+                Integer.MAX_VALUE /*maxEmojiCount*/, EmojiCompat.REPLACE_STRATEGY_DEFAULT);
+
+        // replaceAll=true should not call hasGlyph
+        verify(glyphChecker, times(0))
+                .hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt());
+
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed, EmojiMatcher.hasEmoji(Emoji.EMOJI_SINGLE_CODEPOINT));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testProcess_withSpanned_replaceNonExistent() {
+        final EmojiCompat.GlyphChecker glyphChecker = mock(EmojiCompat.GlyphChecker.class);
+        final EmojiCompat.Config config = TestConfigBuilder.freshConfig()
+                .setReplaceAll(false)
+                .setGlyphChecker(glyphChecker);
+        EmojiCompat.reset(config);
+
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(false);
+
+        final String string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).append(
+                Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+        CharSequence processed = EmojiCompat.get().process(string, 0, string.length(),
+                Integer.MAX_VALUE, EmojiCompat.REPLACE_STRATEGY_ALL);
+
+        final SpannedString spanned = new SpannedString(processed);
+        assertThat(spanned, EmojiMatcher.hasEmojiCount(2));
+
+        // change glyphChecker to return true so that no emoji will be added
+        when(glyphChecker.hasGlyph(any(CharSequence.class), anyInt(), anyInt(), anyInt()))
+                .thenReturn(true);
+
+        processed = EmojiCompat.get().process(spanned, 0, spanned.length(),
+                Integer.MAX_VALUE, EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT);
+
+        assertThat(processed, Matchers.not(EmojiMatcher.hasEmoji()));
+
+        // start: 1 char after the first emoji (in the second emoji)
+        processed = EmojiCompat.get().process(spanned, Emoji.EMOJI_SINGLE_CODEPOINT.charCount() + 1,
+                spanned.length(), Integer.MAX_VALUE, EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT);
+
+        assertThat(processed, EmojiMatcher.hasEmojiCount(1));
+        assertThat(processed, EmojiMatcher.hasEmoji(Emoji.EMOJI_SINGLE_CODEPOINT));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testHasEmojiGlyph_withNullCharSequence() {
+        EmojiCompat.get().hasEmojiGlyph(null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testHasEmojiGlyph_withMetadataVersion_withNullCharSequence() {
+        EmojiCompat.get().hasEmojiGlyph(null, Integer.MAX_VALUE);
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testHasEmojiGlyph_pre19() {
+        String sequence = new TestString(new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE}).toString();
+        assertFalse(EmojiCompat.get().hasEmojiGlyph(sequence));
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testHasEmojiGlyph_withMetaVersion_pre19() {
+        String sequence = new TestString(new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE}).toString();
+        assertFalse(EmojiCompat.get().hasEmojiGlyph(sequence, Integer.MAX_VALUE));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testHasEmojiGlyph_returnsTrueForExistingEmoji() {
+        final String sequence = new TestString(Emoji.EMOJI_FLAG).toString();
+        assertTrue(EmojiCompat.get().hasEmojiGlyph(sequence));
+    }
+
+    @Test
+    public void testHasGlyph_returnsFalseForNonExistentEmoji() {
+        final String sequence = new TestString(Emoji.EMOJI_FLAG).append(0x1111).toString();
+        assertFalse(EmojiCompat.get().hasEmojiGlyph(sequence));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testHashEmojiGlyph_withDefaultEmojiStyles() {
+        String sequence = new TestString(new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE}).toString();
+        assertTrue(EmojiCompat.get().hasEmojiGlyph(sequence));
+
+        sequence = new TestString(
+                new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE, Emoji.CHAR_VS_EMOJI}).toString();
+        assertTrue(EmojiCompat.get().hasEmojiGlyph(sequence));
+
+        sequence = new TestString(
+                new int[]{Emoji.CHAR_DEFAULT_EMOJI_STYLE, Emoji.CHAR_VS_TEXT}).toString();
+        assertFalse(EmojiCompat.get().hasEmojiGlyph(sequence));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testHashEmojiGlyph_withMetadataVersion() {
+        final String sequence = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+        assertFalse(EmojiCompat.get().hasEmojiGlyph(sequence, 0));
+        assertTrue(EmojiCompat.get().hasEmojiGlyph(sequence, Integer.MAX_VALUE));
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testGetLoadState_returnsSuccess_pre19() {
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_SUCCEEDED);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testGetLoadState_returnsSuccessIfLoadSuccess() throws InterruptedException {
+        final TestConfigBuilder.WaitingDataLoader
+                metadataLoader = new TestConfigBuilder.WaitingDataLoader(true /*success*/);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(metadataLoader);
+        EmojiCompat.reset(config);
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_LOADING);
+
+        metadataLoader.getLoaderLatch().countDown();
+        metadataLoader.getTestLatch().await();
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_SUCCEEDED);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testGetLoadState_returnsFailIfLoadFail() throws InterruptedException {
+        final TestConfigBuilder.WaitingDataLoader
+                metadataLoader = new TestConfigBuilder.WaitingDataLoader(false/*fail*/);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(metadataLoader);
+        EmojiCompat.reset(config);
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_LOADING);
+
+        metadataLoader.getLoaderLatch().countDown();
+        metadataLoader.getTestLatch().await();
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_FAILED);
+    }
+
+    @Test
+    public void testUpdateEditorInfoAttrs_doesNotSetKeyIfNotInitialized() {
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.extras = new Bundle();
+
+        final TestConfigBuilder.WaitingDataLoader
+                metadataLoader = new TestConfigBuilder.WaitingDataLoader();
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(metadataLoader);
+        EmojiCompat.reset(config);
+
+        EmojiCompat.get().updateEditorInfoAttrs(editorInfo);
+
+        final Bundle extras = editorInfo.extras;
+        assertFalse(extras.containsKey(EmojiCompat.EDITOR_INFO_METAVERSION_KEY));
+        assertFalse(extras.containsKey(EmojiCompat.EDITOR_INFO_REPLACE_ALL_KEY));
+
+        metadataLoader.getLoaderLatch().countDown();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testLoad_throwsException_whenLoadStrategyDefault() {
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+        EmojiCompat.reset(config);
+
+        EmojiCompat.get().load();
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testLoad_pre19() {
+        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+        EmojiCompat.get().load();
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_startsLoading() {
+        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_DEFAULT, EmojiCompat.get().getLoadState());
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_onceSuccessDoesNotStartLoading() {
+        final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
+                .TestEmojiDataLoader());
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+
+        reset(loader);
+        EmojiCompat.get().load();
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.LOAD_STATE_SUCCEEDED, EmojiCompat.get().getLoadState());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_onceLoadingDoesNotStartLoading() throws InterruptedException {
+        final TestConfigBuilder.WaitingDataLoader loader = Mockito.spy(
+                new TestConfigBuilder.WaitingDataLoader(true /*success*/));
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
+                .setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
+
+        EmojiCompat.reset(config);
+
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        EmojiCompat.get().load();
+        verify(loader, times(1)).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_LOADING);
+
+        reset(loader);
+
+        EmojiCompat.get().load();
+        verify(loader, never()).load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        loader.getLoaderLatch().countDown();
+        loader.getTestLatch().await();
+
+        assertEquals(EmojiCompat.get().getLoadState(), EmojiCompat.LOAD_STATE_SUCCEEDED);
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testGetAssetSignature() {
+        final String signature = EmojiCompat.get().getAssetSignature();
+        assertTrue(signature.isEmpty());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testGetAssetSignature_api19() {
+        final String signature = EmojiCompat.get().getAssetSignature();
+        assertNotNull(signature);
+        assertFalse(signature.isEmpty());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUpdateEditorInfoAttrs_setsKeysIfInitialized() {
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.extras = new Bundle();
+        EmojiCompat.Config config = TestConfigBuilder.config().setReplaceAll(false);
+        EmojiCompat.reset(config);
+        EmojiCompat.get().updateEditorInfoAttrs(editorInfo);
+
+        final Bundle extras = editorInfo.extras;
+        assertTrue(extras.containsKey(EmojiCompat.EDITOR_INFO_METAVERSION_KEY));
+        assertTrue(extras.getInt(EmojiCompat.EDITOR_INFO_METAVERSION_KEY) > 0);
+        assertTrue(extras.containsKey(EmojiCompat.EDITOR_INFO_REPLACE_ALL_KEY));
+        assertFalse(extras.getBoolean(EmojiCompat.EDITOR_INFO_REPLACE_ALL_KEY));
+
+        config = new TestConfigBuilder.TestConfig().setReplaceAll(true);
+        EmojiCompat.reset(config);
+        EmojiCompat.get().updateEditorInfoAttrs(editorInfo);
+
+        assertTrue(extras.containsKey(EmojiCompat.EDITOR_INFO_REPLACE_ALL_KEY));
+        assertTrue(extras.getBoolean(EmojiCompat.EDITOR_INFO_REPLACE_ALL_KEY));
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testHandleDeleteSurroundingText_pre19() {
+        final TestString testString = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT);
+        final InputConnection inputConnection = mock(InputConnection.class);
+        final Editable editable = spy(new SpannableStringBuilder(testString.toString()));
+
+        Selection.setSelection(editable, testString.emojiEndIndex());
+
+        reset(editable);
+        reset(inputConnection);
+        verifyNoMoreInteractions(editable);
+        verifyNoMoreInteractions(inputConnection);
+
+        // try backwards delete 1 character
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(inputConnection, editable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 18)
+    public void testOnKeyDown_pre19() {
+        final TestString testString = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT);
+        final Editable editable = spy(new SpannableStringBuilder(testString.toString()));
+        Selection.setSelection(editable, testString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.del();
+
+        reset(editable);
+        verifyNoMoreInteractions(editable);
+
+        assertFalse(EmojiCompat.handleOnKeyDown(editable, event.getKeyCode(), event));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUseEmojiAsDefaultStyle_whenEmojiInTheMiddle() {
+        final EmojiCompat.Config config = TestConfigBuilder.config().setReplaceAll(true);
+        EmojiCompat.reset(config);
+        String s = new TestString(0x0061, Emoji.CHAR_DEFAULT_TEXT_STYLE, 0x0062).toString();
+        // no span should be added as the emoji is text style presented by default
+        assertThat(EmojiCompat.get().process(s), Matchers.not(EmojiMatcher.hasEmoji()));
+        // a span should be added when we use the emoji style presentation as default
+        EmojiCompat.reset(config.setUseEmojiAsDefaultStyle(true));
+        assertThat(EmojiCompat.get().process(s),
+                EmojiMatcher.hasEmojiAt(Emoji.DEFAULT_TEXT_STYLE, 1, 2));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUseEmojiAsDefaultStyle_whenEmojiAtTheEnd() {
+        final EmojiCompat.Config config = TestConfigBuilder.config().setReplaceAll(true);
+        EmojiCompat.reset(config);
+        String s = new TestString(0x0061, Emoji.CHAR_DEFAULT_TEXT_STYLE).toString();
+        // no span should be added as the emoji is text style presented by default
+        assertThat(EmojiCompat.get().process(s), Matchers.not(EmojiMatcher.hasEmoji()));
+        // a span should be added when we use the emoji style presentation as default
+        EmojiCompat.reset(config.setUseEmojiAsDefaultStyle(true));
+        assertThat(EmojiCompat.get().process(s),
+                EmojiMatcher.hasEmojiAt(Emoji.DEFAULT_TEXT_STYLE, 1, 2));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUseEmojiAsDefaultStyle_noEmojisAdded_whenMarkedAsException() {
+        final String s = new TestString(Emoji.CHAR_DEFAULT_TEXT_STYLE).toString();
+        final List<Integer> exceptions =
+                Arrays.asList(Emoji.CHAR_DEFAULT_TEXT_STYLE + 1, Emoji.CHAR_DEFAULT_TEXT_STYLE);
+        final EmojiCompat.Config config = TestConfigBuilder.config().setReplaceAll(true)
+                .setUseEmojiAsDefaultStyle(true, exceptions);
+        EmojiCompat.reset(config);
+        // no span should be added as the text style codepoint is marked as exception
+        assertThat(EmojiCompat.get().process(s), Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUseEmojiAsDefaultStyle_emojisAdded_whenNotMarkedAsException() {
+        final String s = new TestString(Emoji.CHAR_DEFAULT_TEXT_STYLE).toString();
+        final List<Integer> exceptions =
+                Arrays.asList(Emoji.CHAR_DEFAULT_TEXT_STYLE - 1, Emoji.CHAR_DEFAULT_TEXT_STYLE + 1);
+        final EmojiCompat.Config config = TestConfigBuilder.config().setReplaceAll(true)
+                .setUseEmojiAsDefaultStyle(true, exceptions);
+        EmojiCompat.reset(config);
+        // a span should be added as the codepoint is not included in the set of exceptions
+        assertThat(EmojiCompat.get().process(s),
+                EmojiMatcher.hasEmojiAt(Emoji.DEFAULT_TEXT_STYLE, 0, 1));
+    }
+
+    private void assertCodePointMatch(Emoji.EmojiMapping emoji) {
+        assertCodePointMatch(emoji.id(), emoji.codepoints());
+    }
+
+    private void assertCodePointMatch(int id, int[] codepoints) {
+        TestString string = new TestString(codepoints);
+        CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence,
+                EmojiMatcher.hasEmojiAt(id, string.emojiStartIndex(), string.emojiEndIndex()));
+
+        // case where Emoji is in the middle of string
+        string = new TestString(codepoints).withPrefix().withSuffix();
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence,
+                EmojiMatcher.hasEmojiAt(id, string.emojiStartIndex(), string.emojiEndIndex()));
+
+        // case where Emoji is at the end of string
+        string = new TestString(codepoints).withSuffix();
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence,
+                EmojiMatcher.hasEmojiAt(id, string.emojiStartIndex(), string.emojiEndIndex()));
+    }
+
+    private void assertCodePointDoesNotMatch(int[] codepoints) {
+        TestString string = new TestString(codepoints);
+        CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, Matchers.not(EmojiMatcher.hasEmoji()));
+
+        string = new TestString(codepoints).withSuffix().withPrefix();
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, Matchers.not(EmojiMatcher.hasEmoji()));
+
+        string = new TestString(codepoints).withPrefix();
+        charSequence = EmojiCompat.get().process(string.toString());
+        assertThat(charSequence, Matchers.not(EmojiMatcher.hasEmoji()));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
new file mode 100644
index 0000000..9b813d4
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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 org.junit.Assert.assertThat;
+
+import android.app.Instrumentation;
+import android.text.Editable;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+import androidx.emoji2.test.R;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.KeyboardUtil;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.Suppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.testutils.PollingCheck;
+
+import org.hamcrest.core.IsNot;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@Suppress
+public class EmojiKeyboardTest {
+
+    @SuppressWarnings("deprecation")
+    @Rule
+    public androidx.test.rule.ActivityTestRule<TestActivity> mActivityRule =
+            new androidx.test.rule.ActivityTestRule<>(TestActivity.class);
+    private Instrumentation mInstrumentation;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    }
+
+    @Test
+    public void testAppendWithSoftKeyboard() throws Exception {
+        TestActivity activity = mActivityRule.getActivity();
+        final EditText editText = (EditText) activity.findViewById(R.id.editText);
+        final TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix()
+                .withSuffix();
+
+        final InputConnection inputConnection = KeyboardUtil.initTextViewForSimulatedIme(
+                mInstrumentation, editText);
+        KeyboardUtil.setComposingTextInBatch(mInstrumentation, inputConnection,
+                string.toString());
+        Editable editable = editText.getEditableText();
+
+        assertThat(editable, EmojiMatcher.hasEmojiAt(Emoji.EMOJI_WITH_ZWJ, string.emojiStartIndex(),
+                string.emojiEndIndex()));
+    }
+
+    @Test
+    public void testBackDeleteWithSoftKeyboard() throws Exception {
+        TestActivity activity = mActivityRule.getActivity();
+        final EditText editText = (EditText) activity.findViewById(R.id.editText);
+        final TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix()
+                .withSuffix();
+        final InputConnection inputConnection = KeyboardUtil.initTextViewForSimulatedIme(
+                mInstrumentation, editText);
+        KeyboardUtil.setComposingTextInBatch(mInstrumentation, inputConnection, string.toString());
+
+        // assert that emoji is there
+        final Editable editable = editText.getEditableText();
+        assertThat(editable, EmojiMatcher.hasEmoji());
+
+        // put selection at the end of emoji and back delete
+        KeyboardUtil.setSelection(mInstrumentation, editText.getEditableText(),
+                string.emojiEndIndex());
+        KeyboardUtil.deleteSurroundingText(mInstrumentation, inputConnection, 1, 0);
+
+        assertThat(editable, IsNot.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    public void testForwardDeleteWithSoftKeyboard() throws Exception {
+        TestActivity activity = mActivityRule.getActivity();
+        final EditText editText = (EditText) activity.findViewById(R.id.editText);
+        final TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix()
+                .withSuffix();
+        final InputConnection inputConnection = KeyboardUtil.initTextViewForSimulatedIme(
+                mInstrumentation, editText);
+        KeyboardUtil.setComposingTextInBatch(mInstrumentation, inputConnection, string.toString());
+
+        // assert that emoji is there
+        final Editable editable = editText.getEditableText();
+        assertThat(editable, EmojiMatcher.hasEmoji());
+
+        // put selection at the beginning of emoji and forward delete
+        KeyboardUtil.setSelection(mInstrumentation, editText.getEditableText(),
+                string.emojiStartIndex());
+        KeyboardUtil.deleteSurroundingText(mInstrumentation, inputConnection, 0, 1);
+
+
+        assertThat(editable, IsNot.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    public void testBackDeleteWithHardwareKeyboard() throws Exception {
+        TestActivity activity = mActivityRule.getActivity();
+        final EditText editText = (EditText) activity.findViewById(R.id.editText);
+        final TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix()
+                .withSuffix();
+        final InputConnection inputConnection = KeyboardUtil.initTextViewForSimulatedIme(
+                mInstrumentation, editText);
+        KeyboardUtil.setComposingTextInBatch(mInstrumentation, inputConnection, string.toString());
+
+        // assert that emoji is there
+        final Editable editable = editText.getEditableText();
+        assertThat(editable, EmojiMatcher.hasEmoji());
+
+        // put selection at the end of emoji and back delete
+        KeyboardUtil.setSelection(mInstrumentation, editText.getEditableText(),
+                string.emojiEndIndex());
+        mInstrumentation.sendKeySync(KeyboardUtil.del());
+        mInstrumentation.waitForIdleSync();
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return IsNot.not(EmojiMatcher.hasEmoji()).matches(true);
+            }
+        });
+        assertThat(editable, IsNot.not(EmojiMatcher.hasEmoji()));
+    }
+
+    @Test
+    public void testForwardDeleteWithHardwareKeyboard() throws Exception {
+        TestActivity activity = mActivityRule.getActivity();
+        final EditText editText = (EditText) activity.findViewById(R.id.editText);
+        final TestString string = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix()
+                .withSuffix();
+        final InputConnection inputConnection = KeyboardUtil.initTextViewForSimulatedIme(
+                mInstrumentation, editText);
+        KeyboardUtil.setComposingTextInBatch(mInstrumentation, inputConnection, string.toString());
+
+        // assert that emoji is there
+        final Editable editable = editText.getEditableText();
+        assertThat(editable, EmojiMatcher.hasEmoji());
+
+        // put selection at the beginning of emoji and forward delete
+        KeyboardUtil.setSelection(mInstrumentation, editText.getEditableText(),
+                string.emojiStartIndex());
+        mInstrumentation.sendKeySync(KeyboardUtil.forwardDel());
+        mInstrumentation.waitForIdleSync();
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return IsNot.not(EmojiMatcher.hasEmoji()).matches(true);
+            }
+        });
+        assertThat(editable, IsNot.not(EmojiMatcher.hasEmoji()));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
new file mode 100644
index 0000000..573a8f4
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import android.app.Instrumentation;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.style.RelativeSizeSpan;
+import android.util.TypedValue;
+import android.widget.TextView;
+
+import androidx.emoji2.test.R;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiSpanInstrumentationTest {
+
+    @SuppressWarnings("deprecation")
+    @Rule
+    public androidx.test.rule.ActivityTestRule<TestActivity> mActivityRule =
+            new androidx.test.rule.ActivityTestRule<>(TestActivity.class);
+    private Instrumentation mInstrumentation;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    }
+
+    @Test
+    public void testGetSize_withRelativeSizeSpan() {
+        final TestActivity activity = mActivityRule.getActivity();
+        final TextView textView = (TextView) activity.findViewById(R.id.text);
+
+        // create a string with single codepoint emoji
+        final TestString string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT)
+                .withPrefix().withSuffix();
+        final CharSequence charSequence = EmojiCompat.get().process(string.toString());
+        assertNotNull(charSequence);
+        assertThat(charSequence, EmojiMatcher.hasEmojiCount(1));
+
+        final Spannable spanned = (Spannable) charSequence;
+        final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class);
+        final EmojiSpan span = spans[0];
+
+        // set text to the charSequence with the EmojiSpan
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                textView.setText(charSequence);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        // record height of the default span
+        final int defaultHeight = span.getHeight();
+
+        // cover the charsequence with RelativeSizeSpan which will triple the size of the
+        // characters.
+        final float multiplier = 3.0f;
+        final RelativeSizeSpan sizeSpan = new RelativeSizeSpan(multiplier);
+        spanned.setSpan(sizeSpan, 0, charSequence.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        // set the new text
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                textView.setText(charSequence);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        // record the height measured after RelativeSizeSpan
+        final int heightWithRelativeSpan = span.getHeight();
+
+        // accept 1sp error rate.
+        final float delta = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
+                mInstrumentation.getTargetContext().getResources().getDisplayMetrics());
+        assertEquals(defaultHeight * 3, heightWithRelativeSpan, delta);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
new file mode 100644
index 0000000..77694aa
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.text.TextPaint;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiSpanTest {
+
+    @Before
+    public void setup() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Test
+    public void testGetSize() {
+        final short dimensionX = 18;
+        final short dimensionY = 20;
+        final int fontHeight = 10;
+        final float expectedRatio = fontHeight * 1.0f / dimensionY;
+        final TextPaint paint = mock(TextPaint.class);
+
+        // mock TextPaint to return test font metrics
+        when(paint.getFontMetricsInt(any(FontMetricsInt.class))).thenAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                final FontMetricsInt fontMetrics = (FontMetricsInt) invocation.getArguments()[0];
+                fontMetrics.ascent = 0;
+                fontMetrics.descent = -fontHeight;
+                return null;
+            }
+        });
+
+        final EmojiMetadata metadata = mock(EmojiMetadata.class);
+        when(metadata.getWidth()).thenReturn(dimensionX);
+        when(metadata.getHeight()).thenReturn(dimensionY);
+        final EmojiSpan span = new TypefaceEmojiSpan(metadata);
+
+        final int resultSize = span.getSize(paint, "", 0, 0, null);
+        assertEquals((int) (dimensionX * expectedRatio), resultSize);
+        assertEquals(expectedRatio, span.getRatio(), 0.01f);
+        assertEquals((int) (dimensionX * expectedRatio), span.getWidth());
+        assertEquals((int) (dimensionY * expectedRatio), span.getHeight());
+    }
+
+    @Test
+    public void testBackgroundIndicator() {
+        // control the size of the emoji span
+        final EmojiMetadata metadata = mock(EmojiMetadata.class);
+        when(metadata.getWidth()).thenReturn((short) 10);
+        when(metadata.getHeight()).thenReturn((short) 10);
+
+        final EmojiSpan span = new TypefaceEmojiSpan(metadata);
+        final int spanWidth = span.getSize(mock(Paint.class), "", 0, 0, null);
+        // prepare parameters for draw() call
+        final Canvas canvas = mock(Canvas.class);
+        final float x = 10;
+        final int top = 15;
+        final int y = 20;
+        final int bottom = 30;
+
+        // verify the case where indicators are disabled
+        EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(false));
+        span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
+
+        verify(canvas, times(0)).drawRect(eq(x), eq((float) top), eq(x + spanWidth),
+                eq((float) bottom), any(Paint.class));
+
+        // verify the case where indicators are enabled
+        EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(true));
+        reset(canvas);
+        span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
+
+        verify(canvas, times(1)).drawRect(eq(x), eq((float) top), eq(x + spanWidth),
+                eq((float) bottom), any(Paint.class));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
new file mode 100644
index 0000000..92afb67
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
@@ -0,0 +1,585 @@
+/*
+ * 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 android.content.res.AssetManager.ACCESS_BUFFER;
+
+import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_NOT_FOUND;
+import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE;
+import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_MALFORMED_QUERY;
+import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_OK;
+import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_OK;
+import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.provider.FontRequest;
+import androidx.core.provider.FontsContractCompat.FontFamilyResult;
+import androidx.core.provider.FontsContractCompat.FontInfo;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class FontRequestEmojiCompatConfigTest {
+    private static final int DEFAULT_TIMEOUT_MILLIS = 3000;
+    private Context mContext;
+    private FontRequest mFontRequest;
+    private FontRequestEmojiCompatConfig.FontProviderHelper mFontProviderHelper;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mFontRequest = new FontRequest("authority", "package", "query",
+                new ArrayList<List<byte[]>>());
+        mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructor_withNullContext() {
+        new FontRequestEmojiCompatConfig(null, mFontRequest);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testConstructor_withNullFontRequest() {
+        new FontRequestEmojiCompatConfig(mContext, null);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_whenGetFontThrowsException() throws NameNotFoundException {
+        final Exception exception = new RuntimeException();
+        doThrow(exception).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
+                mFontProviderHelper);
+
+        config.getMetadataRepoLoader().load(callback);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+        verify(callback, times(1)).onFailed(same(exception));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_providerNotFound() throws NameNotFoundException {
+        doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                mFontRequest, mFontProviderHelper);
+
+        config.getMetadataRepoLoader().load(callback);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+
+        final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
+        verify(callback, times(1)).onFailed(argumentCaptor.capture());
+        assertThat(argumentCaptor.getValue().getMessage(), containsString("provider not found"));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_wrongCertificate() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_WRONG_CERTIFICATES, null /* fonts */,
+                "fetchFonts failed (" + STATUS_WRONG_CERTIFICATES + ")");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_fontNotFound() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_OK,
+                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_NOT_FOUND),
+                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_NOT_FOUND + ")");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_fontUnavailable() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_OK,
+                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_UNAVAILABLE),
+                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_UNAVAILABLE + ")");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_malformedQuery() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_OK,
+                getTestFontInfoWithInvalidPath(RESULT_CODE_MALFORMED_QUERY),
+                "fetchFonts result is not OK. (" + RESULT_CODE_MALFORMED_QUERY + ")");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_resultNotFound() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_OK, new FontInfo[] {},
+                "fetchFonts failed (empty result)");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_nullFontInfo() throws NameNotFoundException {
+        verifyLoaderOnFailedCalled(STATUS_OK, null /* fonts */,
+                "fetchFonts failed (empty result)");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_cannotLoadTypeface() throws NameNotFoundException {
+        // getTestFontInfoWithInvalidPath returns FontInfo with invalid path to file.
+        verifyLoaderOnFailedCalled(STATUS_OK,
+                getTestFontInfoWithInvalidPath(RESULT_CODE_OK),
+                "Unable to open file.");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_success() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final FontInfo[] fonts =  new FontInfo[] {
+                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_OK)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                mFontRequest, mFontProviderHelper);
+
+        config.getMetadataRepoLoader().load(callback);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+        verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_retryPolicy() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final FontInfo[] fonts =  new FontInfo[] {
+                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(-1, 1));
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+
+        config.getMetadataRepoLoader().load(callback);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+        verify(callback, never()).onLoaded(any(MetadataRepo.class));
+        verify(callback, times(1)).onFailed(any(Throwable.class));
+        verify(retryPolicy, times(1)).getRetryDelay();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_keepRetryingAndGiveUp() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final FontInfo[] fonts =  new FontInfo[] {
+                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+
+        config.getMetadataRepoLoader().load(callback);
+        retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+        verify(callback, never()).onLoaded(any(MetadataRepo.class));
+        verify(callback, never()).onFailed(any(Throwable.class));
+        verify(retryPolicy, atLeastOnce()).getRetryDelay();
+        retryPolicy.changeReturnValue(-1);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+        verify(callback, never()).onLoaded(any(MetadataRepo.class));
+        verify(callback, times(1)).onFailed(any(Throwable.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_keepRetryingAndFail() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final Uri uri = Uri.fromFile(file);
+
+        final FontInfo[] fonts = new FontInfo[] {
+                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+
+        HandlerThread thread = new HandlerThread("testThread");
+        thread.start();
+        try {
+            Handler handler = new Handler(thread.getLooper());
+
+            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                    mFontRequest, mFontProviderHelper).setHandler(handler)
+                    .setRetryPolicy(retryPolicy);
+
+            config.getMetadataRepoLoader().load(callback);
+            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+            verify(retryPolicy, atLeastOnce()).getRetryDelay();
+
+            // To avoid race condition, change the fetchFonts result on the handler thread.
+            handler.post(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        final FontInfo[] fontsSuccess = new FontInfo[] {
+                                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                                        false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
+                        };
+
+                        doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+                                mFontProviderHelper).fetchFonts(any(Context.class),
+                                any(FontRequest.class));
+                    } catch (NameNotFoundException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            });
+
+            callback.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, times(1)).onFailed(any(Throwable.class));
+        } finally {
+            thread.quit();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_keepRetryingAndSuccess() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final Uri uri = Uri.fromFile(file);
+
+        final FontInfo[] fonts = new FontInfo[]{
+                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
+
+        HandlerThread thread = new HandlerThread("testThread");
+        thread.start();
+        try {
+            Handler handler = new Handler(thread.getLooper());
+
+            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                    mFontRequest, mFontProviderHelper).setHandler(handler)
+                    .setRetryPolicy(retryPolicy);
+
+            config.getMetadataRepoLoader().load(callback);
+            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+            verify(retryPolicy, atLeastOnce()).getRetryDelay();
+
+            final FontInfo[] fontsSuccess = new FontInfo[]{
+                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                            false /* italic */, RESULT_CODE_OK)
+            };
+
+            // To avoid race condition, change the fetchFonts result on the handler thread.
+            handler.post(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+                                mFontProviderHelper).fetchFonts(any(Context.class),
+                                any(FontRequest.class));
+                    } catch (NameNotFoundException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            });
+
+            callback.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+        } finally {
+            thread.quit();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_ObserverNotifyAndSuccess() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final Uri uri = Uri.fromFile(file);
+        final FontInfo[] fonts = new FontInfo[]{
+                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
+
+        HandlerThread thread = new HandlerThread("testThread");
+        thread.start();
+        try {
+            Handler handler = new Handler(thread.getLooper());
+            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                    mFontRequest, mFontProviderHelper).setHandler(handler)
+                    .setRetryPolicy(retryPolicy);
+
+            ArgumentCaptor<ContentObserver> observerCaptor =
+                    ArgumentCaptor.forClass(ContentObserver.class);
+
+            config.getMetadataRepoLoader().load(callback);
+            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+            verify(retryPolicy, atLeastOnce()).getRetryDelay();
+            verify(mFontProviderHelper, times(1)).registerObserver(
+                    any(Context.class), eq(uri), observerCaptor.capture());
+
+            final FontInfo[] fontsSuccess = new FontInfo[]{
+                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                            false /* italic */, RESULT_CODE_OK)
+            };
+            doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+                    mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
+
+            final ContentObserver observer = observerCaptor.getValue();
+            handler.post(new Runnable() {
+                @Override
+                public void run() {
+                    observer.onChange(false /* self change */, uri);
+                }
+            });
+
+            callback.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+        } finally {
+            thread.quit();
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testLoad_ObserverNotifyAndFail() throws IOException, NameNotFoundException {
+        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
+        final Uri uri = Uri.fromFile(file);
+        final FontInfo[] fonts = new FontInfo[]{
+                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
+        };
+        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
+
+        HandlerThread thread = new HandlerThread("testThread");
+        thread.start();
+        try {
+            Handler handler = new Handler(thread.getLooper());
+            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
+                    mFontRequest, mFontProviderHelper).setHandler(handler)
+                    .setRetryPolicy(retryPolicy);
+
+            ArgumentCaptor<ContentObserver> observerCaptor =
+                    ArgumentCaptor.forClass(ContentObserver.class);
+
+            config.getMetadataRepoLoader().load(callback);
+            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, never()).onFailed(any(Throwable.class));
+            verify(retryPolicy, atLeastOnce()).getRetryDelay();
+            verify(mFontProviderHelper, times(1)).registerObserver(
+                    any(Context.class), eq(uri), observerCaptor.capture());
+
+            final FontInfo[] fontsSuccess = new FontInfo[]{
+                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
+                            false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
+            };
+            doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
+                    mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
+
+            final ContentObserver observer = observerCaptor.getValue();
+            handler.post(new Runnable() {
+                @Override
+                public void run() {
+                    observer.onChange(false /* self change */, uri);
+                }
+            });
+
+            callback.await(DEFAULT_TIMEOUT_MILLIS);
+            verify(callback, never()).onLoaded(any(MetadataRepo.class));
+            verify(callback, times(1)).onFailed(any(Throwable.class));
+        } finally {
+            thread.quit();
+        }
+    }
+
+    private void verifyLoaderOnFailedCalled(final int statusCode,
+            final FontInfo[] fonts, String exceptionMessage) throws NameNotFoundException {
+        doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontProviderHelper).fetchFonts(
+                any(Context.class), any(FontRequest.class));
+        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
+        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
+                mFontProviderHelper);
+
+        config.getMetadataRepoLoader().load(callback);
+        callback.await(DEFAULT_TIMEOUT_MILLIS);
+
+        final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
+        verify(callback, times(1)).onFailed(argumentCaptor.capture());
+        assertThat(argumentCaptor.getValue().getMessage(), containsString(exceptionMessage));
+    }
+
+    public static class WaitingRetryPolicy extends FontRequestEmojiCompatConfig.RetryPolicy {
+        private final CountDownLatch mLatch;
+        private final Object mLock = new Object();
+        @GuardedBy("mLock")
+        private long mReturnValue;
+
+        public WaitingRetryPolicy(long returnValue, int callCount) {
+            mLatch = new CountDownLatch(callCount);
+            synchronized (mLock) {
+                mReturnValue = returnValue;
+            }
+        }
+
+        @Override
+        public long getRetryDelay() {
+            mLatch.countDown();
+            synchronized (mLock) {
+                return mReturnValue;
+            }
+        }
+
+        public void changeReturnValue(long value) {
+            synchronized (mLock) {
+                mReturnValue = value;
+            }
+        }
+
+        public void await(long timeoutMillis) {
+            try {
+                mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public static class WaitingLoaderCallback extends EmojiCompat.MetadataRepoLoaderCallback {
+        final CountDownLatch mLatch;
+
+        public WaitingLoaderCallback() {
+            mLatch = new CountDownLatch(1);
+        }
+
+        @Override
+        public void onLoaded(@NonNull MetadataRepo metadataRepo) {
+            mLatch.countDown();
+        }
+
+        @Override
+        public void onFailed(@Nullable Throwable throwable) {
+            mLatch.countDown();
+        }
+
+        public void await(long timeoutMillis) {
+            try {
+                mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    public static File loadFont(Context context, String fileName) {
+        File cacheFile = new File(context.getCacheDir(), fileName);
+        try {
+            copyToCacheFile(context, fileName, cacheFile);
+            return cacheFile;
+        } catch (IOException e) {
+            fail();
+        }
+        return null;
+    }
+
+    private static void copyToCacheFile(final Context context, final String assetPath,
+            final File cacheFile) throws IOException {
+        try (InputStream is = context.getAssets().open(assetPath, ACCESS_BUFFER);
+             FileOutputStream fos = new FileOutputStream(cacheFile, false)) {
+            byte[] buffer = new byte[1024];
+            int readLen;
+            while ((readLen = is.read(buffer)) != -1) {
+                fos.write(buffer, 0, readLen);
+            }
+        }
+    }
+
+    private FontInfo[] getTestFontInfoWithInvalidPath(int resultCode) {
+        return new FontInfo[] { new FontInfo(Uri.parse("file:///some/placeholder/file"),
+                0 /* ttc index */, 400 /* weight */, false /* italic */, resultCode) };
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
new file mode 100644
index 0000000..5ea5680
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.view.KeyEvent;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.KeyboardUtil;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.core.IsNot;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class HardDeleteTest {
+
+    private TestString mTestString;
+    private Editable mEditable;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+        mEditable = new SpannableStringBuilder(mTestString.toString());
+        EmojiCompat.get().process(mEditable);
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmojiCount(1));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_whenKeyCodeIsNotDelOrForwardDel() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.zero();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withOtherModifiers() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.fnDel();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withAltModifier() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.altDel();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withCtrlModifier() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.ctrlDel();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withShiftModifier() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.shiftDel();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withSelectionLongerThanZeroLength() {
+        // when there is a selection which is longer than 0, it should not delete.
+        Selection.setSelection(mEditable, 0, mEditable.length());
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withoutEmojiSpans() {
+        final Editable editable = new SpannableStringBuilder("abc");
+        Selection.setSelection(editable, 1);
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_whenNoSpansBefore() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_deletesEmoji() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.del();
+        assertTrue(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotForwardDeleteEmoji_withNoSpansAfter() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.forwardDel();
+        assertFalse(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_forwardDeletesEmoji() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        final KeyEvent event = KeyboardUtil.forwardDel();
+        assertTrue(EmojiCompat.handleOnKeyDown(mEditable, event.getKeyCode(), event));
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_deletesEmoji_ifSelectionIsInSpanBoundaries() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex() + 1);
+        final KeyEvent delEvent = KeyboardUtil.del();
+        assertTrue(EmojiCompat.handleOnKeyDown(mEditable, delEvent.getKeyCode(), delEvent));
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_deletesEmoji_ifSelectionIsInSpanBoundaries_withForwardDel() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex() + 1);
+        final KeyEvent forwardDelEvent = KeyboardUtil.forwardDel();
+        assertTrue(EmojiCompat.handleOnKeyDown(mEditable, forwardDelEvent.getKeyCode(),
+                forwardDelEvent));
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testOnKeyDown_deletesOnlyEmojiBeforeTheCursor() {
+        // contains three emojis
+        mTestString = new TestString(Emoji.EMOJI_FLAG)
+                .append(Emoji.EMOJI_WITH_ZWJ)
+                .append(Emoji.EMOJI_GENDER)
+                .withPrefix().withSuffix();
+        mEditable = new SpannableStringBuilder(mTestString.toString());
+        EmojiCompat.get().process(mEditable);
+
+        // put the cursor after the second emoji
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex()
+                + Emoji.EMOJI_FLAG.charCount()
+                + Emoji.EMOJI_WITH_ZWJ.charCount());
+
+        // delete
+        final KeyEvent forwardDelEvent = KeyboardUtil.del();
+        assertTrue(EmojiCompat.handleOnKeyDown(mEditable, forwardDelEvent.getKeyCode(),
+                forwardDelEvent));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmojiCount(2));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_FLAG));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_GENDER));
+
+        assertEquals(new TestString(Emoji.EMOJI_FLAG).append(Emoji.EMOJI_GENDER)
+                .withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
new file mode 100644
index 0000000..118d823
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class InitCallbackTest {
+
+    @Test
+    public void testRegisterInitCallback_callsSuccessCallback() {
+        final EmojiCompat.InitCallback initCallback1 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.InitCallback initCallback2 = mock(EmojiCompat.InitCallback.class);
+
+        final EmojiCompat.Config config = TestConfigBuilder.config();
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        emojiCompat.registerInitCallback(initCallback1);
+        emojiCompat.registerInitCallback(initCallback2);
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback1, times(1)).onInitialized();
+        verify(initCallback2, times(1)).onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testRegisterInitCallback_callsFailCallback() {
+        final EmojiCompat.InitCallback initCallback1 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.InitCallback initCallback2 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.MetadataRepoLoader loader = mock(EmojiCompat.MetadataRepoLoader.class);
+        doThrow(new RuntimeException("")).when(loader)
+                .load(any(EmojiCompat.MetadataRepoLoaderCallback.class));
+
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        emojiCompat.registerInitCallback(initCallback1);
+        emojiCompat.registerInitCallback(initCallback2);
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback1, times(1)).onFailed(nullable(Throwable.class));
+        verify(initCallback2, times(1)).onFailed(nullable(Throwable.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testRegisterInitCallback_callsFailCallback_whenOnFailCalledByLoader() {
+        final EmojiCompat.InitCallback initCallback = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.MetadataRepoLoader loader = new EmojiCompat.MetadataRepoLoader() {
+            @Override
+            public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+                loaderCallback.onFailed(new RuntimeException(""));
+            }
+        };
+
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        emojiCompat.registerInitCallback(initCallback);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback, times(1)).onFailed(nullable(Throwable.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testRegisterInitCallback_callsFailCallback_whenMetadataRepoIsNull() {
+        final EmojiCompat.InitCallback initCallback = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.MetadataRepoLoader loader = new EmojiCompat.MetadataRepoLoader() {
+            @Override
+            public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+                loaderCallback.onLoaded(null);
+            }
+        };
+
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        emojiCompat.registerInitCallback(initCallback);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback, times(1)).onFailed(nullable(Throwable.class));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testUnregisterInitCallback_doesNotInteractWithCallback()
+            throws InterruptedException {
+        // will be registered
+        final EmojiCompat.InitCallback callback = mock(EmojiCompat.InitCallback.class);
+        // will be registered, and then unregistered before metadata load is complete
+        final EmojiCompat.InitCallback callbackUnregister = mock(EmojiCompat.InitCallback.class);
+        // will be registered to config
+        final EmojiCompat.InitCallback callbackConfigUnregister = mock(
+                EmojiCompat.InitCallback.class);
+        // will be registered to config and then unregistered
+        final EmojiCompat.InitCallback callbackConfig = mock(EmojiCompat.InitCallback.class);
+
+        //make sure that loader does not load before unregister
+        final TestConfigBuilder.WaitingDataLoader
+                metadataLoader = new TestConfigBuilder.WaitingDataLoader(false/*fail*/);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(metadataLoader)
+                .registerInitCallback(callbackConfig)
+                .registerInitCallback(callbackConfigUnregister)
+                .unregisterInitCallback(callbackConfigUnregister);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        // register before metadata is loaded
+        emojiCompat.registerInitCallback(callbackUnregister);
+        emojiCompat.registerInitCallback(callback);
+
+        // unregister before metadata is loaded
+        emojiCompat.unregisterInitCallback(callbackUnregister);
+
+        // fire metadata loaded event
+        metadataLoader.getLoaderLatch().countDown();
+        metadataLoader.getTestLatch().await();
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(callbackUnregister, times(0)).onFailed(any(Throwable.class));
+        verify(callbackConfigUnregister, times(0)).onFailed(nullable(Throwable.class));
+        verify(callback, times(1)).onFailed(nullable(Throwable.class));
+        verify(callbackConfig, times(1)).onFailed(nullable(Throwable.class));
+    }
+
+    @Test
+    public void testInitCallback_addedToConfigAndInstance_callsSuccess() {
+        final EmojiCompat.InitCallback initCallback1 = mock(EmojiCompat.InitCallback.class);
+        final EmojiCompat.InitCallback initCallback2 = mock(EmojiCompat.InitCallback.class);
+
+        final EmojiCompat.Config config = TestConfigBuilder.config()
+                .registerInitCallback(initCallback1);
+        final EmojiCompat emojiCompat = EmojiCompat.reset(config);
+        emojiCompat.registerInitCallback(initCallback2);
+
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        verify(initCallback1, times(1)).onInitialized();
+        verify(initCallback2, times(1)).onInitialized();
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
new file mode 100644
index 0000000..cead30d
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class MetadataRepoTest {
+
+    MetadataRepo mMetadataRepo;
+
+    @Before
+    public void clearResourceIndex() {
+        mMetadataRepo = new MetadataRepo();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testPut_withNullMetadata() {
+        mMetadataRepo.put(null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPut_withEmptyKeys() {
+        mMetadataRepo.put(new TestEmojiMetadata(new int[0]));
+    }
+
+    @Test
+    public void testPut_withSingleCodePointMapping() {
+        final int[] codePoint = new int[]{1};
+        final TestEmojiMetadata metadata = new TestEmojiMetadata(codePoint);
+        mMetadataRepo.put(metadata);
+        assertSame(metadata, getNode(codePoint));
+    }
+
+    @Test
+    public void testPut_withMultiCodePointsMapping() {
+        final int[] codePoint = new int[]{1, 2, 3, 4};
+        final TestEmojiMetadata metadata = new TestEmojiMetadata(codePoint);
+        mMetadataRepo.put(metadata);
+        assertSame(metadata, getNode(codePoint));
+
+        assertEquals(null, getNode(new int[]{1}));
+        assertEquals(null, getNode(new int[]{1, 2}));
+        assertEquals(null, getNode(new int[]{1, 2, 3}));
+        assertEquals(null, getNode(new int[]{1, 2, 3, 5}));
+    }
+
+    @Test
+    public void testPut_sequentialCodePoints() {
+        final int[] codePoint1 = new int[]{1, 2, 3, 4};
+        final EmojiMetadata metadata1 = new TestEmojiMetadata(codePoint1);
+
+        final int[] codePoint2 = new int[]{1, 2, 3};
+        final EmojiMetadata metadata2 = new TestEmojiMetadata(codePoint2);
+
+        final int[] codePoint3 = new int[]{1, 2};
+        final EmojiMetadata metadata3 = new TestEmojiMetadata(codePoint3);
+
+        mMetadataRepo.put(metadata1);
+        mMetadataRepo.put(metadata2);
+        mMetadataRepo.put(metadata3);
+
+        assertSame(metadata1, getNode(codePoint1));
+        assertSame(metadata2, getNode(codePoint2));
+        assertSame(metadata3, getNode(codePoint3));
+
+        assertEquals(null, getNode(new int[]{1}));
+        assertEquals(null, getNode(new int[]{1, 2, 3, 4, 5}));
+    }
+
+    final EmojiMetadata getNode(final int[] codepoints) {
+        return getNode(mMetadataRepo.getRootNode(), codepoints, 0);
+    }
+
+    final EmojiMetadata getNode(MetadataRepo.Node node, final int[] codepoints, int start) {
+        if (codepoints.length < start) return null;
+        if (codepoints.length == start) return node.getData();
+
+        final MetadataRepo.Node childNode = node.get(codepoints[start]);
+        if (childNode == null) return null;
+        return getNode(childNode, codepoints, start + 1);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java
new file mode 100644
index 0000000..9890903
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.view.inputmethod.InputConnection;
+
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.core.IsNot;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class SoftDeleteTest {
+    private InputConnection mInputConnection;
+    private TestString mTestString;
+    private Editable mEditable;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mInputConnection = mock(InputConnection.class);
+        mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+        mEditable = new SpannableStringBuilder(mTestString.toString());
+        EmojiCompat.get().process(mEditable);
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmojiCount(1));
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+    }
+
+    @Test
+    public void testDelete_doesNotDelete_whenSelectionIsUndefined() {
+        // no selection is set on editable
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_doesNotDelete_whenThereIsSelectionLongerThanZero() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex(),
+                mTestString.emojiEndIndex() + 1);
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withNullEditable() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, null,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withNullInputConnection() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(null, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @SuppressLint("Range")
+    @Test
+    public void testDelete_withInvalidLength() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                -1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @SuppressLint("Range")
+    @Test
+    public void testDelete_withInvalidAfterLength() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                0 /*beforeLength*/, -1 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji(Emoji.EMOJI_WITH_ZWJ));
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_backward() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        // backwards delete 1 character, it will delete the emoji
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_backward_inCodepoints() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        // backwards delete 1 character, it will delete the emoji
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, true /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_forward() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+
+        // forward delete 1 character, it will dele the emoji.
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                0 /*beforeLength*/, 1 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_forward_inCodepoints() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+
+        // forward delete 1 codepoint, it will delete the emoji.
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                0 /*beforeLength*/, 1 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals(new TestString().withPrefix().withSuffix().toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_backward_doesNotDeleteWhenSelectionAtCharSequenceStart() {
+        // make sure selection at 0 does not do something weird for backward delete
+        Selection.setSelection(mEditable, 0);
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                1 /*beforeLength*/, 0 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_forward_doesNotDeleteWhenSelectionAtCharSequenceEnd() {
+        // make sure selection at end does not do something weird for forward delete
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+
+        assertFalse(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                0 /*beforeLength*/, 1 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+        assertEquals(mTestString.toString(), mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withMultipleCharacters() {
+        // prepare string as abc[emoji]def
+        mTestString = new TestString(Emoji.EMOJI_FLAG);
+        mEditable = new SpannableStringBuilder("abc" + mTestString.toString() + "def");
+        EmojiCompat.get().process(mEditable);
+
+        // set the selection in the middle of emoji
+        Selection.setSelection(mEditable, "abc".length() + Emoji.EMOJI_FLAG.charCount() / 2);
+
+        // delete 4 characters forward, 4 character backwards
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                4 /*beforeLength*/, 4 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals("af", mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withMultipleCodepoints() {
+        // prepare string as abc[emoji]def
+        mTestString = new TestString(Emoji.EMOJI_FLAG);
+        mEditable = new SpannableStringBuilder("abc" + mTestString.toString() + "def");
+        EmojiCompat.get().process(mEditable);
+
+        // set the selection in the middle of emoji
+        Selection.setSelection(mEditable, "abc".length() + Emoji.EMOJI_FLAG.charCount() / 2);
+
+        // delete 3 codepoints forward, 3 codepoints backwards
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                3 /*beforeLength*/, 3 /*afterLength*/, true /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals("af", mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withMultipleCharacters_withDeleteLengthLongerThanString() {
+        // prepare string as abc[emoji]def
+        mTestString = new TestString(Emoji.EMOJI_FLAG);
+        mEditable = new SpannableStringBuilder("abc" + mTestString.toString() + "def");
+        EmojiCompat.get().process(mEditable);
+
+        // set the selection in the middle of emoji
+        Selection.setSelection(mEditable, "abc".length() + Emoji.EMOJI_FLAG.charCount() / 2);
+
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                100 /*beforeLength*/, 100 /*afterLength*/, false /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals("", mEditable.toString());
+    }
+
+    @Test
+    public void testDelete_withMultipleCodepoints_withDeleteLengthLongerThanString() {
+        // prepare string as abc[emoji]def
+        mTestString = new TestString(Emoji.EMOJI_FLAG);
+        mEditable = new SpannableStringBuilder("abc" + mTestString.toString() + "def");
+        EmojiCompat.get().process(mEditable);
+
+        // set the selection in the middle of emoji
+        Selection.setSelection(mEditable, "abc".length() + Emoji.EMOJI_FLAG.charCount() / 2);
+
+        assertTrue(EmojiCompat.handleDeleteSurroundingText(mInputConnection, mEditable,
+                100 /*beforeLength*/, 100 /*afterLength*/, true /*inCodePoints*/));
+
+        MatcherAssert.assertThat(mEditable, IsNot.not(EmojiMatcher.hasEmoji()));
+        assertEquals("", mEditable.toString());
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
new file mode 100644
index 0000000..bae3630
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
@@ -0,0 +1,30 @@
+/*
+ * 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.app.Activity;
+import android.os.Bundle;
+
+import androidx.emoji2.test.R;
+
+public class TestActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_default);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
new file mode 100644
index 0000000..6753840
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
@@ -0,0 +1,155 @@
+/*
+ * 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 org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.concurrent.CountDownLatch;
+
+public class TestConfigBuilder {
+    private static final String FONT_FILE = "NotoColorEmojiCompat.ttf";
+
+    private TestConfigBuilder() { }
+
+    public static EmojiCompat.Config config() {
+        return new TestConfig().setReplaceAll(true);
+    }
+
+    /**
+     * Forces the creation of Metadata instead of relying on cached metadata. If GlyphChecker is
+     * mocked, a new metadata has to be used instead of the statically cached metadata since the
+     * result of GlyphChecker on the same device might effect other tests.
+     */
+    public static EmojiCompat.Config freshConfig() {
+        return new TestConfig(new ResettingTestDataLoader()).setReplaceAll(true);
+    }
+
+    public static class TestConfig extends EmojiCompat.Config {
+        TestConfig() {
+            super(new TestEmojiDataLoader());
+        }
+
+        TestConfig(final EmojiCompat.MetadataRepoLoader metadataLoader) {
+            super(metadataLoader);
+        }
+    }
+
+    public static class WaitingDataLoader implements EmojiCompat.MetadataRepoLoader {
+        private final CountDownLatch mLoaderLatch;
+        private final CountDownLatch mTestLatch;
+        private final boolean mSuccess;
+
+        public WaitingDataLoader(boolean success) {
+            mLoaderLatch = new CountDownLatch(1);
+            mTestLatch = new CountDownLatch(1);
+            mSuccess = success;
+        }
+
+        public WaitingDataLoader() {
+            this(true);
+        }
+
+        public CountDownLatch getLoaderLatch() {
+            return mLoaderLatch;
+        }
+
+        public CountDownLatch getTestLatch() {
+            return mTestLatch;
+        }
+
+        @Override
+        public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        mLoaderLatch.await();
+                        if (mSuccess) {
+                            loaderCallback.onLoaded(new MetadataRepo());
+                        } else {
+                            loaderCallback.onFailed(null);
+                        }
+
+                        mTestLatch.countDown();
+                    } catch (Throwable e) {
+                        fail();
+                    }
+                }
+            }).start();
+        }
+    }
+
+    public static class TestEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
+        static final Object S_METADATA_REPO_LOCK = new Object();
+        // keep a static instance to in order not to slow down the tests
+        @GuardedBy("sMetadataRepoLock")
+        static volatile MetadataRepo sMetadataRepo;
+
+        TestEmojiDataLoader() {
+        }
+
+        @Override
+        public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            if (sMetadataRepo == null) {
+                synchronized (S_METADATA_REPO_LOCK) {
+                    if (sMetadataRepo == null) {
+                        try {
+                            final Context context = ApplicationProvider.getApplicationContext();
+                            final AssetManager assetManager = context.getAssets();
+                            sMetadataRepo = MetadataRepo.create(assetManager, FONT_FILE);
+                        } catch (Throwable e) {
+                            loaderCallback.onFailed(e);
+                            throw new RuntimeException(e);
+                        }
+                    }
+                }
+            }
+
+            loaderCallback.onLoaded(sMetadataRepo);
+        }
+    }
+
+    public static class ResettingTestDataLoader implements EmojiCompat.MetadataRepoLoader {
+        private MetadataRepo mMetadataRepo;
+
+        ResettingTestDataLoader() {
+        }
+
+        @Override
+        public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+            if (mMetadataRepo == null) {
+                try {
+                    final Context context = ApplicationProvider.getApplicationContext();
+                    final AssetManager assetManager = context.getAssets();
+                    mMetadataRepo = MetadataRepo.create(assetManager, FONT_FILE);
+                } catch (Throwable e) {
+                    loaderCallback.onFailed(e);
+                    throw new RuntimeException(e);
+                }
+            }
+
+            loaderCallback.onLoaded(mMetadataRepo);
+        }
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestEmojiMetadata.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestEmojiMetadata.java
new file mode 100644
index 0000000..ec94608
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestEmojiMetadata.java
@@ -0,0 +1,49 @@
+/*
+ * 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.RequiresApi;
+
+@RequiresApi(19)
+public class TestEmojiMetadata extends EmojiMetadata {
+    private final int[] mCodePoints;
+    private int mId;
+
+    TestEmojiMetadata(int[] codePoints, int id) {
+        super(null, 0);
+        mCodePoints = codePoints;
+        mId = id;
+    }
+
+    TestEmojiMetadata(int[] codePoints) {
+        this(codePoints, 0);
+    }
+
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public int getCodepointAt(int index) {
+        return mCodePoints[index];
+    }
+
+    @Override
+    public int getCodepointsLength() {
+        return mCodePoints.length;
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
new file mode 100644
index 0000000..6cc85cd
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class UninitializedStateTest {
+
+    private TestConfigBuilder.WaitingDataLoader mWaitingDataLoader;
+
+    @Before
+    public void setup() {
+        mWaitingDataLoader = new TestConfigBuilder.WaitingDataLoader(true);
+        final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(mWaitingDataLoader);
+        EmojiCompat.reset(config);
+    }
+
+    @After
+    public void after() {
+        mWaitingDataLoader.getLoaderLatch().countDown();
+        mWaitingDataLoader.getTestLatch().countDown();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testHasEmojiGlyph() {
+        EmojiCompat.get().hasEmojiGlyph("anystring");
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testHasEmojiGlyph_withMetadataVersion() {
+        EmojiCompat.get().hasEmojiGlyph("anystring", 1);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testProcess() {
+        EmojiCompat.get().process("anystring");
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testProcess_withStartEnd() {
+        EmojiCompat.get().process("anystring", 1, 2);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
new file mode 100644
index 0000000..73117a2
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
@@ -0,0 +1,110 @@
+/*
+ * 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.util;
+
+import androidx.annotation.NonNull;
+
+public class Emoji {
+
+    public static final int CHAR_KEYCAP = 0x20E3;
+    public static final int CHAR_DIGIT = 0x0039;
+    public static final int CHAR_ZWJ = 0x200D;
+    public static final int CHAR_VS_EMOJI = 0xFE0f;
+    public static final int CHAR_VS_TEXT = 0xFE0E;
+    public static final int CHAR_FITZPATRICK = 0x1F3FE;
+    public static final int CHAR_FITZPATRICK_TYPE_1 = 0x1F3fB;
+    public static final int CHAR_DEFAULT_TEXT_STYLE = 0x26F9;
+    public static final int CHAR_DEFAULT_EMOJI_STYLE = 0x1f3A2;
+    public static final int CHAR_FEMALE_SIGN = 0x2640;
+    public static final int CHAR_MAN = 0x1F468;
+    public static final int CHAR_HEART = 0x2764;
+    public static final int CHAR_KISS = 0x1F48B;
+    public static final int CHAR_REGIONAL_SYMBOL = 0x1F1E8;
+    public static final int CHAR_ASTERISK = 0x002A;
+
+    public static final EmojiMapping EMOJI_SINGLE_CODEPOINT = new EmojiMapping(
+            new int[]{CHAR_DEFAULT_EMOJI_STYLE}, 0xF01B4);
+
+    public static final EmojiMapping EMOJI_WITH_ZWJ = new EmojiMapping(
+            new int[]{CHAR_MAN, CHAR_ZWJ, CHAR_HEART, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_KISS, CHAR_ZWJ,
+                    CHAR_MAN}, 0xF051F);
+
+    public static final EmojiMapping EMOJI_GENDER = new EmojiMapping(new int[]{
+            CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+    public static final EmojiMapping EMOJI_FLAG = new EmojiMapping(
+            new int[]{CHAR_REGIONAL_SYMBOL, CHAR_REGIONAL_SYMBOL}, 0xF03A0);
+
+    public static final EmojiMapping EMOJI_GENDER_WITHOUT_VS = new EmojiMapping(
+            new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_ZWJ, CHAR_FEMALE_SIGN}, 0xF0950);
+
+    public static final EmojiMapping DEFAULT_TEXT_STYLE = new EmojiMapping(
+            new int[]{CHAR_DEFAULT_TEXT_STYLE, CHAR_VS_EMOJI}, 0xF04C6);
+
+    public static final EmojiMapping EMOJI_REGIONAL_SYMBOL = new EmojiMapping(
+            new int[]{CHAR_REGIONAL_SYMBOL}, 0xF0025);
+
+    public static final EmojiMapping EMOJI_UNKNOWN_FLAG = new EmojiMapping(
+            new int[]{0x1F1FA, 0x1F1F3}, 0xF0599);
+
+    public static final EmojiMapping EMOJI_DIGIT_ES = new EmojiMapping(
+            new int[]{CHAR_DIGIT, CHAR_VS_EMOJI}, 0xF0340);
+
+    public static final EmojiMapping EMOJI_DIGIT_KEYCAP = new EmojiMapping(
+            new int[]{CHAR_DIGIT, CHAR_KEYCAP}, 0xF0377);
+
+    public static final EmojiMapping EMOJI_DIGIT_ES_KEYCAP = new EmojiMapping(
+            new int[]{CHAR_DIGIT, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF0377);
+
+    public static final EmojiMapping EMOJI_ASTERISK_KEYCAP = new EmojiMapping(
+            new int[]{CHAR_ASTERISK, CHAR_VS_EMOJI, CHAR_KEYCAP}, 0xF051D);
+
+    public static final EmojiMapping EMOJI_SKIN_MODIFIER = new EmojiMapping(
+            new int[]{CHAR_MAN, CHAR_FITZPATRICK}, 0xF0603);
+
+    public static final EmojiMapping EMOJI_SKIN_MODIFIER_TYPE_ONE = new EmojiMapping(
+            new int[]{CHAR_MAN, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+    public static final EmojiMapping EMOJI_SKIN_MODIFIER_WITH_VS = new EmojiMapping(
+            new int[]{CHAR_MAN, CHAR_VS_EMOJI, CHAR_FITZPATRICK_TYPE_1}, 0xF0606);
+
+    public static class EmojiMapping {
+        private final int[] mCodepoints;
+        private final int mId;
+
+        private EmojiMapping(@NonNull final int[] codepoints, final int id) {
+            mCodepoints = codepoints;
+            mId = id;
+        }
+
+        public final int[] codepoints() {
+            return mCodepoints;
+        }
+
+        public final int id() {
+            return mId;
+        }
+
+        public final int charCount() {
+            int count = 0;
+            for (int i = 0; i < mCodepoints.length; i++) {
+                count += Character.charCount(mCodepoints[i]);
+            }
+            return count;
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
new file mode 100644
index 0000000..32d8e03
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
@@ -0,0 +1,257 @@
+/*
+ * 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.util;
+
+import static org.mockito.ArgumentMatchers.argThat;
+
+import android.text.Spanned;
+import android.text.TextUtils;
+
+import androidx.emoji2.text.EmojiSpan;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.mockito.ArgumentMatcher;
+
+/**
+ * Utility class that includes matchers specific to emojis and EmojiSpans.
+ */
+public class EmojiMatcher {
+
+    public static Matcher<CharSequence> hasEmojiAt(final int id, final int start,
+            final int end) {
+        return new EmojiResourceMatcher(id, start, end);
+    }
+
+    public static Matcher<CharSequence> hasEmojiAt(final Emoji.EmojiMapping emojiMapping,
+            final int start, final int end) {
+        return new EmojiResourceMatcher(emojiMapping.id(), start, end);
+    }
+
+    public static Matcher<CharSequence> hasEmojiAt(final int start, final int end) {
+        return new EmojiResourceMatcher(-1, start, end);
+    }
+
+    public static Matcher<CharSequence> hasEmoji(final int id) {
+        return new EmojiResourceMatcher(id, -1, -1);
+    }
+
+    public static Matcher<CharSequence> hasEmoji(final Emoji.EmojiMapping emojiMapping) {
+        return new EmojiResourceMatcher(emojiMapping.id(), -1, -1);
+    }
+
+    public static Matcher<CharSequence> hasEmoji() {
+        return new EmojiSpanMatcher();
+    }
+
+    public static Matcher<CharSequence> hasEmojiCount(final int count) {
+        return new EmojiCountMatcher(count);
+    }
+
+    public static <T extends CharSequence> T sameCharSequence(final T expected) {
+        return argThat(new ArgumentMatcher<T>() {
+            @Override
+            public boolean matches(T o) {
+                if (o instanceof CharSequence) {
+                    return TextUtils.equals(expected, o);
+                }
+                return false;
+            }
+
+            @Override
+            public String toString() {
+                return "doesn't match " + expected;
+            }
+        });
+    }
+
+    private static class EmojiSpanMatcher extends TypeSafeMatcher<CharSequence> {
+
+        private EmojiSpan[] mSpans;
+
+        EmojiSpanMatcher() {
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("should have EmojiSpans");
+        }
+
+        @Override
+        protected void describeMismatchSafely(final CharSequence charSequence,
+                Description mismatchDescription) {
+            mismatchDescription.appendText(" has no EmojiSpans");
+        }
+
+        @Override
+        protected boolean matchesSafely(final CharSequence charSequence) {
+            if (charSequence == null) return false;
+            if (!(charSequence instanceof Spanned)) return false;
+            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
+            return mSpans.length != 0;
+        }
+    }
+
+    private static class EmojiCountMatcher extends TypeSafeMatcher<CharSequence> {
+
+        private final int mCount;
+        private EmojiSpan[] mSpans;
+
+        EmojiCountMatcher(final int count) {
+            mCount = count;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("should have ").appendValue(mCount).appendText(" EmojiSpans");
+        }
+
+        @Override
+        protected void describeMismatchSafely(final CharSequence charSequence,
+                Description mismatchDescription) {
+            mismatchDescription.appendText(" has ");
+            if (mSpans == null) {
+                mismatchDescription.appendValue("no");
+            } else {
+                mismatchDescription.appendValue(mSpans.length);
+            }
+
+            mismatchDescription.appendText(" EmojiSpans");
+        }
+
+        @Override
+        protected boolean matchesSafely(final CharSequence charSequence) {
+            if (charSequence == null) return false;
+            if (!(charSequence instanceof Spanned)) return false;
+            mSpans = ((Spanned) charSequence).getSpans(0, charSequence.length(), EmojiSpan.class);
+            return mSpans.length == mCount;
+        }
+    }
+
+    private static class EmojiResourceMatcher extends TypeSafeMatcher<CharSequence> {
+        private static final int ERR_NONE = 0;
+        private static final int ERR_SPANNABLE_NULL = 1;
+        private static final int ERR_NO_SPANS = 2;
+        private static final int ERR_WRONG_INDEX = 3;
+        private final int mResId;
+        private final int mStart;
+        private final int mEnd;
+        private int mError = ERR_NONE;
+        private int mActualStart = -1;
+        private int mActualEnd = -1;
+
+        EmojiResourceMatcher(int resId, int start, int end) {
+            mResId = resId;
+            mStart = start;
+            mEnd = end;
+        }
+
+        @Override
+        public void describeTo(final Description description) {
+            if (mResId == -1) {
+                description.appendText("should have EmojiSpan at ")
+                        .appendValue("[" + mStart + "," + mEnd + "]");
+            } else if (mStart == -1 && mEnd == -1) {
+                description.appendText("should have EmojiSpan with resource id ")
+                        .appendValue(Integer.toHexString(mResId));
+            } else {
+                description.appendText("should have EmojiSpan with resource id ")
+                        .appendValue(Integer.toHexString(mResId))
+                        .appendText(" at ")
+                        .appendValue("[" + mStart + "," + mEnd + "]");
+            }
+        }
+
+        @Override
+        protected void describeMismatchSafely(final CharSequence charSequence,
+                Description mismatchDescription) {
+            int offset = 0;
+            mismatchDescription.appendText("[");
+            while (offset < charSequence.length()) {
+                int codepoint = Character.codePointAt(charSequence, offset);
+                mismatchDescription.appendText(Integer.toHexString(codepoint));
+                offset += Character.charCount(codepoint);
+                if (offset < charSequence.length()) {
+                    mismatchDescription.appendText(",");
+                }
+            }
+            mismatchDescription.appendText("]");
+
+            switch (mError) {
+                case ERR_NO_SPANS:
+                    mismatchDescription.appendText(" had no spans");
+                    break;
+                case ERR_SPANNABLE_NULL:
+                    mismatchDescription.appendText(" was null");
+                    break;
+                case ERR_WRONG_INDEX:
+                    mismatchDescription.appendText(" had Emoji at ")
+                            .appendValue("[" + mActualStart + "," + mActualEnd + "]");
+                    break;
+                default:
+                    mismatchDescription.appendText(" does not have an EmojiSpan with given "
+                            + "resource id ");
+            }
+        }
+
+        @Override
+        protected boolean matchesSafely(final CharSequence charSequence) {
+            if (charSequence == null) {
+                mError = ERR_SPANNABLE_NULL;
+                return false;
+            }
+
+            if (!(charSequence instanceof Spanned)) {
+                mError = ERR_NO_SPANS;
+                return false;
+            }
+
+            Spanned spanned = (Spanned) charSequence;
+            final EmojiSpan[] spans = spanned.getSpans(0, charSequence.length(), EmojiSpan.class);
+
+            if (spans.length == 0) {
+                mError = ERR_NO_SPANS;
+                return false;
+            }
+
+            if (mStart == -1 && mEnd == -1) {
+                for (int index = 0; index < spans.length; index++) {
+                    if (mResId == spans[index].getId()) {
+                        return true;
+                    }
+                }
+                return false;
+            } else {
+                for (int index = 0; index < spans.length; index++) {
+                    if (mResId == -1 || mResId == spans[index].getId()) {
+                        mActualStart = spanned.getSpanStart(spans[index]);
+                        mActualEnd = spanned.getSpanEnd(spans[index]);
+                        if (mActualStart == mStart && mActualEnd == mEnd) {
+                            return true;
+                        }
+                    }
+                }
+
+                if (mActualStart != -1 && mActualEnd != -1) {
+                    mError = ERR_WRONG_INDEX;
+                }
+
+                return false;
+            }
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
new file mode 100644
index 0000000..48a6d2e
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
@@ -0,0 +1,147 @@
+/*
+ * 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.util;
+
+import android.app.Instrumentation;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.method.QwertyKeyListener;
+import android.text.method.TextKeyListener;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Utility class for KeyEvents
+ */
+public class KeyboardUtil {
+    private static final int ALT = KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
+    private static final int CTRL = KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
+    private static final int SHIFT = KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
+    private static final int FN = KeyEvent.META_FUNCTION_ON;
+
+    public static KeyEvent zero() {
+        return keyEvent(KeyEvent.KEYCODE_0);
+    }
+
+    public static KeyEvent del() {
+        return keyEvent(KeyEvent.KEYCODE_DEL);
+    }
+
+    public static KeyEvent altDel() {
+        return keyEvent(KeyEvent.KEYCODE_DEL, ALT);
+    }
+
+    public static KeyEvent ctrlDel() {
+        return keyEvent(KeyEvent.KEYCODE_DEL, CTRL);
+    }
+
+    public static KeyEvent shiftDel() {
+        return keyEvent(KeyEvent.KEYCODE_DEL, SHIFT);
+    }
+
+    public static KeyEvent fnDel() {
+        return keyEvent(KeyEvent.KEYCODE_DEL, FN);
+    }
+
+    public static KeyEvent forwardDel() {
+        return keyEvent(KeyEvent.KEYCODE_FORWARD_DEL);
+    }
+
+    public static KeyEvent keyEvent(int keycode, int metaState) {
+        final long currentTime = System.currentTimeMillis();
+        return new KeyEvent(currentTime, currentTime, KeyEvent.ACTION_DOWN, keycode, 0, metaState);
+    }
+
+    public static KeyEvent keyEvent(int keycode) {
+        final long currentTime = System.currentTimeMillis();
+        return new KeyEvent(currentTime, currentTime, KeyEvent.ACTION_DOWN, keycode, 0);
+    }
+
+    public static void setComposingTextInBatch(final Instrumentation instrumentation,
+            final InputConnection inputConnection, final CharSequence text)
+            throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        instrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                inputConnection.beginBatchEdit();
+                inputConnection.setComposingText(text, 1);
+                inputConnection.endBatchEdit();
+                latch.countDown();
+            }
+        });
+
+        latch.await();
+        instrumentation.waitForIdleSync();
+    }
+
+    public static void deleteSurroundingText(final Instrumentation instrumentation,
+            final InputConnection inputConnection, final int before, final int after)
+            throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        instrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                inputConnection.beginBatchEdit();
+                inputConnection.deleteSurroundingText(before, after);
+                inputConnection.endBatchEdit();
+                latch.countDown();
+            }
+        });
+        latch.await();
+        instrumentation.waitForIdleSync();
+    }
+
+    public static void setSelection(Instrumentation instrumentation, final Spannable spannable,
+            final int start) throws InterruptedException {
+        setSelection(instrumentation, spannable, start, start);
+    }
+
+    public static void setSelection(Instrumentation instrumentation, final Spannable spannable,
+            final int start, final int end) throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        instrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                Selection.setSelection(spannable, start, end);
+                latch.countDown();
+            }
+        });
+        latch.await();
+        instrumentation.waitForIdleSync();
+    }
+
+    public static InputConnection initTextViewForSimulatedIme(Instrumentation instrumentation,
+            final TextView textView) throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        instrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                textView.setKeyListener(
+                        QwertyKeyListener.getInstance(false, TextKeyListener.Capitalize.NONE));
+                textView.setText("", TextView.BufferType.EDITABLE);
+                latch.countDown();
+            }
+        });
+        latch.await();
+        instrumentation.waitForIdleSync();
+        return textView.onCreateInputConnection(new EditorInfo());
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
new file mode 100644
index 0000000..83728da
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
@@ -0,0 +1,114 @@
+/*
+ * 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.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class used to create strings with emojis during tests.
+ */
+public class TestString {
+
+    private static final List<Integer> EMPTY_LIST = new ArrayList<>();
+
+    private static final String EXTRA = "ab";
+    private final List<Integer> mCodePoints;
+    private String mString;
+    private final String mValue;
+    private boolean mHasSuffix;
+    private boolean mHasPrefix;
+
+    public TestString(int... codePoints) {
+        if (codePoints.length == 0) {
+            mCodePoints = EMPTY_LIST;
+        } else {
+            mCodePoints = new ArrayList<>();
+            append(codePoints);
+        }
+        mValue = null;
+    }
+
+    public TestString(Emoji.EmojiMapping emojiMapping) {
+        this(emojiMapping.codepoints());
+    }
+
+    public TestString(String string) {
+        mCodePoints = EMPTY_LIST;
+        mValue = string;
+    }
+
+    public TestString append(int... codePoints) {
+        for (int i = 0; i < codePoints.length; i++) {
+            mCodePoints.add(codePoints[i]);
+        }
+        return this;
+    }
+
+    public TestString prepend(int... codePoints) {
+        for (int i = codePoints.length - 1; i >= 0; i--) {
+            mCodePoints.add(0, codePoints[i]);
+        }
+        return this;
+    }
+
+    public TestString append(Emoji.EmojiMapping emojiMapping) {
+        return append(emojiMapping.codepoints());
+    }
+
+    public TestString withSuffix() {
+        mHasSuffix = true;
+        return this;
+    }
+
+    public TestString withPrefix() {
+        mHasPrefix = true;
+        return this;
+    }
+
+    @SuppressWarnings("ForLoopReplaceableByForEach")
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        if (mHasPrefix) {
+            builder.append(EXTRA);
+        }
+
+        for (int index = 0; index < mCodePoints.size(); index++) {
+            builder.append(Character.toChars(mCodePoints.get(index)));
+        }
+
+        if (mValue != null) {
+            builder.append(mValue);
+        }
+
+        if (mHasSuffix) {
+            builder.append(EXTRA);
+        }
+        mString = builder.toString();
+        return mString;
+    }
+
+    public int emojiStartIndex() {
+        if (mHasPrefix) return EXTRA.length();
+        return 0;
+    }
+
+    public int emojiEndIndex() {
+        if (mHasSuffix) return mString.lastIndexOf(EXTRA);
+        return mString.length();
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java
new file mode 100644
index 0000000..2883836
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperPre19Test.java
@@ -0,0 +1,99 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.text.TextWatcher;
+import android.text.method.KeyListener;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(maxSdkVersion = 18)
+public class EmojiEditTextHelperPre19Test {
+    EmojiEditTextHelper mEmojiEditTextHelper;
+
+    @Before
+    public void setup() {
+        final EditText editText = mock(EditText.class);
+        mEmojiEditTextHelper = new EmojiEditTextHelper(editText);
+        verifyNoMoreInteractions(editText);
+    }
+
+    @Test
+    public void testGetKeyListener_returnsSameKeyListener() {
+        final KeyListener param = mock(KeyListener.class);
+        final KeyListener keyListener = mEmojiEditTextHelper.getKeyListener(
+                param);
+
+        assertSame(param, keyListener);
+    }
+
+    @LargeTest
+    @Test
+    public void testGetOnCreateInputConnection_returnsSameInputConnection() {
+        final InputConnection param = mock(InputConnection.class);
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(param,
+                new EditorInfo());
+
+        assertSame(param, inputConnection);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_withNullAttrs_returnsSameInputConnection() {
+        final InputConnection param = mock(InputConnection.class);
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(param,
+                null);
+
+        assertSame(param, inputConnection);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_withNullInputConnection_returnsNull() {
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(null,
+                new EditorInfo());
+        assertNull(inputConnection);
+    }
+
+    @Test
+    public void testDoesNotAttachTextWatcher() {
+        final EditText editText = mock(EditText.class);
+
+        mEmojiEditTextHelper = new EmojiEditTextHelper(editText);
+
+        verify(editText, times(0)).addTextChangedListener(any(TextWatcher.class));
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java
new file mode 100644
index 0000000..f995697
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextHelperTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.widget;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.text.TextWatcher;
+import android.text.method.KeyListener;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiEditTextHelperTest {
+    EmojiEditTextHelper mEmojiEditTextHelper;
+    EditText mEditText;
+
+    @Before
+    public void setup() {
+        EmojiCompat.reset(mock(EmojiCompat.class));
+        mEditText = new EditText(ApplicationProvider.getApplicationContext());
+        mEmojiEditTextHelper = new EmojiEditTextHelper(mEditText);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testGetKeyListener_withNull_throwsException() {
+        mEmojiEditTextHelper.getKeyListener(null);
+    }
+
+    @Test
+    public void testGetKeyListener_returnsEmojiKeyListener() {
+        final KeyListener keyListener = mEmojiEditTextHelper.getKeyListener(
+                mock(KeyListener.class));
+
+        assertThat(keyListener, instanceOf(EmojiKeyListener.class));
+    }
+
+    @Test
+    public void testGetKeyListener_doesNotCreateNewInstance() {
+        KeyListener mockKeyListener = mock(KeyListener.class);
+        final KeyListener keyListener1 = mEmojiEditTextHelper.getKeyListener(mockKeyListener);
+        final KeyListener keyListener2 = mEmojiEditTextHelper.getKeyListener(keyListener1);
+        assertSame(keyListener1, keyListener2);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_withNullAttrs_returnsInputConnection() {
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(
+                mock(InputConnection.class), null);
+        assertNotNull(inputConnection);
+        assertThat(inputConnection, instanceOf(EmojiInputConnection.class));
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_withNullInputConnection_returnsNull() {
+        InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(null,
+                new EditorInfo());
+        assertNull(inputConnection);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_returnsEmojiInputConnection() {
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(
+                mock(InputConnection.class), null);
+        assertNotNull(inputConnection);
+        assertThat(inputConnection, instanceOf(EmojiInputConnection.class));
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_doesNotCreateNewInstance() {
+        final InputConnection ic1 = mEmojiEditTextHelper.onCreateInputConnection(
+                mock(InputConnection.class), null);
+        final InputConnection ic2 = mEmojiEditTextHelper.onCreateInputConnection(ic1, null);
+
+        assertSame(ic1, ic2);
+    }
+
+    @Test
+    public void testAttachesTextWatcher() {
+        mEditText = mock(EditText.class);
+        mEmojiEditTextHelper = new EmojiEditTextHelper(mEditText);
+
+        final ArgumentCaptor<TextWatcher> argumentCaptor = ArgumentCaptor.forClass(
+                TextWatcher.class);
+
+        verify(mEditText, times(1)).addTextChangedListener(argumentCaptor.capture());
+        assertThat(argumentCaptor.getValue(), instanceOf(EmojiTextWatcher.class));
+    }
+
+    @Test
+    public void testSetMaxCount() {
+        mEditText = mock(EditText.class);
+        mEmojiEditTextHelper = new EmojiEditTextHelper(mEditText);
+        // capture TextWatcher
+        final ArgumentCaptor<TextWatcher> argumentCaptor = ArgumentCaptor.forClass(
+                TextWatcher.class);
+        verify(mEditText, times(1)).addTextChangedListener(argumentCaptor.capture());
+        assertThat(argumentCaptor.getValue(), instanceOf(EmojiTextWatcher.class));
+        final EmojiTextWatcher emojiTextWatcher = (EmojiTextWatcher) argumentCaptor.getValue();
+
+        mEmojiEditTextHelper.setMaxEmojiCount(1);
+
+        assertEquals(1, emojiTextWatcher.getMaxEmojiCount());
+    }
+
+    @Test
+    public void testSetEmojiReplaceStrategy() {
+        mEditText = mock(EditText.class);
+        mEmojiEditTextHelper = new EmojiEditTextHelper(mEditText);
+
+        //assert the default value
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_DEFAULT,
+                mEmojiEditTextHelper.getEmojiReplaceStrategy());
+
+        // capture TextWatcher
+        final ArgumentCaptor<TextWatcher> argumentCaptor = ArgumentCaptor.forClass(
+                TextWatcher.class);
+        verify(mEditText, times(1)).addTextChangedListener(argumentCaptor.capture());
+        assertThat(argumentCaptor.getValue(), instanceOf(EmojiTextWatcher.class));
+        final EmojiTextWatcher emojiTextWatcher = (EmojiTextWatcher) argumentCaptor.getValue();
+
+        mEmojiEditTextHelper.setEmojiReplaceStrategy(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT);
+
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT,
+                mEmojiEditTextHelper.getEmojiReplaceStrategy());
+
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT,
+                emojiTextWatcher.getEmojiReplaceStrategy());
+    }
+
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
new file mode 100644
index 0000000..9db816f
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditTextTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import android.app.Instrumentation;
+
+import androidx.emoji2.test.R;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.TestActivity;
+import androidx.emoji2.text.TestConfigBuilder;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiEditTextTest {
+
+    @SuppressWarnings("deprecation")
+    @Rule
+    public androidx.test.rule.ActivityTestRule<TestActivity> mActivityRule =
+            new androidx.test.rule.ActivityTestRule<>(TestActivity.class);
+    private Instrumentation mInstrumentation;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    }
+
+    @Test
+    public void testInflateWithMaxEmojiCount() {
+        final TestActivity activity = mActivityRule.getActivity();
+        final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
+
+        // value set in XML
+        assertEquals(5, editText.getMaxEmojiCount());
+
+        // set max emoji count
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                editText.setMaxEmojiCount(1);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertEquals(1, editText.getMaxEmojiCount());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testSetKeyListener_withNull() {
+        final TestActivity activity = mActivityRule.getActivity();
+        final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
+        editText.setKeyListener(null);
+        assertNull(editText.getKeyListener());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void testSetMaxCount() {
+        final TestActivity activity = mActivityRule.getActivity();
+        final EmojiEditText editText = activity.findViewById(R.id.editTextWithMaxCount);
+
+        // set max emoji count to 1 and set text with 2 emojis
+        mInstrumentation.runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                editText.setMaxEmojiCount(1);
+                final String string = new TestString(Emoji.EMOJI_SINGLE_CODEPOINT).append(
+                        Emoji.EMOJI_SINGLE_CODEPOINT).toString();
+                editText.setText(string);
+            }
+        });
+        mInstrumentation.waitForIdleSync();
+
+        assertThat(editText.getText(), EmojiMatcher.hasEmojiCount(1));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java
new file mode 100644
index 0000000..7addec0
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiEditableFactoryTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.widget;
+
+import static org.hamcrest.Matchers.arrayWithSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+
+import androidx.emoji2.text.EmojiMetadata;
+import androidx.emoji2.text.EmojiSpan;
+import androidx.emoji2.text.TypefaceEmojiSpan;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiEditableFactoryTest {
+
+    @Test
+    public void testGetInstance() {
+        final Editable.Factory instance = EmojiEditableFactory.getInstance();
+        assertNotNull(instance);
+
+        final Editable.Factory instance2 = EmojiEditableFactory.getInstance();
+        assertSame(instance, instance2);
+    }
+
+    @Test
+    public void testNewEditable_returnsEditable() {
+        final Editable editable = EmojiEditableFactory.getInstance().newEditable("abc");
+        assertNotNull(editable);
+        assertThat(editable, instanceOf(Editable.class));
+    }
+
+    @Test
+    public void testNewEditable_preservesCharSequenceData() {
+        final String string = "abc";
+        final SpannableString str = new SpannableString(string);
+        final EmojiMetadata metadata = mock(EmojiMetadata.class);
+        final EmojiSpan span = new TypefaceEmojiSpan(metadata);
+        str.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        final Editable editable = EmojiEditableFactory.getInstance().newEditable(str);
+        assertNotNull(editable);
+        assertEquals(string, editable.toString());
+        final EmojiSpan[] spans = editable.getSpans(0, 1, EmojiSpan.class);
+        assertThat(spans, arrayWithSize(1));
+        assertSame(spans[0], span);
+    }
+
+    @SuppressLint("PrivateApi")
+    @Test
+    public void testNewEditable_returnsEmojiSpannableIfWatcherClassExists() {
+        Class<?> clazz = null;
+        try {
+            String className = "android.text.DynamicLayout$ChangeWatcher";
+            clazz = Class.forName(className, false, getClass().getClassLoader());
+        } catch (Throwable t) {
+            // ignore
+        }
+
+        if (clazz == null) {
+            final Editable editable = EmojiEditableFactory.getInstance().newEditable("");
+            assertThat(editable, instanceOf(Editable.class));
+        } else {
+            final Editable editable = EmojiEditableFactory.getInstance().newEditable("");
+            assertThat(editable, instanceOf(SpannableBuilder.class));
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
new file mode 100644
index 0000000..2bb4ec04
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiExtractTextLayoutTest.java
@@ -0,0 +1,203 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.inputmethodservice.InputMethodService;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+
+import androidx.emoji2.R;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiExtractTextLayoutTest {
+
+    private InputMethodService mInputMethodService;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(mock(EmojiCompat.class));
+    }
+
+    @Before
+    public void setup() {
+        mInputMethodService = mock(InputMethodService.class);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testInflate() {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+
+        final EmojiExtractEditText extractEditText = layout.findViewById(
+                android.R.id.inputExtractEditText);
+        assertNotNull(extractEditText);
+
+        final ViewGroup inputExtractAccessories = layout.findViewById(
+                R.id.inputExtractAccessories);
+        assertNotNull(inputExtractAccessories);
+
+        final ExtractButtonCompat extractButton = inputExtractAccessories.findViewById(
+                R.id.inputExtractAction);
+        assertNotNull(extractButton);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testSetKeyListener_withNull() {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+
+        final EmojiExtractEditText extractEditText = layout.findViewById(
+                android.R.id.inputExtractEditText);
+        assertNotNull(extractEditText);
+
+        extractEditText.setKeyListener(null);
+        assertNull(extractEditText.getKeyListener());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testSetEmojiReplaceStrategy() {
+        final Context context = ApplicationProvider.getApplicationContext();
+
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view_with_attrs, null);
+
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT, layout.getEmojiReplaceStrategy());
+
+        final EmojiExtractEditText extractEditText = layout.findViewById(
+                android.R.id.inputExtractEditText);
+        assertNotNull(extractEditText);
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT,
+                extractEditText.getEmojiReplaceStrategy());
+
+        layout.setEmojiReplaceStrategy(EmojiCompat.REPLACE_STRATEGY_ALL);
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_ALL, layout.getEmojiReplaceStrategy());
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_ALL, extractEditText.getEmojiReplaceStrategy());
+    }
+
+    @Test
+    @UiThreadTest
+    @SdkSuppress(minSdkVersion = 19)
+    public void testSetEmojiReplaceStrategyCallEmojiCompatWithCorrectStrategy() {
+        final Context context = ApplicationProvider.getApplicationContext();
+
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view_with_attrs, null);
+
+        final EmojiExtractEditText extractEditText = layout.findViewById(
+                android.R.id.inputExtractEditText);
+        assertNotNull(layout);
+        assertNotNull(extractEditText);
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT, layout.getEmojiReplaceStrategy());
+
+        final EmojiCompat emojiCompat = mock(EmojiCompat.class);
+        when(emojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+        EmojiCompat.reset(emojiCompat);
+
+        final String testString = "anytext";
+        extractEditText.setText(testString);
+
+        verify(emojiCompat, times(1)).process(EmojiMatcher.sameCharSequence(testString),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                eq(EmojiCompat.REPLACE_STRATEGY_NON_EXISTENT));
+    }
+
+    @Test
+    @UiThreadTest
+    public void testOnUpdateExtractingViews() {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.actionLabel = "My Action Label";
+        editorInfo.imeOptions = EditorInfo.IME_ACTION_SEND;
+        editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
+
+        when(mInputMethodService.isExtractViewShown()).thenReturn(true);
+
+        final ViewGroup inputExtractAccessories = layout.findViewById(
+                R.id.inputExtractAccessories);
+        inputExtractAccessories.setVisibility(View.GONE);
+
+        final ExtractButtonCompat extractButton = inputExtractAccessories.findViewById(
+                R.id.inputExtractAction);
+
+        layout.onUpdateExtractingViews(mInputMethodService, editorInfo);
+
+        assertEquals(View.VISIBLE, inputExtractAccessories.getVisibility());
+        assertEquals(editorInfo.actionLabel, extractButton.getText());
+        assertTrue(extractButton.hasOnClickListeners());
+    }
+
+    @Test
+    @UiThreadTest
+    public void testOnUpdateExtractingViews_hidesAccessoriesIfNoAction() {
+        final Context context = ApplicationProvider.getApplicationContext();
+        final EmojiExtractTextLayout layout = (EmojiExtractTextLayout) LayoutInflater.from(context)
+                .inflate(androidx.emoji2.test.R.layout.extract_view, null);
+
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.imeOptions = EditorInfo.IME_ACTION_NONE;
+        when(mInputMethodService.isExtractViewShown()).thenReturn(true);
+
+        final ViewGroup inputExtractAccessories = layout.findViewById(
+                R.id.inputExtractAccessories);
+        final ExtractButtonCompat extractButton = inputExtractAccessories.findViewById(
+                R.id.inputExtractAction);
+
+        layout.onUpdateExtractingViews(mInputMethodService, editorInfo);
+
+        assertEquals(View.GONE, inputExtractAccessories.getVisibility());
+        assertFalse(extractButton.hasOnClickListeners());
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java
new file mode 100644
index 0000000..11d580a
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputConnectionTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Build;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.TestConfigBuilder;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiInputConnectionTest {
+
+    private InputConnection mInputConnection;
+    private TestString mTestString;
+    private Editable mEditable;
+    private EmojiInputConnection mEmojiEmojiInputConnection;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+        mEditable = new SpannableStringBuilder(mTestString.toString());
+        mInputConnection = mock(InputConnection.class);
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final TextView textView = spy(new TextView(context));
+        EmojiCompat.get().process(mEditable);
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+
+        doReturn(mEditable).when(textView).getEditableText();
+        when(mInputConnection.deleteSurroundingText(anyInt(), anyInt())).thenReturn(false);
+        setupDeleteSurroundingText();
+
+        mEmojiEmojiInputConnection = new EmojiInputConnection(textView, mInputConnection,
+                new EditorInfo());
+    }
+
+    private void setupDeleteSurroundingText() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            when(mInputConnection.deleteSurroundingTextInCodePoints(anyInt(), anyInt())).thenReturn(
+                    false);
+        }
+    }
+
+    @Test
+    public void testDeleteSurroundingText_doesNotDelete() {
+        Selection.setSelection(mEditable, 0, mEditable.length());
+        assertFalse(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
+        verify(mInputConnection, times(1)).deleteSurroundingText(1, 0);
+    }
+
+    @Test
+    public void testDeleteSurroundingText_deletesEmojiBackward() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        assertTrue(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
+        verify(mInputConnection, never()).deleteSurroundingText(anyInt(), anyInt());
+    }
+
+    @Test
+    public void testDeleteSurroundingText_doesNotDeleteEmojiIfSelectionAtStartIndex() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        assertFalse(mEmojiEmojiInputConnection.deleteSurroundingText(1, 0));
+        verify(mInputConnection, times(1)).deleteSurroundingText(1, 0);
+    }
+
+    @Test
+    public void testDeleteSurroundingText_deletesEmojiForward() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        assertTrue(mEmojiEmojiInputConnection.deleteSurroundingText(0, 1));
+        verify(mInputConnection, never()).deleteSurroundingText(anyInt(), anyInt());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    public void testDeleteSurroundingTextInCodePoints_doesNotDelete() {
+        Selection.setSelection(mEditable, 0, mEditable.length());
+        assertFalse(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
+        verify(mInputConnection, times(1)).deleteSurroundingTextInCodePoints(1, 0);
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    public void testDeleteSurroundingTextInCodePoints_deletesEmojiBackward() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        assertTrue(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
+        verify(mInputConnection, never()).deleteSurroundingTextInCodePoints(anyInt(), anyInt());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    public void testDeleteSurroundingTextInCodePoints_deletesEmojiForward() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        assertTrue(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(0, 1));
+        verify(mInputConnection, never()).deleteSurroundingTextInCodePoints(anyInt(), anyInt());
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    public void testDeleteSurroundingTextInCodePoints_doesNotDeleteEmojiIfSelectionAtStartIndex() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        assertFalse(mEmojiEmojiInputConnection.deleteSurroundingTextInCodePoints(1, 0));
+        verify(mInputConnection, times(1)).deleteSurroundingTextInCodePoints(1, 0);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java
new file mode 100644
index 0000000..dc9b977
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiInputFilterTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.widget.TextView;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiInputFilterTest {
+
+    private EmojiInputFilter mInputFilter;
+    private EmojiCompat mEmojiCompat;
+
+    @Before
+    public void setup() {
+        final TextView textView = mock(TextView.class);
+        mEmojiCompat = mock(EmojiCompat.class);
+        EmojiCompat.reset(mEmojiCompat);
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+        mInputFilter = new EmojiInputFilter(textView);
+    }
+
+    @Test
+    public void testFilter_withNullSource() {
+        assertNull(mInputFilter.filter(null, 0, 1, null, 0, 1));
+        verify(mEmojiCompat, never()).process(any(CharSequence.class));
+        verify(mEmojiCompat, never()).process(any(CharSequence.class), anyInt(), anyInt());
+    }
+
+    @Test
+    public void testFilter_withString() {
+        final String testString = "abc";
+        when(mEmojiCompat.process(any(CharSequence.class), anyInt(), anyInt()))
+                .thenReturn(new SpannableString(testString));
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        assertTrue(result instanceof Spannable);
+        verify(mEmojiCompat, times(1)).process(EmojiMatcher.sameCharSequence("a"), eq(0), eq(1));
+    }
+
+    @Test
+    public void testFilter_withSpannable() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.process(any(Spannable.class), anyInt(), anyInt())).thenReturn(testString);
+
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        assertSame(result, testString);
+        verify(mEmojiCompat, times(1)).process(
+                EmojiMatcher.sameCharSequence(testString.subSequence(0, 1)),
+                eq(0), eq(1));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoading() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_LOADING);
+
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        assertSame(result, testString);
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoadFailed() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_FAILED);
+
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(0)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        final CharSequence result = mInputFilter.filter(testString, 0, 1, null, 0, 1);
+
+        assertNotNull(result);
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java
new file mode 100644
index 0000000..3bee8fe
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiKeyListenerTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.text.Editable;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.KeyListener;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.TestConfigBuilder;
+import androidx.emoji2.util.Emoji;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.emoji2.util.KeyboardUtil;
+import androidx.emoji2.util.TestString;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiKeyListenerTest {
+
+    private KeyListener mKeyListener;
+    private TestString mTestString;
+    private Editable mEditable;
+    private EmojiKeyListener mEmojiKeyListener;
+
+    @BeforeClass
+    public static void setupEmojiCompat() {
+        EmojiCompat.reset(TestConfigBuilder.config());
+    }
+
+    @Before
+    public void setup() {
+        mKeyListener = mock(KeyListener.class);
+        mTestString = new TestString(Emoji.EMOJI_WITH_ZWJ).withPrefix().withSuffix();
+        mEditable = new SpannableStringBuilder(mTestString.toString());
+        mEmojiKeyListener = new EmojiKeyListener(mKeyListener);
+        EmojiCompat.get().process(mEditable);
+        MatcherAssert.assertThat(mEditable, EmojiMatcher.hasEmoji());
+
+        when(mKeyListener.onKeyDown(any(View.class), any(Editable.class), anyInt(),
+                any(KeyEvent.class))).thenReturn(false);
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_whenKeyCodeIsNotDelOrForwardDel() {
+        Selection.setSelection(mEditable, 0);
+        final KeyEvent event = KeyboardUtil.zero();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withOtherModifiers() {
+        Selection.setSelection(mEditable, 0);
+        final KeyEvent event = KeyboardUtil.fnDel();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withAltModifier() {
+        Selection.setSelection(mEditable, 0);
+        final KeyEvent event = KeyboardUtil.altDel();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withCtrlModifier() {
+        Selection.setSelection(mEditable, 0);
+        final KeyEvent event = KeyboardUtil.ctrlDel();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withShiftModifier() {
+        Selection.setSelection(mEditable, 0);
+        final KeyEvent event = KeyboardUtil.shiftDel();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withSelection() {
+        Selection.setSelection(mEditable, 0, mEditable.length());
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_withoutEmojiSpans() {
+        Editable editable = new SpannableStringBuilder("abc");
+        Selection.setSelection(editable, 1);
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, editable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(editable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotDelete_whenNoSpansBefore() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        final KeyEvent event = KeyboardUtil.del();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_deletesEmoji() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.del();
+        assertTrue(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verifyNoMoreInteractions(mKeyListener);
+    }
+
+    @Test
+    public void testOnKeyDown_doesNotForwardDeleteEmoji_withNoSpansAfter() {
+        Selection.setSelection(mEditable, mTestString.emojiEndIndex());
+        final KeyEvent event = KeyboardUtil.forwardDel();
+        assertFalse(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verify(mKeyListener, times(1)).onKeyDown((View) eq(null), same(mEditable),
+                eq(event.getKeyCode()), same(event));
+    }
+
+    @Test
+    public void testOnKeyDown_forwardDeletesEmoji() {
+        Selection.setSelection(mEditable, mTestString.emojiStartIndex());
+        final KeyEvent event = KeyboardUtil.forwardDel();
+        assertTrue(mEmojiKeyListener.onKeyDown(null, mEditable, event.getKeyCode(), event));
+        verifyNoMoreInteractions(mKeyListener);
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java
new file mode 100644
index 0000000..eaa6a01
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperPre19Test.java
@@ -0,0 +1,87 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import android.text.InputFilter;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(maxSdkVersion = 18)
+public class EmojiTextViewHelperPre19Test {
+    EmojiTextViewHelper mTextViewHelper;
+    TextView mTextView;
+
+    @Before
+    public void setup() {
+        mTextView = new TextView(ApplicationProvider.getApplicationContext());
+        mTextViewHelper = new EmojiTextViewHelper(mTextView);
+    }
+
+    @Test
+    public void testUpdateTransformationMethod_doesNotUpdateTransformationMethod() {
+        final TransformationMethod tm = mock(TransformationMethod.class);
+        mTextView.setTransformationMethod(tm);
+
+        mTextViewHelper.updateTransformationMethod();
+
+        assertSame(tm, mTextView.getTransformationMethod());
+    }
+
+    @Test
+    public void testGetFilters_returnsSameFilters() {
+        final InputFilter existingFilter = mock(InputFilter.class);
+        final InputFilter[] filters = new InputFilter[]{existingFilter};
+
+        final InputFilter[] newFilters = mTextViewHelper.getFilters(filters);
+
+        assertSame(filters, newFilters);
+    }
+
+    @Test
+    public void testGetTransformationMethod_returnSameTransformationMethod() {
+        assertNull(mTextViewHelper.wrapTransformationMethod(null));
+
+        final TransformationMethod tm = mock(TransformationMethod.class);
+        assertSame(tm, mTextViewHelper.wrapTransformationMethod(tm));
+    }
+
+    @Test
+    public void testSetAllCaps_doesNotUpdateTransformationMethod() {
+        final TransformationMethod tm = mock(TransformationMethod.class);
+        mTextView.setTransformationMethod(tm);
+        mTextViewHelper.setAllCaps(true);
+        assertSame(tm, mTextView.getTransformationMethod());
+
+        mTextViewHelper.setAllCaps(false);
+        assertSame(tm, mTextView.getTransformationMethod());
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java
new file mode 100644
index 0000000..d4b00e9
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextViewHelperTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.widget;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+import android.text.InputFilter;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+public class EmojiTextViewHelperTest {
+    EmojiTextViewHelper mTextViewHelper;
+    TextView mTextView;
+
+    @Before
+    public void setup() {
+        mTextView = new TextView(ApplicationProvider.getApplicationContext());
+        mTextViewHelper = new EmojiTextViewHelper(mTextView);
+    }
+
+    @Test
+    public void testUpdateTransformationMethod() {
+        mTextView.setTransformationMethod(mock(TransformationMethod.class));
+
+        mTextViewHelper.updateTransformationMethod();
+
+        assertThat(mTextView.getTransformationMethod(),
+                instanceOf(EmojiTransformationMethod.class));
+    }
+
+    @Test
+    public void testUpdateTransformationMethod_doesNotUpdateForPasswordTransformation() {
+        final PasswordTransformationMethod transformationMethod =
+                new PasswordTransformationMethod();
+        mTextView.setTransformationMethod(transformationMethod);
+
+        mTextViewHelper.updateTransformationMethod();
+
+        assertEquals(transformationMethod, mTextView.getTransformationMethod());
+    }
+
+    @Test
+    public void testUpdateTransformationMethod_doesNotCreateNewInstance() {
+        mTextView.setTransformationMethod(mock(TransformationMethod.class));
+
+        mTextViewHelper.updateTransformationMethod();
+        final TransformationMethod tm = mTextView.getTransformationMethod();
+        assertThat(tm, instanceOf(EmojiTransformationMethod.class));
+
+        // call the function again
+        mTextViewHelper.updateTransformationMethod();
+        assertSame(tm, mTextView.getTransformationMethod());
+    }
+
+    @Test
+    public void testGetFilters() {
+        final InputFilter existingFilter = mock(InputFilter.class);
+        final InputFilter[] filters = new InputFilter[]{existingFilter};
+
+        final InputFilter[] newFilters = mTextViewHelper.getFilters(filters);
+
+        assertEquals(2, newFilters.length);
+        assertThat(Arrays.asList(newFilters), hasItem(existingFilter));
+        assertNotNull(findEmojiInputFilter(newFilters));
+    }
+
+    @Test
+    public void testGetFilters_doesNotAddSecondInstance() {
+        final InputFilter existingFilter = mock(InputFilter.class);
+        final InputFilter[] filters = new InputFilter[]{existingFilter};
+
+        InputFilter[] newFilters = mTextViewHelper.getFilters(filters);
+        EmojiInputFilter emojiInputFilter = findEmojiInputFilter(newFilters);
+        assertNotNull(emojiInputFilter);
+
+        // run it again with the updated filters and see that it does not add new filter
+        newFilters = mTextViewHelper.getFilters(newFilters);
+
+        assertEquals(2, newFilters.length);
+        assertThat(Arrays.asList(newFilters), hasItem(existingFilter));
+        assertThat(Arrays.asList(newFilters), hasItem(emojiInputFilter));
+    }
+
+    private EmojiInputFilter findEmojiInputFilter(final InputFilter[] filters) {
+        for (int i = 0; i < filters.length; i++) {
+            if (filters[i] instanceof EmojiInputFilter) {
+                return (EmojiInputFilter) filters[i];
+            }
+        }
+        return null;
+    }
+
+    @Test
+    public void testWrapTransformationMethod() {
+        assertThat(mTextViewHelper.wrapTransformationMethod(null),
+                instanceOf(EmojiTransformationMethod.class));
+    }
+
+    @Test
+    public void testWrapTransformationMethod_doesNotCreateNewInstance() {
+        final TransformationMethod tm1 = mTextViewHelper.wrapTransformationMethod(null);
+        final TransformationMethod tm2 = mTextViewHelper.wrapTransformationMethod(tm1);
+        assertSame(tm1, tm2);
+    }
+
+    @Test
+    public void testSetAllCaps_withTrueSetsTransformationMethod() {
+        mTextView.setTransformationMethod(mock(TransformationMethod.class));
+        mTextViewHelper.setAllCaps(true);
+        assertThat(mTextView.getTransformationMethod(),
+                instanceOf(EmojiTransformationMethod.class));
+    }
+
+    @Test
+    public void testSetAllCaps_withFalseDoesNotSetTransformationMethod() {
+        mTextView.setTransformationMethod(null);
+        mTextViewHelper.setAllCaps(false);
+        assertNull(mTextView.getTransformationMethod());
+    }
+
+    @Test
+    public void testSetAllCaps_withPasswordTransformationDoesNotSetTransformationMethod() {
+        final PasswordTransformationMethod transformationMethod =
+                new PasswordTransformationMethod();
+        mTextView.setTransformationMethod(transformationMethod);
+        mTextViewHelper.setAllCaps(true);
+        assertSame(transformationMethod, mTextView.getTransformationMethod());
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java
new file mode 100644
index 0000000..35b5d2e
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTextWatcherTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.widget.EditText;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiTextWatcherTest {
+
+    private EmojiTextWatcher mTextWatcher;
+    private EmojiCompat mEmojiCompat;
+
+    @Before
+    public void setup() {
+        final EditText editText = mock(EditText.class);
+        mEmojiCompat = mock(EmojiCompat.class);
+        EmojiCompat.reset(mEmojiCompat);
+        mTextWatcher = new EmojiTextWatcher(editText);
+    }
+
+    @Test
+    public void testOnTextChanged_callsProcess() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(1)).process(
+                EmojiMatcher.sameCharSequence(testString),
+                eq(0),
+                eq(1),
+                eq(Integer.MAX_VALUE),
+                anyInt());
+        verify(mEmojiCompat, times(0)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testOnTextChanged_whenEmojiCompatLoading() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_LOADING);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
+                anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testOnTextChanged_whenEmojiCompatLoadFailed() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_FAILED);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
+                anyInt());
+        verify(mEmojiCompat, times(0)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testSetEmojiReplaceStrategy() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+
+        assertEquals(EmojiCompat.REPLACE_STRATEGY_DEFAULT, mTextWatcher.getEmojiReplaceStrategy());
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(1)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
+                eq(EmojiCompat.REPLACE_STRATEGY_DEFAULT));
+
+        mTextWatcher.setEmojiReplaceStrategy(EmojiCompat.REPLACE_STRATEGY_ALL);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(1)).process(any(Spannable.class), anyInt(), anyInt(), anyInt(),
+                eq(EmojiCompat.REPLACE_STRATEGY_ALL));
+    }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        final Spannable testString = new SpannableString("abc");
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        mTextWatcher.onTextChanged(testString, 0, 0, 1);
+
+        verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
+        verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java
new file mode 100644
index 0000000..31122c8
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/EmojiTransformationMethodTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.widget;
+
+import static junit.framework.TestCase.assertSame;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.method.TransformationMethod;
+import android.view.View;
+
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.util.EmojiMatcher;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EmojiTransformationMethodTest {
+
+    private EmojiTransformationMethod mTransformationMethod;
+    private TransformationMethod mWrappedTransformationMethod;
+    private View mView;
+    private EmojiCompat mEmojiCompat;
+    private final String mTestString = "abc";
+
+    @Before
+    public void setup() {
+        mEmojiCompat = mock(EmojiCompat.class);
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
+        when(mEmojiCompat.process(any(CharSequence.class))).thenAnswer(new Answer<CharSequence>() {
+            @Override
+            public CharSequence answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                return new SpannableString((String) args[0]);
+            }
+        });
+        EmojiCompat.reset(mEmojiCompat);
+
+        mView = mock(View.class);
+        when(mView.isInEditMode()).thenReturn(false);
+
+        mWrappedTransformationMethod = mock(TransformationMethod.class);
+        when(mWrappedTransformationMethod.getTransformation(any(CharSequence.class),
+                any(View.class))).thenAnswer(new Answer<CharSequence>() {
+                    @Override
+                    public CharSequence answer(InvocationOnMock invocation) {
+                        Object[] args = invocation.getArguments();
+                        return (String) args[0];
+                    }
+                });
+
+        mTransformationMethod = new EmojiTransformationMethod(mWrappedTransformationMethod);
+    }
+
+    @Test
+    public void testFilter_withNullSource() {
+        assertNull(mTransformationMethod.getTransformation(null, mView));
+        verify(mEmojiCompat, never()).process(any(CharSequence.class));
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testFilter_withNullView() {
+        mTransformationMethod.getTransformation("", null);
+    }
+
+    @Test
+    public void testFilter_withNullTransformationMethod() {
+        mTransformationMethod = new EmojiTransformationMethod(null);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+        verify(mEmojiCompat, times(1)).process(EmojiMatcher.sameCharSequence(mTestString));
+    }
+
+    @Test
+    public void testFilter() {
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertTrue(TextUtils.equals(new SpannableString(mTestString), result));
+        assertTrue(result instanceof Spannable);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                EmojiMatcher.sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, times(1)).process(EmojiMatcher.sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoading() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_LOADING);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                EmojiMatcher.sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(EmojiMatcher.sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_whenEmojiCompatLoadFailed() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_FAILED);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                EmojiMatcher.sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(EmojiMatcher.sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+
+    @Test
+    public void testFilter_withManualLoadStrategy() {
+        when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_DEFAULT);
+
+        final CharSequence result = mTransformationMethod.getTransformation(mTestString, mView);
+
+        assertSame(mTestString, result);
+        verify(mWrappedTransformationMethod, times(1)).getTransformation(
+                EmojiMatcher.sameCharSequence(mTestString), same(mView));
+        verify(mEmojiCompat, never()).process(EmojiMatcher.sameCharSequence(mTestString));
+        verify(mEmojiCompat, never()).registerInitCallback(any(EmojiCompat.InitCallback.class));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java
new file mode 100644
index 0000000..919ba54
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/widget/SpannableBuilderTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.widget;
+
+import static org.hamcrest.Matchers.arrayWithSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
+
+import android.text.Editable;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.style.QuoteSpan;
+
+import androidx.emoji2.text.EmojiSpan;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SpannableBuilderTest {
+
+    private TextWatcher mWatcher;
+    private Class<?> mClass;
+
+    @Before
+    public void setup() {
+        mWatcher = mock(TextWatcher.class, withSettings().extraInterfaces(SpanWatcher.class));
+        mClass = mWatcher.getClass();
+    }
+
+    @Test
+    public void testConstructor() {
+        new SpannableBuilder(mClass);
+
+        new SpannableBuilder(mClass, "abc");
+
+        new SpannableBuilder(mClass, "abc", 0, 3);
+
+        // test spannable copying? do I need it?
+    }
+
+    @Test
+    public void testSubSequence() {
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abc");
+        final QuoteSpan span1 = mock(QuoteSpan.class);
+        final QuoteSpan span2 = mock(QuoteSpan.class);
+        spannable.setSpan(span1, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        spannable.setSpan(span2, 2, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        final CharSequence subsequence = spannable.subSequence(0, 1);
+        assertNotNull(subsequence);
+        assertThat(subsequence, instanceOf(SpannableBuilder.class));
+
+        final QuoteSpan[] spans = spannable.getSpans(0, 1, QuoteSpan.class);
+        assertThat(spans, arrayWithSize(1));
+        assertSame(spans[0], span1);
+    }
+
+    @Test
+    public void testSetAndGetSpan() {
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abcde");
+        spannable.setSpan(mWatcher, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+        // getSpans should return the span
+        Object[] spans = spannable.getSpans(0, spannable.length(), mClass);
+        assertNotNull(spans);
+        assertThat(spans, arrayWithSize(1));
+        assertSame(mWatcher, spans[0]);
+
+        // span attributes should be correct
+        assertEquals(1, spannable.getSpanStart(mWatcher));
+        assertEquals(2, spannable.getSpanEnd(mWatcher));
+        assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spannable.getSpanFlags(mWatcher));
+
+        // should remove the span
+        spannable.removeSpan(mWatcher);
+        spans = spannable.getSpans(0, spannable.length(), QuoteSpan.class);
+        assertNotNull(spans);
+        assertThat(spans, arrayWithSize(0));
+    }
+
+    @Test
+    public void testNextSpanTransition() {
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "abcde");
+        spannable.setSpan(mWatcher, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        final int start = spannable.nextSpanTransition(0, spannable.length(), mClass);
+        assertEquals(1, start);
+    }
+
+    @Test
+    public void testBlocksSpanCallbacks_forEmojiSpans() {
+        final EmojiSpan span = mock(EmojiSpan.class);
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
+        spannable.setSpan(mWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        reset(mWatcher);
+
+        spannable.delete(0, 3);
+
+        // verify that characters are deleted
+        assertEquals("456", spannable.toString());
+        // verify EmojiSpan is deleted
+        EmojiSpan[] spans = spannable.getSpans(0, spannable.length(), EmojiSpan.class);
+        assertThat(spans, arrayWithSize(0));
+
+        // verify the call to span callbacks are blocked
+        verify((SpanWatcher) mWatcher, never()).onSpanRemoved(any(Spannable.class),
+                same(span), anyInt(), anyInt());
+        verify((SpanWatcher) mWatcher, never()).onSpanAdded(any(Spannable.class),
+                same(span), anyInt(), anyInt());
+        verify((SpanWatcher) mWatcher, never()).onSpanChanged(any(Spannable.class),
+                same(span), anyInt(), anyInt(), anyInt(), anyInt());
+
+        // verify the call to TextWatcher callbacks are called
+        verify(mWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(),
+                anyInt(), anyInt());
+        verify(mWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
+                anyInt());
+        verify(mWatcher, times(1)).afterTextChanged(any(Editable.class));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testDoesNotBlockSpanCallbacks_forNonEmojiSpans() {
+        final QuoteSpan span = mock(QuoteSpan.class);
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
+        spannable.setSpan(mWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        reset(mWatcher);
+
+        spannable.delete(0, 3);
+
+        // verify that characters are deleted
+        assertEquals("456", spannable.toString());
+        // verify QuoteSpan is deleted
+        QuoteSpan[] spans = spannable.getSpans(0, spannable.length(), QuoteSpan.class);
+        assertThat(spans, arrayWithSize(0));
+
+        // verify the call to span callbacks are not blocked
+        verify((SpanWatcher) mWatcher, times(1)).onSpanRemoved(any(Spannable.class),
+                anyObject(), anyInt(), anyInt());
+
+        // verify the call to TextWatcher callbacks are called
+        verify(mWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(), anyInt(),
+                anyInt());
+        verify(mWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
+                anyInt());
+        verify(mWatcher, times(1)).afterTextChanged(any(Editable.class));
+    }
+
+    @Test
+    public void testDoesNotBlockSpanCallbacksForOtherWatchers() {
+        final TextWatcher textWatcher = mock(TextWatcher.class);
+        final SpanWatcher spanWatcher = mock(SpanWatcher.class);
+
+        final EmojiSpan span = mock(EmojiSpan.class);
+        final SpannableBuilder spannable = new SpannableBuilder(mClass, "123456");
+        spannable.setSpan(textWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        spannable.setSpan(spanWatcher, 0, spannable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        reset(textWatcher);
+
+        spannable.delete(0, 3);
+
+        // verify that characters are deleted
+        assertEquals("456", spannable.toString());
+        // verify EmojiSpan is deleted
+        EmojiSpan[] spans = spannable.getSpans(0, spannable.length(), EmojiSpan.class);
+        assertThat(spans, arrayWithSize(0));
+
+        // verify the call to span callbacks are blocked
+        verify(spanWatcher, times(1)).onSpanRemoved(any(Spannable.class), same(span),
+                anyInt(), anyInt());
+
+        // verify the call to TextWatcher callbacks are called
+        verify(textWatcher, times(1)).beforeTextChanged(any(CharSequence.class), anyInt(),
+                anyInt(), anyInt());
+        verify(textWatcher, times(1)).onTextChanged(any(CharSequence.class), anyInt(), anyInt(),
+                anyInt());
+        verify(textWatcher, times(1)).afterTextChanged(any(Editable.class));
+    }
+}
diff --git a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
new file mode 100644
index 0000000..2d7968c
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              xmlns:app="http://schemas.android.com/apk/res-auto"
+              android:id="@+id/root"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="8sp"/>
+
+    <androidx.emoji2.widget.EmojiEditText
+        android:id="@+id/editText"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <androidx.emoji2.widget.EmojiEditText
+        android:id="@+id/editTextWithMaxCount"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:maxEmojiCount="5"/>
+
+</LinearLayout>
diff --git a/emoji2/emoji2/src/androidTest/res/layout/extract_view.xml b/emoji2/emoji2/src/androidTest/res/layout/extract_view.xml
new file mode 100644
index 0000000..b1a53fd
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/res/layout/extract_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ 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.
+  -->
+
+<androidx.emoji2.widget.EmojiExtractTextLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/emoji2/emoji2/src/androidTest/res/layout/extract_view_with_attrs.xml b/emoji2/emoji2/src/androidTest/res/layout/extract_view_with_attrs.xml
new file mode 100644
index 0000000..f91c105
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/res/layout/extract_view_with_attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ 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.
+  -->
+
+<androidx.emoji2.widget.EmojiExtractTextLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/emojiExtractTextLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    app:emojiReplaceStrategy="nonExistent"/>
\ No newline at end of file
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
new file mode 100644
index 0000000..1dc1ac4
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -0,0 +1,1335 @@
+/*
+ * 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_GROUP_PREFIX;
+
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
+import android.text.method.KeyListener;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.CheckResult;
+import androidx.annotation.ColorInt;
+import androidx.annotation.GuardedBy;
+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 androidx.annotation.VisibleForTesting;
+import androidx.collection.ArraySet;
+import androidx.core.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Main class to keep Android devices up to date with the newest emojis by adding {@link EmojiSpan}s
+ * to a given {@link CharSequence}. It is a singleton class that can be configured using a {@link
+ * EmojiCompat.Config} instance.
+ * <p/>
+ * EmojiCompat has to be initialized using {@link #init(EmojiCompat.Config)} function before it can
+ * process a {@link CharSequence}.
+ * <pre><code>EmojiCompat.init(&#47;* a config instance *&#47;);</code></pre>
+ * <p/>
+ * It is suggested to make the initialization as early as possible in your app. Please check {@link
+ * EmojiCompat.Config} for more configuration parameters. Once {@link #init(EmojiCompat.Config)} is
+ * called a singleton instance will be created. Any call after that will not create a new instance
+ * and will return immediately.
+ * <p/>
+ * During initialization information about emojis is loaded on a background thread. Before the
+ * EmojiCompat instance is initialized, calls to functions such as {@link
+ * EmojiCompat#process(CharSequence)} will throw an exception. You can use the {@link InitCallback}
+ * class to be informed about the state of initialization.
+ * <p/>
+ * After initialization the {@link #get()} function can be used to get the configured instance and
+ * the {@link #process(CharSequence)} function can be used to update a CharSequence with emoji
+ * EmojiSpans.
+ * <p/>
+ * <pre><code>CharSequence processedSequence = EmojiCompat.get().process("some string")</pre>
+ */
+@AnyThread
+public class EmojiCompat {
+    /**
+     * Key in {@link EditorInfo#extras} that represents the emoji metadata version used by the
+     * widget. The existence of the value means that the widget is using EmojiCompat.
+     * <p/>
+     * If exists, the value for the key is an {@code int} and can be used to query EmojiCompat to
+     * see whether the widget has the ability to display a certain emoji using
+     * {@link #hasEmojiGlyph(CharSequence, int)}.
+     */
+    public static final String EDITOR_INFO_METAVERSION_KEY =
+            "android.support.text.emoji.emojiCompat_metadataVersion";
+
+    /**
+     * Key in {@link EditorInfo#extras} that represents {@link
+     * EmojiCompat.Config#setReplaceAll(boolean)} configuration parameter. The key is added only if
+     * EmojiCompat is used by the widget. If exists, the value is a boolean.
+     */
+    public static final String EDITOR_INFO_REPLACE_ALL_KEY =
+            "android.support.text.emoji.emojiCompat_replaceAll";
+
+    /**
+     * EmojiCompat instance is constructed, however the initialization did not start yet.
+     *
+     * @see #getLoadState()
+     */
+    public static final int LOAD_STATE_DEFAULT = 3;
+
+    /**
+     * EmojiCompat is initializing.
+     *
+     * @see #getLoadState()
+     */
+    public static final int LOAD_STATE_LOADING = 0;
+
+    /**
+     * EmojiCompat successfully initialized.
+     *
+     * @see #getLoadState()
+     */
+    public static final int LOAD_STATE_SUCCEEDED = 1;
+
+    /**
+     * An unrecoverable error occurred during initialization of EmojiCompat. Calls to functions
+     * such as {@link #process(CharSequence)} will fail.
+     *
+     * @see #getLoadState()
+     */
+    public static final int LOAD_STATE_FAILED = 2;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @IntDef({LOAD_STATE_DEFAULT, LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LoadState {
+    }
+
+    /**
+     * Replace strategy that uses the value given in {@link EmojiCompat.Config}.
+     *
+     * @see #process(CharSequence, int, int, int, int)
+     */
+    public static final int REPLACE_STRATEGY_DEFAULT = 0;
+
+    /**
+     * Replace strategy to add {@link EmojiSpan}s for all emoji that were found.
+     *
+     * @see #process(CharSequence, int, int, int, int)
+     */
+    public static final int REPLACE_STRATEGY_ALL = 1;
+
+    /**
+     * Replace strategy to add {@link EmojiSpan}s only for emoji that do not exist in the system.
+     */
+    public static final int REPLACE_STRATEGY_NON_EXISTENT = 2;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @IntDef({REPLACE_STRATEGY_DEFAULT, REPLACE_STRATEGY_NON_EXISTENT, REPLACE_STRATEGY_ALL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ReplaceStrategy {
+    }
+
+    /**
+     * {@link EmojiCompat} will start loading metadata when {@link #init(Config)} is called.
+     *
+     * @see Config#setMetadataLoadStrategy(int)
+     */
+    public static final int LOAD_STRATEGY_DEFAULT = 0;
+
+    /**
+     * {@link EmojiCompat} will wait for {@link #load()} to be called by developer in order to
+     * start loading metadata.
+     *
+     * @see Config#setMetadataLoadStrategy(int)
+     */
+    public static final int LOAD_STRATEGY_MANUAL = 1;
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @IntDef({LOAD_STRATEGY_DEFAULT, LOAD_STRATEGY_MANUAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LoadStrategy {
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    static final int EMOJI_COUNT_UNLIMITED = Integer.MAX_VALUE;
+
+    private static final Object INSTANCE_LOCK = new Object();
+
+    @GuardedBy("INSTANCE_LOCK")
+    private static volatile EmojiCompat sInstance;
+
+    private final ReadWriteLock mInitLock;
+
+    @GuardedBy("mInitLock")
+    private final Set<InitCallback> mInitCallbacks;
+
+    @GuardedBy("mInitLock")
+    @LoadState
+    private int mLoadState;
+
+    /**
+     * Handler with main looper to run the callbacks on.
+     */
+    private final Handler mMainHandler;
+
+    /**
+     * Helper class for pre 19 compatibility.
+     */
+    private final CompatInternal mHelper;
+
+    /**
+     * Metadata loader instance given in the Config instance.
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final MetadataRepoLoader mMetadataLoader;
+
+    /**
+     * @see Config#setReplaceAll(boolean)
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final boolean mReplaceAll;
+
+    /**
+     * @see Config#setUseEmojiAsDefaultStyle(boolean)
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final boolean mUseEmojiAsDefaultStyle;
+
+    /**
+     * @see Config#setUseEmojiAsDefaultStyle(boolean, List)
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    final int[] mEmojiAsDefaultStyleExceptions;
+
+    /**
+     * @see Config#setEmojiSpanIndicatorEnabled(boolean)
+     */
+    private final boolean mEmojiSpanIndicatorEnabled;
+
+    /**
+     * @see Config#setEmojiSpanIndicatorColor(int)
+     */
+    private final int mEmojiSpanIndicatorColor;
+
+    /**
+     * @see Config#setMetadataLoadStrategy(int)
+     */
+    @LoadStrategy private final int mMetadataLoadStrategy;
+
+    /**
+     * @see Config#setGlyphChecker(GlyphChecker)
+     */
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    private final GlyphChecker mGlyphChecker;
+
+    /**
+     * Private constructor for singleton instance.
+     *
+     * @see #init(Config)
+     */
+    private EmojiCompat(@NonNull final Config config) {
+        mInitLock = new ReentrantReadWriteLock();
+        mLoadState = LOAD_STATE_DEFAULT;
+        mReplaceAll = config.mReplaceAll;
+        mUseEmojiAsDefaultStyle = config.mUseEmojiAsDefaultStyle;
+        mEmojiAsDefaultStyleExceptions = config.mEmojiAsDefaultStyleExceptions;
+        mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled;
+        mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor;
+        mMetadataLoader = config.mMetadataLoader;
+        mMetadataLoadStrategy = config.mMetadataLoadStrategy;
+        mGlyphChecker = config.mGlyphChecker;
+        mMainHandler = new Handler(Looper.getMainLooper());
+        mInitCallbacks = new ArraySet<>();
+        if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) {
+            mInitCallbacks.addAll(config.mInitCallbacks);
+        }
+        mHelper = Build.VERSION.SDK_INT < 19 ? new CompatInternal(this) : new CompatInternal19(
+                this);
+        loadMetadata();
+    }
+
+    /**
+     * Initialize the singleton instance with a configuration. When used on devices running API 18
+     * or below, the singleton instance is immediately moved into {@link #LOAD_STATE_SUCCEEDED}
+     * state without loading any metadata. When called for the first time, the library will create
+     * the singleton instance and any call after that will not create a new instance and return
+     * immediately.
+     *
+     * @see EmojiCompat.Config
+     */
+    @SuppressWarnings("GuardedBy")
+    public static EmojiCompat init(@NonNull final Config config) {
+        if (sInstance == null) {
+            synchronized (INSTANCE_LOCK) {
+                if (sInstance == null) {
+                    sInstance = new EmojiCompat(config);
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * Used by the tests to reset EmojiCompat with a new configuration. Every time it is called a
+     * new instance is created with the new configuration.
+     *
+     * @hide
+     */
+    @SuppressWarnings("GuardedBy")
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @VisibleForTesting
+    public static EmojiCompat reset(@NonNull final Config config) {
+        synchronized (INSTANCE_LOCK) {
+            sInstance = new EmojiCompat(config);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Used by the tests to reset EmojiCompat with a new singleton instance.
+     *
+     * @hide
+     */
+    @SuppressWarnings("GuardedBy")
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @VisibleForTesting
+    public static EmojiCompat reset(final EmojiCompat emojiCompat) {
+        synchronized (INSTANCE_LOCK) {
+            sInstance = emojiCompat;
+        }
+        return sInstance;
+    }
+
+    /**
+     * Return singleton EmojiCompat instance. Should be called after
+     * {@link #init(EmojiCompat.Config)} is called to initialize the singleton instance.
+     *
+     * @return EmojiCompat instance
+     *
+     * @throws IllegalStateException if called before {@link #init(EmojiCompat.Config)}
+     */
+    public static EmojiCompat get() {
+        synchronized (INSTANCE_LOCK) {
+            Preconditions.checkState(sInstance != null,
+                    "EmojiCompat is not initialized. Please call EmojiCompat.init() first");
+            return sInstance;
+        }
+    }
+
+    /**
+     * When {@link Config#setMetadataLoadStrategy(int)} is set to {@link #LOAD_STRATEGY_MANUAL},
+     * this function starts loading the metadata. Calling the function when
+     * {@link Config#setMetadataLoadStrategy(int)} is {@code not} set to
+     * {@link #LOAD_STRATEGY_MANUAL} will throw an exception. The load will {@code not} start if:
+     * <ul>
+     *     <li>the metadata is already loaded successfully and {@link #getLoadState()} is
+     *     {@link #LOAD_STATE_SUCCEEDED}.
+     *     </li>
+     *      <li>a previous load attempt is not finished yet and {@link #getLoadState()} is
+     *     {@link #LOAD_STATE_LOADING}.</li>
+     * </ul>
+     *
+     * @throws IllegalStateException when {@link Config#setMetadataLoadStrategy(int)} is not set
+     * to {@link #LOAD_STRATEGY_MANUAL}
+     */
+    public void load() {
+        Preconditions.checkState(mMetadataLoadStrategy == LOAD_STRATEGY_MANUAL,
+                "Set metadataLoadStrategy to LOAD_STRATEGY_MANUAL to execute manual loading");
+        if (isInitialized()) return;
+
+        mInitLock.writeLock().lock();
+        try {
+            if (mLoadState == LOAD_STATE_LOADING) return;
+            mLoadState = LOAD_STATE_LOADING;
+        } finally {
+            mInitLock.writeLock().unlock();
+        }
+
+        mHelper.loadMetadata();
+    }
+
+    private void loadMetadata() {
+        mInitLock.writeLock().lock();
+        try {
+            if (mMetadataLoadStrategy == LOAD_STRATEGY_DEFAULT) {
+                mLoadState = LOAD_STATE_LOADING;
+            }
+        } finally {
+            mInitLock.writeLock().unlock();
+        }
+
+        if (getLoadState() == LOAD_STATE_LOADING) {
+            mHelper.loadMetadata();
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void onMetadataLoadSuccess() {
+        final Collection<InitCallback> initCallbacks = new ArrayList<>();
+        mInitLock.writeLock().lock();
+        try {
+            mLoadState = LOAD_STATE_SUCCEEDED;
+            initCallbacks.addAll(mInitCallbacks);
+            mInitCallbacks.clear();
+        } finally {
+            mInitLock.writeLock().unlock();
+        }
+
+        mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState));
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    void onMetadataLoadFailed(@Nullable final Throwable throwable) {
+        final Collection<InitCallback> 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.
+     * <p/>
+     * 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)
+     */
+    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.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    boolean isEmojiSpanIndicatorEnabled() {
+        return mEmojiSpanIndicatorEnabled;
+    }
+
+    /**
+     * @return whether a background should be drawn for the emoji.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @ColorInt int getEmojiSpanIndicatorColor() {
+        return mEmojiSpanIndicatorColor;
+    }
+
+    /**
+     * 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.
+     * <p/>
+     * If there is a selection where selection start is not equal to selection end, does not
+     * delete.
+     * <p/>
+     * 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,
+            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.
+     * <p/>
+     * If there is a selection where selection start is not equal to selection end, does not
+     * delete.
+     * <p/>
+     * 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}.
+     *
+     * @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
+     */
+    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}.
+     *
+     * @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
+     */
+    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);
+    }
+
+    /**
+     * 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)
+     */
+    @CheckResult
+    public CharSequence process(@NonNull 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.
+        //noinspection ConstantConditions
+        @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.
+     * <p>
+     * <ul>
+     * <li>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.</li>
+     * <li>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. </li>
+     * <li>If the given input is a Spannable, the same instance is returned. </li>
+     * </ul>
+     * 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()}
+     */
+    @CheckResult
+    public CharSequence process(@NonNull 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.
+     * <p>
+     * <ul>
+     * <li>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.</li>
+     * <li>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. </li>
+     * <li>If the given input is a Spannable, the same instance is returned. </li>
+     * </ul>
+     * 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}
+     */
+    @CheckResult
+    public CharSequence process(@NonNull 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.
+     * <p>
+     * <ul>
+     * <li>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.</li>
+     * <li>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. </li>
+     * <li>If the given input is a Spannable, the same instance is returned. </li>
+     * </ul>
+     * 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}
+     */
+    @CheckResult
+    public CharSequence process(@NonNull 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
+        //noinspection ConstantConditions
+        if (charSequence == null) {
+            return charSequence;
+        }
+
+        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.
+     *
+     * @param outAttrs EditorInfo instance passed to
+     *                 {@link android.widget.TextView#onCreateInputConnection(EditorInfo)}
+     *
+     * @see #EDITOR_INFO_METAVERSION_KEY
+     * @see #EDITOR_INFO_REPLACE_ALL_KEY
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    public void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) {
+        //noinspection ConstantConditions
+        if (isInitialized() && outAttrs != null && outAttrs.extras != null) {
+            mHelper.updateEditorInfoAttrs(outAttrs);
+        }
+    }
+
+    /**
+     * Factory class that creates the EmojiSpans. By default it creates {@link TypefaceEmojiSpan}.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @RequiresApi(19)
+    static class SpanFactory {
+        /**
+         * Create EmojiSpan instance.
+         *
+         * @param metadata EmojiMetadata instance
+         *
+         * @return EmojiSpan instance
+         */
+        EmojiSpan createSpan(@NonNull final EmojiMetadata metadata) {
+            return new TypefaceEmojiSpan(metadata);
+        }
+    }
+
+    /**
+     * 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(@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
+         */
+        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.
+         *
+         * <p>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.
+         *
+         * <p>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.
+         *
+         * <p>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.
+         *
+         * <p>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.
+         *
+         * <p>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 */
+        final MetadataRepoLoader mMetadataLoader;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        boolean mReplaceAll;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        boolean mUseEmojiAsDefaultStyle;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        int[] mEmojiAsDefaultStyleExceptions;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        Set<InitCallback> 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 */
+        GlyphChecker mGlyphChecker = new EmojiProcessor.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
+         */
+        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
+         */
+        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
+         */
+        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
+         */
+        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.
+         */
+        public Config setUseEmojiAsDefaultStyle(final boolean useEmojiAsDefaultStyle,
+                @Nullable final List<Integer> 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
+         */
+        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)
+         */
+        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.
+         * <p/>
+         * 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}.
+         * <p/>
+         * 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.
+         * <pre>
+         * 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());
+         * </pre>
+         *
+         * @param strategy one of {@link EmojiCompat#LOAD_STRATEGY_DEFAULT},
+         *                  {@link EmojiCompat#LOAD_STRATEGY_MANUAL}
+         *
+         */
+        public Config setMetadataLoadStrategy(@LoadStrategy int strategy) {
+            mMetadataLoadStrategy = strategy;
+            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}.
+         */
+        protected final MetadataRepoLoader getMetadataRepoLoader() {
+            return mMetadataLoader;
+        }
+    }
+
+    /**
+     * Runnable to call success/failure case for the listeners.
+     */
+    private static class ListenerDispatcher implements Runnable {
+        private final List<InitCallback> 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<InitCallback> initCallbacks,
+                @LoadState final int loadState) {
+            this(initCallbacks, loadState, null);
+        }
+
+        ListenerDispatcher(@NonNull final Collection<InitCallback> 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;
+        }
+
+        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 "";
+        }
+    }
+
+    @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,
+                    new SpanFactory(),
+                    mEmojiCompat.mGlyphChecker,
+                    mEmojiCompat.mUseEmojiAsDefaultStyle,
+                    mEmojiCompat.mEmojiAsDefaultStyleExceptions);
+
+            mEmojiCompat.onMetadataLoadSuccess();
+        }
+
+        @Override
+        boolean hasEmojiGlyph(@NonNull CharSequence sequence) {
+            return mProcessor.getEmojiMetadata(sequence) != null;
+        }
+
+        @Override
+        boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) {
+            final EmojiMetadata emojiMetadata = mProcessor.getEmojiMetadata(sequence);
+            return emojiMetadata != null && emojiMetadata.getCompatAdded() <= metadataVersion;
+        }
+
+        @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/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
new file mode 100644
index 0000000..ba46f82
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiMetadata.java
@@ -0,0 +1,235 @@
+/*
+ * 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_GROUP_PREFIX;
+
+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.text.emoji.flatbuffer.MetadataItem;
+import androidx.text.emoji.flatbuffer.MetadataList;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Information about a single emoji.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@AnyThread
+@RequiresApi(19)
+public class EmojiMetadata {
+    /**
+     * Defines whether the system can render the emoji.
+     */
+    @IntDef({HAS_GLYPH_UNKNOWN, HAS_GLYPH_ABSENT, HAS_GLYPH_EXISTS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface HasGlyph {
+    }
+
+    /**
+     * Not calculated on device yet.
+     */
+    public static final int HAS_GLYPH_UNKNOWN = 0;
+
+    /**
+     * Device cannot render the emoji.
+     */
+    public static final int HAS_GLYPH_ABSENT = 1;
+
+    /**
+     * Device can render the emoji.
+     */
+    public static final int HAS_GLYPH_EXISTS = 2;
+
+    /**
+     * @see #getMetadataItem()
+     */
+    private static final ThreadLocal<MetadataItem> sMetadataItem = new ThreadLocal<>();
+
+    /**
+     * Index of the EmojiMetadata in {@link MetadataList}.
+     */
+    private final int mIndex;
+
+    /**
+     * MetadataRepo that holds this instance.
+     */
+    private final MetadataRepo mMetadataRepo;
+
+    /**
+     * Whether the system can render the emoji. Calculated at runtime on the device.
+     */
+    @HasGlyph
+    private volatile int mHasGlyph = HAS_GLYPH_UNKNOWN;
+
+    EmojiMetadata(@NonNull final MetadataRepo metadataRepo, @IntRange(from = 0) final int index) {
+        mMetadataRepo = metadataRepo;
+        mIndex = index;
+    }
+
+    /**
+     * Draws the emoji represented by this EmojiMetadata 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 this metadata
+     */
+    public Typeface getTypeface() {
+        return mMetadataRepo.getTypeface();
+    }
+
+    /**
+     * @return a ThreadLocal instance of MetadataItem for this EmojiMetadata
+     */
+    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 EmojiMetadata, 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 EmojiMetadata classes in the same thread.
+        mMetadataRepo.getMetadataList().list(result, mIndex);
+        return result;
+    }
+
+    /**
+     * @return unique id for the emoji
+     */
+    public int getId() {
+        return getMetadataItem().id();
+    }
+
+    /**
+     * @return width of the emoji image
+     */
+    public short getWidth() {
+        return getMetadataItem().width();
+    }
+
+    /**
+     * @return height of the emoji image
+     */
+    public short getHeight() {
+        return getMetadataItem().height();
+    }
+
+    /**
+     * @return in which metadata version the emoji was added to metadata
+     */
+    public short getCompatAdded() {
+        return getMetadataItem().compatAdded();
+    }
+
+    /**
+     * @return first SDK that the support for this emoji was added
+     */
+    public short getSdkAdded() {
+        return getMetadataItem().sdkAdded();
+    }
+
+    /**
+     * @return whether the emoji is in Emoji Presentation by default (without emoji
+     * style selector 0xFE0F)
+     */
+    @HasGlyph
+    public int getHasGlyph() {
+        return mHasGlyph;
+    }
+
+    /**
+     * Set whether the system can render the emoji.
+     *
+     * @param hasGlyph {@code true} if system can render the emoji
+     */
+    public void setHasGlyph(boolean hasGlyph) {
+        mHasGlyph = hasGlyph ? HAS_GLYPH_EXISTS : HAS_GLYPH_ABSENT;
+    }
+
+    /**
+     * @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();
+    }
+
+    @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/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
new file mode 100644
index 0000000..d8c4765
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiProcessor.java
@@ -0,0 +1,834 @@
+/*
+ * 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_GROUP_PREFIX;
+
+import android.os.Build;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+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 androidx.core.graphics.PaintCompat;
+import androidx.emoji2.widget.SpannableBuilder;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Processes the CharSequence and adds the emojis.
+ *
+ * @hide
+ */
+@AnyThread
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiProcessor {
+
+    /**
+     * State transition commands.
+     */
+    @IntDef({ACTION_ADVANCE_BOTH, ACTION_ADVANCE_END, ACTION_FLUSH})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface Action {
+    }
+
+    /**
+     * 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;
+
+    /**
+     * Factory used to create EmojiSpans.
+     */
+    private final EmojiCompat.SpanFactory mSpanFactory;
+
+    /**
+     * Emoji metadata repository.
+     */
+    private final MetadataRepo mMetadataRepo;
+
+    /**
+     * Utility class that checks if the system can render a given glyph.
+     */
+    private EmojiCompat.GlyphChecker mGlyphChecker;
+
+    /**
+     * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean)
+     */
+    private final boolean mUseEmojiAsDefaultStyle;
+
+    /**
+     * @see EmojiCompat.Config#setUseEmojiAsDefaultStyle(boolean, List)
+     */
+    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
+    ) {
+        mSpanFactory = spanFactory;
+        mMetadataRepo = metadataRepo;
+        mGlyphChecker = glyphChecker;
+        mUseEmojiAsDefaultStyle = useEmojiAsDefaultStyle;
+        mEmojiAsDefaultStyleExceptions = emojiAsDefaultStyleExceptions;
+    }
+
+    EmojiMetadata getEmojiMetadata(@NonNull final CharSequence charSequence) {
+        final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+                mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
+        final int end = charSequence.length();
+        int currentOffset = 0;
+
+        while (currentOffset < end) {
+            final int codePoint = Character.codePointAt(charSequence, currentOffset);
+            final int action = sm.check(codePoint);
+            if (action != ACTION_ADVANCE_END) {
+                return null;
+            }
+            currentOffset += Character.charCount(codePoint);
+        }
+
+        if (sm.isInFlushableState()) {
+            return sm.getCurrentMetadata();
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found.
+     * <p>
+     * <ul>
+     * <li>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.</li>
+     * <li>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. </li>
+     * <li>If the given input is a Spannable, the same instance is returned. </li>
+     * </ul>
+     *
+     * @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 {
+            Spannable 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 = (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 SpannableString(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;
+            }
+            // add new ones
+            int addedCount = 0;
+            final ProcessorSm sm = new ProcessorSm(mMetadataRepo.getRootNode(),
+                    mUseEmojiAsDefaultStyle, mEmojiAsDefaultStyleExceptions);
+
+            int currentOffset = start;
+            int codePoint = Character.codePointAt(charSequence, currentOffset);
+
+            while (currentOffset < end && addedCount < maxEmojiCount) {
+                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 (replaceAll || !hasGlyph(charSequence, start, currentOffset,
+                                sm.getFlushMetadata())) {
+                            if (spannable == null) {
+                                spannable = new SpannableString(charSequence);
+                            }
+                            addEmoji(spannable, sm.getFlushMetadata(), start, currentOffset);
+                            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) {
+                if (replaceAll || !hasGlyph(charSequence, start, currentOffset,
+                        sm.getCurrentMetadata())) {
+                    if (spannable == null) {
+                        spannable = new SpannableString(charSequence);
+                    }
+                    addEmoji(spannable, sm.getCurrentMetadata(), start, currentOffset);
+                    addedCount++;
+                }
+            }
+            return spannable == null ? charSequence : spannable;
+        } finally {
+            if (isSpannableBuilder) {
+                ((SpannableBuilder) charSequence).endBatchEdit();
+            }
+        }
+    }
+
+    /**
+     * 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.
+     * <p/>
+     * 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,
+            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(final Editable content, 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.
+     * <p/>
+     * 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(KeyEvent event) {
+        return !KeyEvent.metaStateHasNoModifiers(event.getMetaState());
+    }
+
+    private void addEmoji(@NonNull final Spannable spannable, final EmojiMetadata metadata,
+            final int start, final int end) {
+        final EmojiSpan span = mSpanFactory.createSpan(metadata);
+        spannable.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+    }
+
+    /**
+     * 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 metadata EmojiMetadata 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 EmojiMetadata metadata) {
+        // if the existence is not calculated yet
+        if (metadata.getHasGlyph() == EmojiMetadata.HAS_GLYPH_UNKNOWN) {
+            final boolean hasGlyph = mGlyphChecker.hasGlyph(charSequence, start, end,
+                    metadata.getSdkAdded());
+            metadata.setHasGlyph(hasGlyph);
+        }
+
+        return metadata.getHasGlyph() == EmojiMetadata.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
+         */
+        EmojiMetadata getFlushMetadata() {
+            return mFlushNode.getData();
+        }
+
+        /**
+         * @return current pointer to the metadata node in the trie
+         */
+        EmojiMetadata 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.
+     */
+    private static final class CodepointIndexFinder {
+        private static final int INVALID_INDEX = -1;
+
+        /**
+         * 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;
+            }
+        }
+    }
+
+    /**
+     * Utility class that checks if the system can render a given glyph.
+     *
+     * @hide
+     */
+    @AnyThread
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    public static class DefaultGlyphChecker implements EmojiCompat.GlyphChecker {
+        /**
+         * Default text size for {@link #mTextPaint}.
+         */
+        private static final int PAINT_TEXT_SIZE = 10;
+
+        /**
+         * Used to create strings required by
+         * {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
+         */
+        private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
+
+        /**
+         * TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
+         */
+        private final TextPaint mTextPaint;
+
+        DefaultGlyphChecker() {
+            mTextPaint = new TextPaint();
+            mTextPaint.setTextSize(PAINT_TEXT_SIZE);
+        }
+
+        @Override
+        public boolean hasGlyph(
+                @NonNull CharSequence charSequence,
+                int start,
+                int end,
+                int sdkAdded
+        ) {
+            // For pre M devices, heuristic in PaintCompat can result in false positives. we are
+            // adding another heuristic using the sdkAdded field. if the emoji was added to OS
+            // at a later version we assume that the system probably cannot render it.
+            if (Build.VERSION.SDK_INT < 23 && sdkAdded > Build.VERSION.SDK_INT) {
+                return false;
+            }
+
+            final StringBuilder builder = getStringBuilder();
+            builder.setLength(0);
+
+            while (start < end) {
+                builder.append(charSequence.charAt(start));
+                start++;
+            }
+
+            return PaintCompat.hasGlyph(mTextPaint, builder.toString());
+        }
+
+        private static StringBuilder getStringBuilder() {
+            if (sStringBuilder.get() == null) {
+                sStringBuilder.set(new StringBuilder());
+            }
+            return sStringBuilder.get();
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
new file mode 100644
index 0000000..70dd537
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -0,0 +1,142 @@
+/*
+ * 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_GROUP_PREFIX;
+
+import android.graphics.Paint;
+import android.text.style.ReplacementSpan;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+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.
+     */
+    private final EmojiMetadata mMetadata;
+
+    /**
+     * 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 metadata information about the emoji, cannot be {@code null}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    EmojiSpan(@NonNull final EmojiMetadata metadata) {
+        Preconditions.checkNotNull(metadata, "metadata cannot be null");
+        mMetadata = metadata;
+    }
+
+    @Override
+    public int getSize(@NonNull final Paint paint, final CharSequence text, final int start,
+            final int end, final Paint.FontMetricsInt fm) {
+        paint.getFontMetricsInt(mTmpFontMetrics);
+        final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
+
+        mRatio = fontHeight * 1.0f / mMetadata.getHeight();
+        mHeight = (short) (mMetadata.getHeight() * mRatio);
+        mWidth = (short) (mMetadata.getWidth() * mRatio);
+
+        if (fm != null) {
+            fm.ascent = mTmpFontMetrics.ascent;
+            fm.descent = mTmpFontMetrics.descent;
+            fm.top = mTmpFontMetrics.top;
+            fm.bottom = mTmpFontMetrics.bottom;
+        }
+
+        return mWidth;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    final EmojiMetadata getMetadata() {
+        return mMetadata;
+    }
+
+    /**
+     * @return width of the span
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    final int getWidth() {
+        return mWidth;
+    }
+
+    /**
+     * @return height of the span
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    final int getHeight() {
+        return mHeight;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    final float getRatio() {
+        return mRatio;
+    }
+
+    /**
+     * @return unique id for the emoji that this EmojiSpan is used for
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @VisibleForTesting
+    public final int getId() {
+        return getMetadata().getId();
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
new file mode 100644
index 0000000..4ade79f
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/FontRequestEmojiCompatConfig.java
@@ -0,0 +1,363 @@
+/*
+ * 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.HandlerThread;
+import android.os.Process;
+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.core.graphics.TypefaceCompatUtil;
+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;
+
+/**
+ * {@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.
+ * <p/>
+ */
+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#setHandler}.
+     */
+    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.
+         * <br />
+         * If {@code zero} is returned, metadata loader will retry immediately.
+         * <br/>
+         * 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.
+         * <p/>
+         * 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_GROUP_PREFIX)
+    public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
+            @NonNull FontProviderHelper fontProviderHelper) {
+        super(new FontRequestMetadataLoader(context, request, fontProviderHelper));
+    }
+
+    /**
+     * Sets the custom handler to be used for initialization.
+     *
+     * Since font fetch take longer time, the metadata loader will fetch the fonts on the background
+     * thread. You can pass your own handler for this background fetching. This handler is also used
+     * for retrying.
+     *
+     * @param handler A {@link Handler} to be used for initialization. Can be {@code null}. In case
+     *               of {@code null}, the metadata loader creates own {@link HandlerThread} for
+     *               initialization.
+     */
+    public FontRequestEmojiCompatConfig setHandler(Handler handler) {
+        ((FontRequestMetadataLoader) getMetadataRepoLoader()).setHandler(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.
+     */
+    public FontRequestEmojiCompatConfig setRetryPolicy(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 final Context mContext;
+        private final FontRequest mRequest;
+        private final FontProviderHelper mFontProviderHelper;
+
+        private final Object mLock = new Object();
+        @GuardedBy("mLock")
+        private Handler mHandler;
+        @GuardedBy("mLock")
+        private HandlerThread mThread;
+        @GuardedBy("mLock")
+        private @Nullable RetryPolicy mRetryPolicy;
+
+        // Following three variables must be touched only on the thread associated with mHandler.
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        EmojiCompat.MetadataRepoLoaderCallback mCallback;
+        private ContentObserver mObserver;
+        private Runnable mHandleMetadataCreationRunner;
+
+        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 setHandler(Handler handler) {
+            synchronized (mLock) {
+                mHandler = handler;
+            }
+        }
+
+        public void setRetryPolicy(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) {
+                if (mHandler == null) {
+                    // Developer didn't give a thread for fetching. Create our own one.
+                    mThread = new HandlerThread("emojiCompat", Process.THREAD_PRIORITY_BACKGROUND);
+                    mThread.start();
+                    mHandler = new Handler(mThread.getLooper());
+                }
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mCallback = loaderCallback;
+                        createMetadata();
+                    }
+                });
+            }
+        }
+
+        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.
+        }
+
+        // Must be called on the mHandler.
+        @RequiresApi(19)
+        private void scheduleRetry(Uri uri, long waitMs) {
+            synchronized (mLock) {
+                if (mObserver == null) {
+                    mObserver = new ContentObserver(mHandler) {
+                        @Override
+                        public void onChange(boolean selfChange, Uri uri) {
+                            createMetadata();
+                        }
+                    };
+                    mFontProviderHelper.registerObserver(mContext, uri, mObserver);
+                }
+                if (mHandleMetadataCreationRunner == null) {
+                    mHandleMetadataCreationRunner = new Runnable() {
+                        @Override
+                        public void run() {
+                            createMetadata();
+                        }
+                    };
+                }
+                mHandler.postDelayed(mHandleMetadataCreationRunner, waitMs);
+            }
+        }
+
+        // Must be called on the mHandler.
+        private void cleanUp() {
+            mCallback = null;
+            if (mObserver != null) {
+                mFontProviderHelper.unregisterObserver(mContext, mObserver);
+                mObserver = null;
+            }
+            synchronized (mLock) {
+                mHandler.removeCallbacks(mHandleMetadataCreationRunner);
+                if (mThread != null) {
+                    mThread.quit();
+                }
+                mHandler = null;
+                mThread = null;
+            }
+        }
+
+        // Must be called on the mHandler.
+        @RequiresApi(19)
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        void createMetadata() {
+            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 + ")");
+                }
+
+                // 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) {
+                    throw new RuntimeException("Unable to open file.");
+                }
+                mCallback.onLoaded(MetadataRepo.create(typeface, buffer));
+                cleanUp();
+            } catch (Throwable t) {
+                mCallback.onFailed(t);
+                cleanUp();
+            }
+        }
+    }
+
+    /**
+     * Delegate class for mocking FontsContractCompat.fetchFonts.
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public static class FontProviderHelper {
+        /** Calls FontsContractCompat.fetchFonts. */
+        public FontFamilyResult fetchFonts(@NonNull Context context,
+                @NonNull FontRequest request) throws NameNotFoundException {
+            return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
+        }
+
+        /** Calls FontsContractCompat.buildTypeface. */
+        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/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
new file mode 100644
index 0000000..f9f397f
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataListReader.java
@@ -0,0 +1,349 @@
+/*
+ * 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_GROUP_PREFIX;
+
+import android.content.res.AssetManager;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntRange;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.text.emoji.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(LIBRARY_GROUP_PREFIX)
+@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 byte[] mByteArray;
+        private final ByteBuffer mByteBuffer;
+        private final 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(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 ByteBuffer mByteBuffer;
+
+        /**
+         * Constructs the reader with the given ByteBuffer.
+         *
+         * @param byteBuffer ByteBuffer to read from
+         */
+        ByteBufferReader(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/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
new file mode 100644
index 0000000..2f5ea66
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -0,0 +1,246 @@
+/*
+ * 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_GROUP_PREFIX;
+
+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.util.Preconditions;
+import androidx.text.emoji.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;
+
+    /**
+     * MetadataList that contains the emoji metadata.
+     */
+    private final MetadataList mMetadataList;
+
+    /**
+     * char presentation of all EmojiMetadata'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 char[] mEmojiCharArray;
+
+    /**
+     * Empty root node of the trie.
+     */
+    private final Node mRootNode;
+
+    /**
+     * Typeface to be used to render emojis.
+     */
+    private final Typeface mTypeface;
+
+    /**
+     * Constructor used for tests.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    MetadataRepo() {
+        mTypeface = null;
+        mMetadataList = null;
+        mRootNode = new Node(DEFAULT_ROOT_SIZE);
+        mEmojiCharArray = new char[0];
+    }
+
+    /**
+     * 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 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
+     */
+    public static MetadataRepo create(@NonNull final Typeface typeface,
+            @NonNull final InputStream inputStream) throws IOException {
+        return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
+    }
+
+    /**
+     * 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
+     */
+    public static MetadataRepo create(@NonNull final Typeface typeface,
+            @NonNull final ByteBuffer byteBuffer) throws IOException {
+        return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
+    }
+
+    /**
+     * 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
+     */
+    public static MetadataRepo create(@NonNull final AssetManager assetManager,
+            final String assetPath) throws IOException {
+        final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
+        return new MetadataRepo(typeface, MetadataListReader.read(assetManager, assetPath));
+    }
+
+    /**
+     * 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 EmojiMetadata metadata = new EmojiMetadata(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
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    Typeface getTypeface() {
+        return mTypeface;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    int getMetadataVersion() {
+        return mMetadataList.version();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    Node getRootNode() {
+        return mRootNode;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    public char[] getEmojiCharArray() {
+        return mEmojiCharArray;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    public MetadataList getMetadataList() {
+        return mMetadataList;
+    }
+
+    /**
+     * Add an EmojiMetadata to the index.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    @VisibleForTesting
+    void put(@NonNull final EmojiMetadata 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 EmojiMetadata. A single codepoint
+     * emoji is represented by a child of the root node.
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    static class Node {
+        private final SparseArray<Node> mChildren;
+        private EmojiMetadata 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 EmojiMetadata getData() {
+            return mData;
+        }
+
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        void put(@NonNull final EmojiMetadata 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/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
new file mode 100644
index 0000000..2c3ab58
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/TypefaceEmojiSpan.java
@@ -0,0 +1,72 @@
+/*
+ * 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_GROUP_PREFIX;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.TextPaint;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+
+/**
+ * EmojiSpan subclass used to render emojis using Typeface.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+public final class TypefaceEmojiSpan extends EmojiSpan {
+
+    /**
+     * Paint object used to draw a background in debug mode.
+     */
+    private static Paint sDebugPaint;
+
+    /**
+     * Default constructor.
+     *
+     * @param metadata metadata representing the emoji that this span will draw
+     */
+    public TypefaceEmojiSpan(final EmojiMetadata metadata) {
+        super(metadata);
+    }
+
+    @Override
+    public void draw(@NonNull final Canvas canvas, 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) {
+        if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
+            canvas.drawRect(x, top , x + getWidth(), bottom, getDebugPaint());
+        }
+        getMetadata().draw(canvas, x, y, paint);
+    }
+
+    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/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
new file mode 100644
index 0000000..7157c64
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EditTextAttributeHelper.java
@@ -0,0 +1,54 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.R;
+
+/**
+ * Helper class to parse EmojiCompat EditText attributes.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public class EditTextAttributeHelper {
+    static final int MAX_EMOJI_COUNT = Integer.MAX_VALUE;
+    private int mMaxEmojiCount;
+
+    public EditTextAttributeHelper(@NonNull View view, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        if (attrs != null) {
+            final Context context = view.getContext();
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EmojiEditText,
+                    defStyleAttr, defStyleRes);
+            mMaxEmojiCount = a.getInteger(R.styleable.EmojiEditText_maxEmojiCount, MAX_EMOJI_COUNT);
+            a.recycle();
+        }
+    }
+
+    public int getMaxEmojiCount() {
+        return mMaxEmojiCount;
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java
new file mode 100644
index 0000000..3fb4089
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiButton.java
@@ -0,0 +1,96 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.os.Build;
+import android.text.InputFilter;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.widget.Button;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.widget.TextViewCompat;
+
+/**
+ * Button widget enhanced with emoji capability by using {@link EmojiTextViewHelper}. When used
+ * on devices running API 18 or below, this widget acts as a regular {@link Button}.
+ */
+public class EmojiButton extends Button {
+    private EmojiTextViewHelper mEmojiTextViewHelper;
+
+    /**
+     * Prevent calling {@link #init()} multiple times in case super() constructors
+     * call other constructors.
+     */
+    private boolean mInitialized;
+
+    public EmojiButton(Context context) {
+        super(context);
+        init();
+    }
+
+    public EmojiButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    public EmojiButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init();
+    }
+
+    private void init() {
+        if (!mInitialized) {
+            mInitialized = true;
+            getEmojiTextViewHelper().updateTransformationMethod();
+        }
+    }
+
+    @Override
+    public void setFilters(InputFilter[] filters) {
+        super.setFilters(getEmojiTextViewHelper().getFilters(filters));
+    }
+
+    @Override
+    public void setAllCaps(boolean allCaps) {
+        super.setAllCaps(allCaps);
+        getEmojiTextViewHelper().setAllCaps(allCaps);
+    }
+
+    private EmojiTextViewHelper getEmojiTextViewHelper() {
+        if (mEmojiTextViewHelper == null) {
+            mEmojiTextViewHelper = new EmojiTextViewHelper(this);
+        }
+        return mEmojiTextViewHelper;
+    }
+
+    /**
+     * See
+     * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+     */
+    @Override
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        super.setCustomSelectionActionModeCallback(TextViewCompat
+                .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java
new file mode 100644
index 0000000..a97688b
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditText.java
@@ -0,0 +1,137 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.os.Build;
+import android.text.method.KeyListener;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * EditText widget enhanced with emoji capability by using {@link EmojiEditTextHelper}. When used
+ * on devices running API 18 or below, this widget acts as a regular {@link EditText}.
+ *
+ * {@link androidx.emoji.R.attr#maxEmojiCount}
+ */
+public class EmojiEditText extends EditText {
+    private EmojiEditTextHelper mEmojiEditTextHelper;
+
+    /**
+     * Prevent calling {@link #init(AttributeSet, int, int)} multiple times in case super()
+     * constructors call other constructors.
+     */
+    private boolean mInitialized;
+
+    public EmojiEditText(Context context) {
+        super(context);
+        init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+    }
+
+    public EmojiEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
+    }
+
+    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs, defStyleAttr, 0 /*defStyleRes*/);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        if (!mInitialized) {
+            mInitialized = true;
+            final EditTextAttributeHelper attrHelper = new EditTextAttributeHelper(this, attrs,
+                    defStyleAttr, defStyleRes);
+            setMaxEmojiCount(attrHelper.getMaxEmojiCount());
+            setKeyListener(super.getKeyListener());
+        }
+    }
+
+    @Override
+    public void setKeyListener(@Nullable KeyListener keyListener) {
+        if (keyListener != null) {
+            keyListener = getEmojiEditTextHelper().getKeyListener(keyListener);
+        }
+        super.setKeyListener(keyListener);
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+        return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
+    }
+
+    /**
+     * Set the maximum number of EmojiSpans to be added to a CharSequence. The number of spans in a
+     * CharSequence affects the performance of the EditText insert/delete operations. Insert/delete
+     * operations slow down as the number of spans increases.
+     *
+     * @param maxEmojiCount maximum number of EmojiSpans to be added to a single CharSequence,
+     *                      should be equal or greater than 0
+     *
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     *
+     * {@link androidx.emoji.R.attr#maxEmojiCount}
+     */
+    public void setMaxEmojiCount(@IntRange(from = 0) int maxEmojiCount) {
+        getEmojiEditTextHelper().setMaxEmojiCount(maxEmojiCount);
+    }
+
+    /**
+     * Returns the maximum number of EmojiSpans to be added to a CharSequence.
+     *
+     * @see #setMaxEmojiCount(int)
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     *
+     * {@link androidx.emoji.R.attr#maxEmojiCount}
+     */
+    public int getMaxEmojiCount() {
+        return getEmojiEditTextHelper().getMaxEmojiCount();
+    }
+
+    private EmojiEditTextHelper getEmojiEditTextHelper() {
+        if (mEmojiEditTextHelper == null) {
+            mEmojiEditTextHelper = new EmojiEditTextHelper(this);
+        }
+        return mEmojiEditTextHelper;
+    }
+
+    /**
+     * See
+     * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+     */
+    @Override
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        super.setCustomSelectionActionModeCallback(TextViewCompat
+                .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java
new file mode 100644
index 0000000..32ee18b
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditTextHelper.java
@@ -0,0 +1,241 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.os.Build;
+import android.text.method.KeyListener;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.util.Preconditions;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
+
+/**
+ * Utility class to enhance custom EditText widgets with {@link EmojiCompat}.
+ * <p/>
+ * <pre>
+ * public class MyEmojiEditText extends EditText {
+ *      public MyEmojiEditText(Context context) {
+ *          super(context);
+ *          init();
+ *      }
+ *      // ...
+ *      private void init() {
+ *          super.setKeyListener(getEmojiEditTextHelper().getKeyListener(getKeyListener()));
+ *      }
+ *
+ *      {@literal @}Override
+ *      public void setKeyListener(android.text.method.KeyListener keyListener) {
+ *          super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener));
+ *      }
+ *
+ *      {@literal @}Override
+ *      public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ *          InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+ *          return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
+ *      }
+ *
+ *      private EmojiEditTextHelper getEmojiEditTextHelper() {
+ *          if (mEmojiEditTextHelper == null) {
+ *              mEmojiEditTextHelper = new EmojiEditTextHelper(this);
+ *          }
+ *          return mEmojiEditTextHelper;
+ *      }
+ * }
+ * </pre>
+ *
+ */
+public final class EmojiEditTextHelper {
+    private final HelperInternal mHelper;
+    private int mMaxEmojiCount = EditTextAttributeHelper.MAX_EMOJI_COUNT;
+    @EmojiCompat.ReplaceStrategy
+    private int mEmojiReplaceStrategy = EmojiCompat.REPLACE_STRATEGY_DEFAULT;
+
+    /**
+     * Default constructor.
+     *
+     * @param editText EditText instance
+     */
+    public EmojiEditTextHelper(@NonNull final EditText editText) {
+        Preconditions.checkNotNull(editText, "editText cannot be null");
+        mHelper = Build.VERSION.SDK_INT >= 19 ? new HelperInternal19(editText)
+                : new HelperInternal();
+    }
+
+    /**
+     * Set the maximum number of EmojiSpans to be added to a CharSequence. The number of spans in a
+     * CharSequence affects the performance of the EditText insert/delete operations. Insert/delete
+     * operations slow down as the number of spans increases.
+     * <p/>
+     *
+     * @param maxEmojiCount maximum number of EmojiSpans to be added to a single CharSequence,
+     *                      should be equal or greater than 0
+     *
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     */
+    public void setMaxEmojiCount(@IntRange(from = 0) int maxEmojiCount) {
+        Preconditions.checkArgumentNonnegative(maxEmojiCount,
+                "maxEmojiCount should be greater than 0");
+        mMaxEmojiCount = maxEmojiCount;
+        mHelper.setMaxEmojiCount(maxEmojiCount);
+    }
+
+    /**
+     * Returns the maximum number of EmojiSpans to be added to a CharSequence.
+     *
+     * @see #setMaxEmojiCount(int)
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     */
+    public int getMaxEmojiCount() {
+        return mMaxEmojiCount;
+    }
+
+    /**
+     * Attaches EmojiCompat KeyListener to the widget. Should be called from {@link
+     * TextView#setKeyListener(KeyListener)}. Existing keyListener is wrapped into EmojiCompat
+     * KeyListener. When used on devices running API 18 or below, this method returns
+     * {@code keyListener} that is given as a parameter.
+     *
+     * @param keyListener KeyListener passed into {@link TextView#setKeyListener(KeyListener)}
+     *
+     * @return a new KeyListener instance that wraps {@code keyListener}.
+     */
+    @NonNull
+    public KeyListener getKeyListener(@NonNull final KeyListener keyListener) {
+        Preconditions.checkNotNull(keyListener, "keyListener cannot be null");
+        return mHelper.getKeyListener(keyListener);
+    }
+
+    /**
+     * Updates the InputConnection with emoji support. Should be called from {@link
+     * TextView#onCreateInputConnection(EditorInfo)}. When used on devices running API 18 or below,
+     * this method returns {@code inputConnection} that is given as a parameter. If
+     * {@code inputConnection} is {@code null}, returns {@code null}.
+     *
+     * @param inputConnection InputConnection instance created by TextView
+     * @param outAttrs        EditorInfo passed into
+     *                        {@link TextView#onCreateInputConnection(EditorInfo)}
+     *
+     * @return a new InputConnection instance that wraps {@code inputConnection}
+     */
+    @Nullable
+    public InputConnection onCreateInputConnection(@Nullable final InputConnection inputConnection,
+            @NonNull final EditorInfo outAttrs) {
+        if (inputConnection == null) return null;
+        return mHelper.onCreateInputConnection(inputConnection, outAttrs);
+    }
+
+    /**
+     * Sets whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @param replaceStrategy should be one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     *
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+        mEmojiReplaceStrategy = replaceStrategy;
+        mHelper.setEmojiReplaceStrategy(replaceStrategy);
+    }
+
+    /**
+     * Returns whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @return one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    int getEmojiReplaceStrategy() {
+        return mEmojiReplaceStrategy;
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static class HelperInternal {
+
+        KeyListener getKeyListener(@NonNull KeyListener keyListener) {
+            return keyListener;
+        }
+
+        InputConnection onCreateInputConnection(@NonNull InputConnection inputConnection,
+                @NonNull EditorInfo outAttrs) {
+            return inputConnection;
+        }
+
+        void setMaxEmojiCount(int maxEmojiCount) {
+            // do nothing
+        }
+
+        void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+            // do nothing
+        }
+    }
+
+    @RequiresApi(19)
+    private static class HelperInternal19 extends HelperInternal {
+        private final EditText mEditText;
+        private final EmojiTextWatcher mTextWatcher;
+
+        HelperInternal19(@NonNull EditText editText) {
+            mEditText = editText;
+            mTextWatcher = new EmojiTextWatcher(mEditText);
+            mEditText.addTextChangedListener(mTextWatcher);
+            mEditText.setEditableFactory(EmojiEditableFactory.getInstance());
+        }
+
+        @Override
+        void setMaxEmojiCount(int maxEmojiCount) {
+            mTextWatcher.setMaxEmojiCount(maxEmojiCount);
+        }
+
+        @Override
+        void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+            mTextWatcher.setEmojiReplaceStrategy(replaceStrategy);
+        }
+
+        @Override
+        KeyListener getKeyListener(@NonNull final KeyListener keyListener) {
+            if (keyListener instanceof EmojiKeyListener) {
+                return keyListener;
+            }
+            return new EmojiKeyListener(keyListener);
+        }
+
+        @Override
+        InputConnection onCreateInputConnection(@NonNull final InputConnection inputConnection,
+                @NonNull final EditorInfo outAttrs) {
+            if (inputConnection instanceof EmojiInputConnection) {
+                return inputConnection;
+            }
+            return new EmojiInputConnection(mEditText, inputConnection, outAttrs);
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java
new file mode 100644
index 0000000..b9c1abd
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiEditableFactory.java
@@ -0,0 +1,78 @@
+/*
+ * 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.widget;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * EditableFactory used to improve editing operations on an EditText.
+ * <p>
+ * EditText uses DynamicLayout, which attaches to the Spannable instance that is being edited using
+ * ChangeWatcher. ChangeWatcher implements SpanWatcher and Textwatcher. Currently every delete/add
+ * operation is reported to DynamicLayout, for every span that has changed. For each change,
+ * DynamicLayout performs some expensive computations. i.e. if there is 100 EmojiSpans and the first
+ * span is deleted, DynamicLayout gets 99 calls about the change of position occurred in the
+ * remaining spans. This causes a huge delay in response time.
+ * <p>
+ * Since "android.text.DynamicLayout$ChangeWatcher" class is not a public class,
+ * EmojiEditableFactory checks if the watcher is in the classpath, and if so uses the modified
+ * Spannable which reduces the total number of calls to DynamicLayout for operations that affect
+ * EmojiSpans.
+ *
+ * @see SpannableBuilder
+ */
+final class EmojiEditableFactory extends Editable.Factory {
+    private static final Object INSTANCE_LOCK = new Object();
+    @GuardedBy("INSTANCE_LOCK")
+    private static volatile Editable.Factory sInstance;
+
+    @Nullable private static Class<?> sWatcherClass;
+
+    @SuppressLint("PrivateApi")
+    private EmojiEditableFactory() {
+        try {
+            String className = "android.text.DynamicLayout$ChangeWatcher";
+            sWatcherClass = Class.forName(className, false, getClass().getClassLoader());
+        } catch (Throwable t) {
+            // ignore
+        }
+    }
+
+    @SuppressWarnings("GuardedBy")
+    public static Editable.Factory getInstance() {
+        if (sInstance == null) {
+            synchronized (INSTANCE_LOCK) {
+                if (sInstance == null) {
+                    sInstance = new EmojiEditableFactory();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    @Override
+    public Editable newEditable(@NonNull final CharSequence source) {
+        if (sWatcherClass != null) {
+            return SpannableBuilder.create(sWatcherClass, source);
+        }
+        return super.newEditable(source);
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
new file mode 100644
index 0000000..288d3d9
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractEditText.java
@@ -0,0 +1,165 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.content.Context;
+import android.inputmethodservice.ExtractEditText;
+import android.os.Build;
+import android.text.method.KeyListener;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.TextView;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.widget.TextViewCompat;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
+
+/**
+ * ExtractEditText widget enhanced with emoji capability by using {@link EmojiEditTextHelper}.
+ * When used on devices running API 18 or below, this widget acts as a {@link ExtractEditText} and
+ * does not provide any emoji compatibility feature.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public class EmojiExtractEditText extends ExtractEditText {
+    private EmojiEditTextHelper mEmojiEditTextHelper;
+
+    /**
+     * Prevent calling {@link #init(AttributeSet, int)} multiple times in case super() constructors
+     * call other constructors.
+     */
+    private boolean mInitialized;
+
+    public EmojiExtractEditText(Context context) {
+        super(context);
+        init(null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+    }
+
+    public EmojiExtractEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs, android.R.attr.editTextStyle, 0 /*defStyleRes*/);
+    }
+
+    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(attrs, defStyleAttr, 0 /*defStyleRes*/);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    public EmojiExtractEditText(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        if (!mInitialized) {
+            mInitialized = true;
+            final EditTextAttributeHelper attrHelper = new EditTextAttributeHelper(this, attrs,
+                    defStyleAttr, defStyleRes);
+            setMaxEmojiCount(attrHelper.getMaxEmojiCount());
+            setKeyListener(super.getKeyListener());
+        }
+    }
+
+    @Override
+    public void setKeyListener(@Nullable KeyListener keyListener) {
+        if (keyListener != null) {
+            keyListener = getEmojiEditTextHelper().getKeyListener(keyListener);
+        }
+        super.setKeyListener(keyListener);
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
+        return getEmojiEditTextHelper().onCreateInputConnection(inputConnection, outAttrs);
+    }
+
+    /**
+     * Set the maximum number of EmojiSpans to be added to a CharSequence. The number of spans in a
+     * CharSequence affects the performance of the EditText insert/delete operations. Insert/delete
+     * operations slow down as the number of spans increases.
+     *
+     * @param maxEmojiCount maximum number of EmojiSpans to be added to a single CharSequence,
+     *                      should be equal or greater than 0
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     */
+    public void setMaxEmojiCount(@IntRange(from = 0) int maxEmojiCount) {
+        getEmojiEditTextHelper().setMaxEmojiCount(maxEmojiCount);
+    }
+
+    /**
+     * Sets whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @param replaceStrategy should be one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     */
+    public void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+        getEmojiEditTextHelper().setEmojiReplaceStrategy(replaceStrategy);
+    }
+
+    /**
+     * Returns whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @return one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     */
+    public int getEmojiReplaceStrategy() {
+        return getEmojiEditTextHelper().getEmojiReplaceStrategy();
+    }
+
+    /**
+     * Returns the maximum number of EmojiSpans to be added to a CharSequence.
+     *
+     * @see #setMaxEmojiCount(int)
+     * @see EmojiCompat#process(CharSequence, int, int, int)
+     */
+    public int getMaxEmojiCount() {
+        return getEmojiEditTextHelper().getMaxEmojiCount();
+    }
+
+    private EmojiEditTextHelper getEmojiEditTextHelper() {
+        if (mEmojiEditTextHelper == null) {
+            mEmojiEditTextHelper = new EmojiEditTextHelper(this);
+        }
+        return mEmojiEditTextHelper;
+    }
+
+    /**
+     * See
+     * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+     */
+    @Override
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        super.setCustomSelectionActionModeCallback(TextViewCompat
+                .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
new file mode 100644
index 0000000..ba476e1
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiExtractTextLayout.java
@@ -0,0 +1,228 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.view.ViewCompat;
+import androidx.emoji2.R;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
+
+/**
+ * Layout that contains emoji compatibility enhanced ExtractEditText. Should be used by
+ * {@link InputMethodService} implementations.
+ * <p/>
+ * Call {@link #onUpdateExtractingViews(InputMethodService, EditorInfo)} from
+ * {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
+ * InputMethodService#onUpdateExtractingViews(EditorInfo)}.
+ * <pre>
+ * public class MyInputMethodService extends InputMethodService {
+ *     // ..
+ *     {@literal @}Override
+ *     public View onCreateExtractTextView() {
+ *         mExtractView = getLayoutInflater().inflate(R.layout.emoji_input_method_extract_layout,
+ *                 null);
+ *         return mExtractView;
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onUpdateExtractingViews(EditorInfo ei) {
+ *         mExtractView.onUpdateExtractingViews(this, ei);
+ *     }
+ * }
+ * </pre>
+ *
+ * {@link androidx.emoji.R.attr#emojiReplaceStrategy}
+ */
+public class EmojiExtractTextLayout extends LinearLayout {
+
+    private ExtractButtonCompat mExtractAction;
+    private EmojiExtractEditText mExtractEditText;
+    private ViewGroup mExtractAccessories;
+    private View.OnClickListener mButtonOnClickListener;
+
+    /**
+     * Prevent calling {@link #init(Context, AttributeSet, int)}} multiple times in case super()
+     * constructors call other constructors.
+     */
+    private boolean mInitialized;
+
+    public EmojiExtractTextLayout(Context context) {
+        super(context);
+        init(context, null /*attrs*/, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+    }
+
+    public EmojiExtractTextLayout(Context context,
+            @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0 /*defStyleAttr*/, 0 /*defStyleRes*/);
+    }
+
+    public EmojiExtractTextLayout(Context context,
+            @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr, 0 /*defStyleRes*/);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public EmojiExtractTextLayout(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        if (!mInitialized) {
+            mInitialized = true;
+            setOrientation(HORIZONTAL);
+            final View view = LayoutInflater.from(context)
+                    .inflate(R.layout.input_method_extract_view, this /*root*/,
+                            true /*attachToRoot*/);
+            mExtractAccessories = view.findViewById(R.id.inputExtractAccessories);
+            mExtractAction = view.findViewById(R.id.inputExtractAction);
+            mExtractEditText = view.findViewById(android.R.id.inputExtractEditText);
+
+            if (attrs != null) {
+                final TypedArray a = context.obtainStyledAttributes(attrs,
+                        R.styleable.EmojiExtractTextLayout, defStyleAttr, defStyleRes);
+                ViewCompat.saveAttributeDataForStyleable(
+                        this, context, R.styleable.EmojiExtractTextLayout, attrs, a, defStyleAttr,
+                        defStyleRes);
+                final int replaceStrategy = a.getInteger(
+                        R.styleable.EmojiExtractTextLayout_emojiReplaceStrategy,
+                        EmojiCompat.REPLACE_STRATEGY_DEFAULT);
+                mExtractEditText.setEmojiReplaceStrategy(replaceStrategy);
+                a.recycle();
+            }
+        }
+    }
+
+    /**
+     * Sets whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @param replaceStrategy should be one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     *
+     * {@link androidx.emoji.R.attr#emojiReplaceStrategy}
+     */
+    public void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+        mExtractEditText.setEmojiReplaceStrategy(replaceStrategy);
+    }
+
+    /**
+     * Returns whether to replace all emoji with {@link EmojiSpan}s. Default value is
+     * {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT}.
+     *
+     * @return one of {@link EmojiCompat#REPLACE_STRATEGY_DEFAULT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_NON_EXISTENT},
+     *                        {@link EmojiCompat#REPLACE_STRATEGY_ALL}
+     *
+     * {@link androidx.emoji.R.attr#emojiReplaceStrategy}
+     */
+    public int getEmojiReplaceStrategy() {
+        return mExtractEditText.getEmojiReplaceStrategy();
+    }
+
+    /**
+     * Initializes the layout. Call this function from
+     * {@link InputMethodService#onUpdateExtractingViews(EditorInfo)
+     * InputMethodService#onUpdateExtractingViews(EditorInfo)}.
+     */
+    public void onUpdateExtractingViews(InputMethodService inputMethodService, EditorInfo ei) {
+        // the following code is ported as it is from InputMethodService.onUpdateExtractingViews
+        if (!inputMethodService.isExtractViewShown()) {
+            return;
+        }
+
+        if (mExtractAccessories == null) {
+            return;
+        }
+
+        final boolean hasAction = ei.actionLabel != null
+                || ((ei.imeOptions & EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE
+                && (ei.imeOptions & EditorInfo.IME_FLAG_NO_ACCESSORY_ACTION) == 0
+                && ei.inputType != InputType.TYPE_NULL);
+
+        if (hasAction) {
+            mExtractAccessories.setVisibility(View.VISIBLE);
+            if (mExtractAction != null) {
+                if (ei.actionLabel != null) {
+                    mExtractAction.setText(ei.actionLabel);
+                } else {
+                    mExtractAction.setText(inputMethodService.getTextForImeAction(ei.imeOptions));
+                }
+                mExtractAction.setOnClickListener(getButtonClickListener(inputMethodService));
+            }
+        } else {
+            mExtractAccessories.setVisibility(View.GONE);
+            if (mExtractAction != null) {
+                mExtractAction.setOnClickListener(null);
+            }
+        }
+    }
+
+    private View.OnClickListener getButtonClickListener(
+            final InputMethodService inputMethodService) {
+        if (mButtonOnClickListener == null) {
+            mButtonOnClickListener = new ButtonOnclickListener(inputMethodService);
+        }
+        return mButtonOnClickListener;
+    }
+
+    private static final class ButtonOnclickListener implements View.OnClickListener {
+        private final InputMethodService mInputMethodService;
+
+        ButtonOnclickListener(InputMethodService inputMethodService) {
+            mInputMethodService = inputMethodService;
+        }
+
+        /**
+         * The following code is ported as it is from InputMethodService.mActionClickListener.
+         */
+        @Override
+        public void onClick(View v) {
+            final EditorInfo ei = mInputMethodService.getCurrentInputEditorInfo();
+            final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+            if (ei != null && ic != null) {
+                if (ei.actionId != 0) {
+                    ic.performEditorAction(ei.actionId);
+                } else if ((ei.imeOptions & EditorInfo.IME_MASK_ACTION)
+                        != EditorInfo.IME_ACTION_NONE) {
+                    ic.performEditorAction(ei.imeOptions & EditorInfo.IME_MASK_ACTION);
+                }
+            }
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java
new file mode 100644
index 0000000..53e8ee4
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputConnection.java
@@ -0,0 +1,72 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.text.Editable;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * InputConnectionWrapper for EditText delete operations. Keyboard does not have knowledge about
+ * emojis and therefore might send commands to delete a part of the emoji sequence which creates
+ * invalid codeunits/getCodepointAt in the text.
+ * <p/>
+ * This class tries to correctly delete an emoji checking if there is an emoji span.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiInputConnection extends InputConnectionWrapper {
+    private final TextView mTextView;
+
+    EmojiInputConnection(
+            @NonNull final TextView textView,
+            @NonNull final InputConnection inputConnection,
+            @NonNull final EditorInfo outAttrs) {
+        super(inputConnection, false);
+        mTextView = textView;
+        EmojiCompat.get().updateEditorInfoAttrs(outAttrs);
+    }
+
+    @Override
+    public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
+        final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(),
+                beforeLength, afterLength, false /*inCodePoints*/);
+        return result || super.deleteSurroundingText(beforeLength, afterLength);
+    }
+
+    @Override
+    public boolean deleteSurroundingTextInCodePoints(final int beforeLength,
+            final int afterLength) {
+        final boolean result = EmojiCompat.handleDeleteSurroundingText(this, getEditable(),
+                beforeLength, afterLength, true /*inCodePoints*/);
+        return result || super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+    }
+
+    private Editable getEditable() {
+        return mTextView.getEditableText();
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java
new file mode 100644
index 0000000..fb1bd11
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiInputFilter.java
@@ -0,0 +1,134 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiCompat.InitCallback;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+
+/**
+ * InputFilter to add EmojiSpans to the CharSequence set in a TextView. Unlike EditText where a
+ * TextWatcher is used to enhance the CharSequence, InputFilter is used on TextView. The reason is
+ * that if you add a TextWatcher to a TextView, its internal layout mechanism change, and therefore
+ * depending on the CharSequence provided, adding a TextWatcher might have performance side
+ * effects.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiInputFilter implements android.text.InputFilter {
+    private final TextView mTextView;
+    private InitCallback mInitCallback;
+
+    EmojiInputFilter(@NonNull final TextView textView) {
+        mTextView = textView;
+    }
+
+    @Override
+    public CharSequence filter(final CharSequence source, final int sourceStart,
+            final int sourceEnd, final Spanned dest, final int destStart, final int destEnd) {
+        if (mTextView.isInEditMode()) {
+            return source;
+        }
+
+        switch (EmojiCompat.get().getLoadState()){
+            case EmojiCompat.LOAD_STATE_SUCCEEDED:
+                boolean process = true;
+                if (destEnd == 0 && destStart == 0 && dest.length() == 0) {
+                    final CharSequence oldText = mTextView.getText();
+                    if (source == oldText) {
+                        process = false;
+                    }
+                }
+
+                if (process && source != null) {
+                    final CharSequence text;
+                    if (sourceStart == 0 && sourceEnd == source.length()) {
+                        text = source;
+                    } else {
+                        text = source.subSequence(sourceStart, sourceEnd);
+                    }
+                    return EmojiCompat.get().process(text, 0, text.length());
+                }
+
+                return source;
+            case EmojiCompat.LOAD_STATE_LOADING:
+            case EmojiCompat.LOAD_STATE_DEFAULT:
+                EmojiCompat.get().registerInitCallback(getInitCallback());
+                return source;
+
+            case EmojiCompat.LOAD_STATE_FAILED:
+            default:
+                return source;
+        }
+    }
+
+    private InitCallback getInitCallback() {
+        if (mInitCallback == null) {
+            mInitCallback = new InitCallbackImpl(mTextView);
+        }
+        return mInitCallback;
+    }
+
+    private static class InitCallbackImpl extends InitCallback {
+        private final Reference<TextView> mViewRef;
+
+        InitCallbackImpl(TextView textView) {
+            mViewRef = new WeakReference<>(textView);
+        }
+
+        @Override
+        public void onInitialized() {
+            super.onInitialized();
+            final TextView textView = mViewRef.get();
+            if (textView != null && textView.isAttachedToWindow()) {
+                final CharSequence result = EmojiCompat.get().process(textView.getText());
+
+                final int selectionStart = Selection.getSelectionStart(result);
+                final int selectionEnd = Selection.getSelectionEnd(result);
+
+                textView.setText(result);
+
+                if (result instanceof Spannable) {
+                    updateSelection((Spannable) result, selectionStart, selectionEnd);
+                }
+            }
+        }
+    }
+
+    static void updateSelection(Spannable spannable, final int start, final int end) {
+        if (start >= 0 && end >= 0) {
+            Selection.setSelection(spannable, start, end);
+        } else if (start >= 0) {
+            Selection.setSelection(spannable, start);
+        } else if (end >= 0) {
+            Selection.setSelection(spannable, end);
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java
new file mode 100644
index 0000000..8bac084e
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiKeyListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.text.Editable;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * KeyListener class to handle delete operations correctly.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiKeyListener implements android.text.method.KeyListener {
+    private final android.text.method.KeyListener mKeyListener;
+
+    EmojiKeyListener(android.text.method.KeyListener keyListener) {
+        mKeyListener = keyListener;
+    }
+
+    @Override
+    public int getInputType() {
+        return mKeyListener.getInputType();
+    }
+
+    @Override
+    public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) {
+        final boolean result = EmojiCompat.handleOnKeyDown(content, keyCode, event);
+        return result || mKeyListener.onKeyDown(view, content, keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(View view, Editable text, int keyCode, KeyEvent event) {
+        return mKeyListener.onKeyUp(view, text, keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyOther(View view, Editable text, KeyEvent event) {
+        return mKeyListener.onKeyOther(view, text, event);
+    }
+
+    @Override
+    public void clearMetaKeyState(View view, Editable content, int states) {
+        mKeyListener.clearMetaKeyState(view, content, states);
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java
new file mode 100644
index 0000000..a9a7492
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextView.java
@@ -0,0 +1,96 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.os.Build;
+import android.text.InputFilter;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.widget.TextViewCompat;
+
+/**
+ * TextView widget enhanced with emoji capability by using {@link EmojiTextViewHelper}. When used
+ * on devices running API 18 or below, this widget acts as a regular {@link TextView}.
+ */
+public class EmojiTextView extends TextView {
+    private EmojiTextViewHelper mEmojiTextViewHelper;
+
+    /**
+     * Prevent calling {@link #init()} multiple times in case super() constructors
+     * call other constructors.
+     */
+    private boolean mInitialized;
+
+    public EmojiTextView(Context context) {
+        super(context);
+        init();
+    }
+
+    public EmojiTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init();
+    }
+
+    private void init() {
+        if (!mInitialized) {
+            mInitialized = true;
+            getEmojiTextViewHelper().updateTransformationMethod();
+        }
+    }
+
+    @Override
+    public void setFilters(InputFilter[] filters) {
+        super.setFilters(getEmojiTextViewHelper().getFilters(filters));
+    }
+
+    @Override
+    public void setAllCaps(boolean allCaps) {
+        super.setAllCaps(allCaps);
+        getEmojiTextViewHelper().setAllCaps(allCaps);
+    }
+
+    private EmojiTextViewHelper getEmojiTextViewHelper() {
+        if (mEmojiTextViewHelper == null) {
+            mEmojiTextViewHelper = new EmojiTextViewHelper(this);
+        }
+        return mEmojiTextViewHelper;
+    }
+
+    /**
+     * See
+     * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+     */
+    @Override
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        super.setCustomSelectionActionModeCallback(TextViewCompat
+                .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java
new file mode 100644
index 0000000..d6a34e4
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextViewHelper.java
@@ -0,0 +1,197 @@
+/*
+ * 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.widget;
+
+import android.os.Build;
+import android.text.InputFilter;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Preconditions;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * Utility class to enhance custom TextView widgets with {@link EmojiCompat}.
+ * <pre>
+ * public class MyEmojiTextView extends TextView {
+ *     public MyEmojiTextView(Context context) {
+ *         super(context);
+ *         init();
+ *     }
+ *     // ..
+ *     private void init() {
+ *         getEmojiTextViewHelper().updateTransformationMethod();
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void setFilters(InputFilter[] filters) {
+ *         super.setFilters(getEmojiTextViewHelper().getFilters(filters));
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void setAllCaps(boolean allCaps) {
+ *         super.setAllCaps(allCaps);
+ *         getEmojiTextViewHelper().setAllCaps(allCaps);
+ *     }
+ *
+ *     private EmojiTextViewHelper getEmojiTextViewHelper() {
+ *         if (mEmojiTextViewHelper == null) {
+ *             mEmojiTextViewHelper = new EmojiTextViewHelper(this);
+ *         }
+ *         return mEmojiTextViewHelper;
+ *     }
+ * }
+ * </pre>
+ */
+public final class EmojiTextViewHelper {
+
+    private final HelperInternal mHelper;
+
+    /**
+     * Default constructor.
+     *
+     * @param textView TextView instance
+     */
+    public EmojiTextViewHelper(@NonNull TextView textView) {
+        Preconditions.checkNotNull(textView, "textView cannot be null");
+        mHelper = Build.VERSION.SDK_INT >= 19 ? new HelperInternal19(textView)
+                : new HelperInternal();
+    }
+
+    /**
+     * Updates widget's TransformationMethod so that the transformed text can be processed.
+     * Should be called in the widget constructor. When used on devices running API 18 or below,
+     * this method does nothing.
+     *
+     * @see #wrapTransformationMethod(TransformationMethod)
+     */
+    public void updateTransformationMethod() {
+        mHelper.updateTransformationMethod();
+    }
+
+    /**
+     * Appends EmojiCompat InputFilters to the widget InputFilters. Should be called by {@link
+     * TextView#setFilters(InputFilter[])} to update the InputFilters. When used on devices running
+     * API 18 or below, this method returns {@code filters} that is given as a parameter.
+     *
+     * @param filters InputFilter array passed to {@link TextView#setFilters(InputFilter[])}
+     *
+     * @return same copy if the array already contains EmojiCompat InputFilter. A new array copy if
+     * not.
+     */
+    @NonNull
+    public InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
+        return mHelper.getFilters(filters);
+    }
+
+    /**
+     * Returns transformation method that can update the transformed text to display emojis. When
+     * used on devices running API 18 or below, this method returns {@code transformationMethod}
+     * that is given as a parameter.
+     *
+     * @param transformationMethod instance to be wrapped
+     */
+    @Nullable
+    public TransformationMethod wrapTransformationMethod(
+            @Nullable TransformationMethod transformationMethod) {
+        return mHelper.wrapTransformationMethod(transformationMethod);
+    }
+
+    /**
+     * Call when allCaps is set on TextView. When used on devices running API 18 or below, this
+     * method does nothing.
+     *
+     * @param allCaps allCaps parameter passed to {@link TextView#setAllCaps(boolean)}
+     */
+    public void setAllCaps(boolean allCaps) {
+        mHelper.setAllCaps(allCaps);
+    }
+
+    @SuppressWarnings("WeakerAccess") /* synthetic access */
+    static class HelperInternal {
+
+        void updateTransformationMethod() {
+            // do nothing
+        }
+
+        InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
+            return filters;
+        }
+
+        TransformationMethod wrapTransformationMethod(TransformationMethod transformationMethod) {
+            return transformationMethod;
+        }
+
+        void setAllCaps(boolean allCaps) {
+            // do nothing
+        }
+    }
+
+    @RequiresApi(19)
+    private static class HelperInternal19 extends HelperInternal {
+        private final TextView mTextView;
+        private final EmojiInputFilter mEmojiInputFilter;
+
+        HelperInternal19(TextView textView) {
+            mTextView = textView;
+            mEmojiInputFilter = new EmojiInputFilter(textView);
+        }
+
+        @Override
+        void updateTransformationMethod() {
+            final TransformationMethod tm = mTextView.getTransformationMethod();
+            if (tm != null && !(tm instanceof PasswordTransformationMethod)) {
+                mTextView.setTransformationMethod(wrapTransformationMethod(tm));
+            }
+        }
+
+        @Override
+        InputFilter[] getFilters(@NonNull final InputFilter[] filters) {
+            final int count = filters.length;
+            for (int i = 0; i < count; i++) {
+                if (filters[i] instanceof EmojiInputFilter) {
+                    return filters;
+                }
+            }
+            final InputFilter[] newFilters = new InputFilter[filters.length + 1];
+            System.arraycopy(filters, 0, newFilters, 0, count);
+            newFilters[count] = mEmojiInputFilter;
+            return newFilters;
+        }
+
+        @Override
+        TransformationMethod wrapTransformationMethod(TransformationMethod transformationMethod) {
+            if (transformationMethod instanceof EmojiTransformationMethod) {
+                return transformationMethod;
+            }
+            return new EmojiTransformationMethod(transformationMethod);
+        }
+
+        @Override
+        void setAllCaps(boolean allCaps) {
+            // When allCaps is set to false TextView sets the transformation method to be null. We
+            // are only interested when allCaps is set to true in order to wrap the original method.
+            if (allCaps) {
+                updateTransformationMethod();
+            }
+        }
+
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java
new file mode 100644
index 0000000..46fae2b
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTextWatcher.java
@@ -0,0 +1,133 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.widget.EditText;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiCompat.InitCallback;
+
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+
+/**
+ * TextWatcher used for an EditText.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+final class EmojiTextWatcher implements android.text.TextWatcher {
+    private final EditText mEditText;
+    private InitCallback mInitCallback;
+    private int mMaxEmojiCount = EditTextAttributeHelper.MAX_EMOJI_COUNT;
+    @EmojiCompat.ReplaceStrategy
+    private int mEmojiReplaceStrategy = EmojiCompat.REPLACE_STRATEGY_DEFAULT;
+
+    EmojiTextWatcher(EditText editText) {
+        mEditText = editText;
+    }
+
+    void setMaxEmojiCount(int maxEmojiCount) {
+        this.mMaxEmojiCount = maxEmojiCount;
+    }
+
+    int getMaxEmojiCount() {
+        return mMaxEmojiCount;
+    }
+
+    @EmojiCompat.ReplaceStrategy int getEmojiReplaceStrategy() {
+        return mEmojiReplaceStrategy;
+    }
+
+    void setEmojiReplaceStrategy(@EmojiCompat.ReplaceStrategy int replaceStrategy) {
+        mEmojiReplaceStrategy = replaceStrategy;
+    }
+
+    @Override
+    public void onTextChanged(CharSequence charSequence, final int start, final int before,
+            final int after) {
+        if (mEditText.isInEditMode()) {
+            return;
+        }
+
+        //before > after --> a deletion occurred
+        if (before <= after && charSequence instanceof Spannable) {
+            switch (EmojiCompat.get().getLoadState()){
+                case EmojiCompat.LOAD_STATE_SUCCEEDED:
+                    final Spannable s = (Spannable) charSequence;
+                    EmojiCompat.get().process(s, start, start + after, mMaxEmojiCount,
+                            mEmojiReplaceStrategy);
+                    break;
+                case EmojiCompat.LOAD_STATE_LOADING:
+                case EmojiCompat.LOAD_STATE_DEFAULT:
+                    EmojiCompat.get().registerInitCallback(getInitCallback());
+                    break;
+                case EmojiCompat.LOAD_STATE_FAILED:
+                default:
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        // do nothing
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) {
+        // do nothing
+    }
+
+    private InitCallback getInitCallback() {
+        if (mInitCallback == null) {
+            mInitCallback = new InitCallbackImpl(mEditText);
+        }
+        return mInitCallback;
+    }
+
+    private static class InitCallbackImpl extends InitCallback {
+        private final Reference<EditText> mViewRef;
+
+        InitCallbackImpl(EditText editText) {
+            mViewRef = new WeakReference<>(editText);
+        }
+
+        @Override
+        public void onInitialized() {
+            super.onInitialized();
+            final EditText editText = mViewRef.get();
+            if (editText != null && editText.isAttachedToWindow()) {
+                final Editable text = editText.getEditableText();
+
+                final int selectionStart = Selection.getSelectionStart(text);
+                final int selectionEnd = Selection.getSelectionEnd(text);
+
+                EmojiCompat.get().process(text);
+
+                EmojiInputFilter.updateSelection(text, selectionStart, selectionEnd);
+            }
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java
new file mode 100644
index 0000000..16a4780
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/EmojiTransformationMethod.java
@@ -0,0 +1,76 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.graphics.Rect;
+import android.text.method.TransformationMethod;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.emoji2.text.EmojiCompat;
+
+/**
+ * TransformationMethod wrapper in order to update transformed text with emojis.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+@RequiresApi(19)
+class EmojiTransformationMethod implements TransformationMethod {
+    private final TransformationMethod mTransformationMethod;
+
+    EmojiTransformationMethod(TransformationMethod transformationMethod) {
+        mTransformationMethod = transformationMethod;
+    }
+
+    @Override
+    public CharSequence getTransformation(@Nullable CharSequence source, @NonNull final View view) {
+        if (view.isInEditMode()) {
+            return source;
+        }
+
+        if (mTransformationMethod != null) {
+            source = mTransformationMethod.getTransformation(source, view);
+        }
+
+        if (source != null) {
+            switch (EmojiCompat.get().getLoadState()){
+                case EmojiCompat.LOAD_STATE_SUCCEEDED:
+                    return EmojiCompat.get().process(source);
+                case EmojiCompat.LOAD_STATE_LOADING:
+                case EmojiCompat.LOAD_STATE_FAILED:
+                case EmojiCompat.LOAD_STATE_DEFAULT:
+                default:
+                    break;
+            }
+        }
+        return source;
+    }
+
+    @Override
+    public void onFocusChanged(final View view, final CharSequence sourceText,
+            final boolean focused, final int direction, final Rect previouslyFocusedRect) {
+        if (mTransformationMethod != null) {
+            mTransformationMethod.onFocusChanged(view, sourceText, focused, direction,
+                    previouslyFocusedRect);
+        }
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
new file mode 100644
index 0000000..26070ab
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/ExtractButtonCompat.java
@@ -0,0 +1,74 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.ActionMode;
+import android.widget.Button;
+
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.core.widget.TextViewCompat;
+
+/**
+ * Support library implementation for ExtractButton. Used by {@link EmojiExtractViewHelper} while
+ * inflating {@link EmojiExtractEditText} for keyboard use.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public class ExtractButtonCompat extends Button {
+    public ExtractButtonCompat(Context context) {
+        super(context, null);
+    }
+
+    public ExtractButtonCompat(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public ExtractButtonCompat(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * Pretend like the window this view is in always has focus, so it will
+     * highlight when selected.
+     */
+    @Override
+    public boolean hasWindowFocus() {
+        return isEnabled() && getVisibility() == VISIBLE ? true : false;
+    }
+
+    /**
+     * See
+     * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
+     */
+    @Override
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        super.setCustomSelectionActionModeCallback(TextViewCompat
+                .wrapCustomSelectionActionModeCallback(this, actionModeCallback));
+    }
+}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java
new file mode 100644
index 0000000..97fd4e7
--- /dev/null
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/widget/SpannableBuilder.java
@@ -0,0 +1,436 @@
+/*
+ * 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.widget;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
+
+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 androidx.emoji2.text.EmojiSpan;
+
+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.
+ * <p>
+ * 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
+ * @see EmojiEditableFactory
+ */
+@RestrictTo(LIBRARY_GROUP_PREFIX)
+public final class SpannableBuilder extends SpannableStringBuilder {
+    /**
+     * DynamicLayout$ChangeWatcher class.
+     */
+    private final Class<?> mWatcherClass;
+
+    /**
+     * All WatcherWrappers.
+     */
+    private final List<WatcherWrapper> mWatchers = new ArrayList<>();
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    SpannableBuilder(@NonNull Class<?> watcherClass) {
+        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
+        mWatcherClass = watcherClass;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) {
+        super(text);
+        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
+        mWatcherClass = watcherClass;
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    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
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    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;
+    }
+
+    @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(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.
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> 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(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(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(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(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, Class type) {
+        if (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(LIBRARY_GROUP_PREFIX)
+    public void beginBatchEdit() {
+        blockWatchers();
+    }
+
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP_PREFIX)
+    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());
+        }
+    }
+
+    @Override
+    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
+        blockWatchers();
+        super.replace(start, end, tb);
+        unblockwatchers();
+        return this;
+    }
+
+    @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;
+    }
+
+    @Override
+    public SpannableStringBuilder insert(int where, CharSequence tb) {
+        super.insert(where, tb);
+        return this;
+    }
+
+    @Override
+    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
+        super.insert(where, tb, start, end);
+        return this;
+    }
+
+    @Override
+    public SpannableStringBuilder delete(int start, int end) {
+        super.delete(start, end);
+        return this;
+    }
+
+    @Override
+    public SpannableStringBuilder append(CharSequence text) {
+        super.append(text);
+        return this;
+    }
+
+    @Override
+    public SpannableStringBuilder append(char text) {
+        super.append(text);
+        return this;
+    }
+
+    @Override
+    public SpannableStringBuilder append(CharSequence text, int start, int end) {
+        super.append(text, start, end);
+        return this;
+    }
+
+    @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;
+            }
+            ((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/emoji2/emoji2/src/main/res-public/values/public_attrs.xml b/emoji2/emoji2/src/main/res-public/values/public_attrs.xml
new file mode 100644
index 0000000..56634ff
--- /dev/null
+++ b/emoji2/emoji2/src/main/res-public/values/public_attrs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<!-- Definitions of attributes to be exposed as public -->
+<resources>
+    <public type="attr" name="maxEmojiCount"/>
+    <public type="attr" name="emojiReplaceStrategy"/>
+</resources>
diff --git a/emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml b/emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml
new file mode 100644
index 0000000..0a4d9b0
--- /dev/null
+++ b/emoji2/emoji2/src/main/res/layout/input_method_extract_view.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.emoji2.widget.EmojiExtractEditText
+        android:id="@android:id/inputExtractEditText"
+        android:layout_width="0px"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:gravity="top"
+        android:inputType="text"
+        android:minLines="1"
+        android:scrollbars="vertical"/>
+
+    <FrameLayout
+        android:id="@+id/inputExtractAccessories"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingEnd="8dip"
+        android:paddingStart="8dip">
+
+        <androidx.emoji2.widget.ExtractButtonCompat
+            android:id="@+id/inputExtractAction"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"/>
+
+    </FrameLayout>
+
+</merge>
\ No newline at end of file
diff --git a/emoji2/emoji2/src/main/res/values/attrs.xml b/emoji2/emoji2/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..77254a4
--- /dev/null
+++ b/emoji2/emoji2/src/main/res/values/attrs.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<resources>
+    <declare-styleable name="EmojiEditText">
+        <attr name="maxEmojiCount" format="integer"/>
+    </declare-styleable>
+    <declare-styleable name="EmojiExtractTextLayout">
+        <attr name="emojiReplaceStrategy" format="enum">
+            <!-- Replace strategy that uses the value given in EmojiCompat.Config. Default
+            value. -->
+            <enum name="defaultStrategy" value="0" />
+            <!-- Replace strategy to add EmojiSpans for all emoji that were found. -->
+            <enum name="all" value="1" />
+            <!-- Replace strategy to add EmojiSpans only for emoji that do not exist in the
+            system. -->
+            <enum name="nonExistent" value="2" />
+        </attr>
+    </declare-styleable>
+</resources>
diff --git a/settings.gradle b/settings.gradle
index 40848ca..f2170d8 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -348,6 +348,8 @@
 includeProject(":emoji", "emoji/core", [BuildType.MAIN])
 includeProject(":emoji-appcompat", "emoji/appcompat", [BuildType.MAIN])
 includeProject(":emoji-bundled", "emoji/bundled", [BuildType.MAIN])
+includeProject(":emoji2:emoji2", "emoji2/emoji2", [BuildType.MAIN])
+includeProject(":emoji2:emoji2-bundled", "emoji2/emoji2-bundled", [BuildType.MAIN])
 includeProject(":enterprise-feedback", "enterprise/feedback", [BuildType.MAIN])
 includeProject(":enterprise-feedback-testing", "enterprise/feedback/testing", [BuildType.MAIN])
 includeProject(":exifinterface:exifinterface", "exifinterface/exifinterface", [BuildType.MAIN])