Make widgets no-op for sdk < 19

Test: Existing tests passes, added more tests.
Bug: 35811035
Change-Id: I95197304ca1d7a2536358c6bc30c6d2160568ad3
diff --git a/api/26.0.0.txt b/api/26.0.0.txt
index 5b98882..c1b6f89 100644
--- a/api/26.0.0.txt
+++ b/api/26.0.0.txt
@@ -1854,9 +1854,9 @@
   public final class EmojiTextViewHelper {
     ctor public EmojiTextViewHelper(android.widget.TextView);
     method public android.text.InputFilter[] getFilters(android.text.InputFilter[]);
-    method public android.text.method.TransformationMethod getTransformationMethod(android.text.method.TransformationMethod);
     method public void setAllCaps(boolean);
     method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod wrapTransformationMethod(android.text.method.TransformationMethod);
   }
 
 }
diff --git a/api/current.txt b/api/current.txt
index 8757fb5..6826c22 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1854,9 +1854,9 @@
   public final class EmojiTextViewHelper {
     ctor public EmojiTextViewHelper(android.widget.TextView);
     method public android.text.InputFilter[] getFilters(android.text.InputFilter[]);
-    method public android.text.method.TransformationMethod getTransformationMethod(android.text.method.TransformationMethod);
     method public void setAllCaps(boolean);
     method public void updateTransformationMethod();
+    method public android.text.method.TransformationMethod wrapTransformationMethod(android.text.method.TransformationMethod);
   }
 
 }
diff --git a/emoji/core/src/android/support/text/emoji/widget/EmojiEditText.java b/emoji/core/src/android/support/text/emoji/widget/EmojiEditText.java
index 3e9153b..3743cee 100644
--- a/emoji/core/src/android/support/text/emoji/widget/EmojiEditText.java
+++ b/emoji/core/src/android/support/text/emoji/widget/EmojiEditText.java
@@ -59,8 +59,8 @@
     }
 
     @Override
-    public void setKeyListener(android.text.method.KeyListener input) {
-        super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input));
+    public void setKeyListener(android.text.method.KeyListener keyListener) {
+        super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener));
     }
 
     @Override
diff --git a/emoji/core/src/android/support/text/emoji/widget/EmojiEditTextHelper.java b/emoji/core/src/android/support/text/emoji/widget/EmojiEditTextHelper.java
index c3d5e84..0c3574c 100644
--- a/emoji/core/src/android/support/text/emoji/widget/EmojiEditTextHelper.java
+++ b/emoji/core/src/android/support/text/emoji/widget/EmojiEditTextHelper.java
@@ -15,7 +15,10 @@
  */
 package android.support.text.emoji.widget;
 
+import android.os.Build;
 import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.support.text.emoji.EmojiCompat;
 import android.support.v4.util.Preconditions;
 import android.text.method.KeyListener;
 import android.view.inputmethod.EditorInfo;
@@ -24,11 +27,43 @@
 import android.widget.TextView;
 
 /**
- * Utility class to enhance an EditText with emoji capability.
+ * 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 EditText mEditText;
-    private final EmojiTextWatcher mTextWatcher;
+
+    private final HelperInternal mHelper;
 
     /**
      * Default constructor.
@@ -37,41 +72,30 @@
      */
     public EmojiEditTextHelper(@NonNull final EditText editText) {
         Preconditions.checkNotNull(editText, "editText cannot be null");
-        mEditText = editText;
-        mTextWatcher = new EmojiTextWatcher(mEditText);
-        editText.addTextChangedListener(mTextWatcher);
-        editText.setEditableFactory(EmojiEditableFactory.getInstance());
+        mHelper = Build.VERSION.SDK_INT >= 19 ? new HelperInternal19(editText)
+                : new HelperInternal();
     }
 
     /**
      * Attaches EmojiCompat KeyListener to the widget. Should be called from {@link
      * TextView#setKeyListener(KeyListener)}. Existing keyListener is wrapped into EmojiCompat
-     * KeyListener.
-     * <p/>
-     * <pre><code> {@literal @}Override
-     * public void setKeyListener(android.text.method.KeyListener input) {
-     *     super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input));
-     * }</code></pre>
+     * 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 new EmojiKeyListener(keyListener);
+        return mHelper.getKeyListener(keyListener);
     }
 
     /**
      * Updates the InputConnection with emoji support. Should be called from {@link
-     * TextView#onCreateInputConnection(EditorInfo)}.
-     * <p/>
-     * <pre><code> {@literal @}Override
-     * public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
-     *     InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
-     *     return getEmojiHelper().onCreateInputConnection(inputConnection, outAttrs);
-     * }</code></pre>
+     * TextView#onCreateInputConnection(EditorInfo)}. When used on devices running API 18 or below,
+     * this method returns {@code inputConnection} that is given as a parameter.
      *
      * @param inputConnection InputConnection instance created by TextView
      * @param outAttrs        EditorInfo passed into
@@ -79,9 +103,52 @@
      *
      * @return a new InputConnection instance that wraps {@code inputConnection}
      */
+    @NonNull
     public InputConnection onCreateInputConnection(@NonNull final InputConnection inputConnection,
             @NonNull final EditorInfo outAttrs) {
         Preconditions.checkNotNull(inputConnection, "inputConnection cannot be null");
-        return new EmojiInputConnection(mEditText, inputConnection, outAttrs);
+        return mHelper.onCreateInputConnection(inputConnection, outAttrs);
+    }
+
+    private static class HelperInternal {
+
+        KeyListener getKeyListener(@NonNull KeyListener keyListener) {
+            return keyListener;
+        }
+
+        InputConnection onCreateInputConnection(@NonNull InputConnection inputConnection,
+                @NonNull EditorInfo outAttrs) {
+            return inputConnection;
+        }
+    }
+
+    @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
+        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/emoji/core/src/android/support/text/emoji/widget/EmojiTextViewHelper.java b/emoji/core/src/android/support/text/emoji/widget/EmojiTextViewHelper.java
index e88cc87..abd7410 100644
--- a/emoji/core/src/android/support/text/emoji/widget/EmojiTextViewHelper.java
+++ b/emoji/core/src/android/support/text/emoji/widget/EmojiTextViewHelper.java
@@ -15,7 +15,11 @@
  */
 package android.support.text.emoji.widget;
 
+import android.os.Build;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.text.emoji.EmojiCompat;
 import android.support.v4.util.Preconditions;
 import android.text.InputFilter;
 import android.text.method.PasswordTransformationMethod;
@@ -23,11 +27,41 @@
 import android.widget.TextView;
 
 /**
- * Utility class to enhance a TextView with emoji capability.
+ * 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 TextView mTextView;
-    private final EmojiInputFilter mEmojiInputFilter;
+
+    private final HelperInternal mHelper;
 
     /**
      * Default constructor.
@@ -36,77 +70,126 @@
      */
     public EmojiTextViewHelper(@NonNull TextView textView) {
         Preconditions.checkNotNull(textView, "textView cannot be null");
-        mTextView = textView;
-        mEmojiInputFilter = new EmojiInputFilter(textView);
+        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.
+     * Should be called in the widget constructor. When used on devices running API 18 or below,
+     * this method does nothing.
      *
-     * @see #getTransformationMethod(TransformationMethod)
+     * @see #wrapTransformationMethod(TransformationMethod)
      */
     public void updateTransformationMethod() {
-        final TransformationMethod transformationMethod = mTextView.getTransformationMethod();
-        if (transformationMethod != null
-                && !(transformationMethod instanceof PasswordTransformationMethod)) {
-            mTextView.setTransformationMethod(getTransformationMethod(transformationMethod));
-        }
+        mHelper.updateTransformationMethod();
     }
 
     /**
      * Appends EmojiCompat InputFilters to the widget InputFilters. Should be called by {@link
-     * TextView#setFilters(InputFilter[])} to update the InputFilters.
-     * <p/>
-     * <pre><code> {@literal @}Override
-     * public void setFilters(InputFilter[] filters) {
-     *     super.setFilters(getEmojiTextViewHelper().getFilters(filters));
-     * }</code></pre>
+     * 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) {
-        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;
+        return mHelper.getFilters(filters);
     }
 
     /**
-     * Returns transformation method that can update the transformed text to display emojis.
+     * 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
      */
-    public TransformationMethod getTransformationMethod(
-            final TransformationMethod transformationMethod) {
-        return new EmojiTransformationMethod(transformationMethod);
+    @Nullable
+    public TransformationMethod wrapTransformationMethod(
+            @Nullable TransformationMethod transformationMethod) {
+        return mHelper.wrapTransformationMethod(transformationMethod);
     }
 
     /**
-     * Call when allCaps is set on TextView.
-     * <p/>
-     * <pre><code> {@literal @}Override
-     * public void setAllCaps(boolean allCaps) {
-     *     super.setAllCaps(allCaps);
-     *     getEmojiTextViewHelper().setAllCaps(allCaps);
-     * }</code></pre>
+     * 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) {
-        // 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();
+        mHelper.setAllCaps(allCaps);
+    }
+
+    private 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/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperPre19Test.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperPre19Test.java
new file mode 100644
index 0000000..a607801
--- /dev/null
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperPre19Test.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.support.text.emoji.widget;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.method.KeyListener;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+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);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_returnsSameInputConnection() {
+        final InputConnection param = mock(InputConnection.class);
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(param,
+                null);
+
+        assertSame(param, inputConnection);
+    }
+
+}
diff --git a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperTest.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperTest.java
new file mode 100644
index 0000000..b7d8f5b
--- /dev/null
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiEditTextHelperTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.support.text.emoji.widget;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+import android.annotation.TargetApi;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.text.emoji.EmojiCompat;
+import android.text.method.KeyListener;
+import android.view.inputmethod.InputConnection;
+import android.widget.EditText;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+@TargetApi(19)
+public class EmojiEditTextHelperTest {
+    EmojiEditTextHelper mEmojiEditTextHelper;
+    EditText mEditText;
+
+    @Before
+    public void setup() {
+        EmojiCompat.reset(mock(EmojiCompat.class));
+        mEditText = new EditText(InstrumentationRegistry.getTargetContext());
+        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(expected = NullPointerException.class)
+    public void testGetOnCreateInputConnection_withNull_throwsException() {
+        mEmojiEditTextHelper.onCreateInputConnection(null, null);
+    }
+
+    @Test
+    public void testGetOnCreateInputConnection_returnsEmojiInputConnection() {
+        final InputConnection inputConnection = mEmojiEditTextHelper.onCreateInputConnection(
+                mock(InputConnection.class), null);
+
+        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);
+    }
+}
diff --git a/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperPre19Test.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperPre19Test.java
new file mode 100644
index 0000000..6d68aad
--- /dev/null
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperPre19Test.java
@@ -0,0 +1,86 @@
+/*
+ * 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 android.support.text.emoji.widget;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.InputFilter;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+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(InstrumentationRegistry.getTargetContext());
+        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/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperTest.java b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperTest.java
new file mode 100644
index 0000000..9a5ccbd
--- /dev/null
+++ b/emoji/core/tests/java/android/support/text/emoji/widget/EmojiTextViewHelperTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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 android.support.text.emoji.widget;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.assertEquals;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+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.annotation.TargetApi;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.InputFilter;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.TransformationMethod;
+import android.widget.TextView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
+@TargetApi(19)
+public class EmojiTextViewHelperTest {
+    EmojiTextViewHelper mTextViewHelper;
+    TextView mTextView;
+
+    @Before
+    public void setup() {
+        mTextView = new TextView(InstrumentationRegistry.getTargetContext());
+        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());
+    }
+}