diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 0499a34569ebf444ee7abd1da96fdf026b6cc25f..46caef62548d5697a3b5757b79de3549d8804207 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.accessibility;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -27,21 +28,18 @@ import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.CollectionUtils;
 
-import java.util.HashMap;
+import java.util.Locale;
 
 public final class KeyCodeDescriptionMapper {
     private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
+    private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
 
     // The resource ID of the string spoken for obscured keys
     private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
 
     private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
 
-    // Map of key labels to spoken description resource IDs
-    private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap();
-
     // Sparse array of spoken description resource IDs indexed by key codes
     private final SparseIntArray mKeyCodeMap;
 
@@ -114,17 +112,12 @@ public final class KeyCodeDescriptionMapper {
             return getDescriptionForActionKey(context, keyboard, key);
         }
 
-        if (!TextUtils.isEmpty(key.getLabel())) {
-            final String label = key.getLabel().trim();
-
-            // First, attempt to map the label to a pre-defined description.
-            if (mKeyLabelMap.containsKey(label)) {
-                return context.getString(mKeyLabelMap.get(label));
-            }
+        if (code == Constants.CODE_OUTPUT_TEXT) {
+            return key.getOutputText();
         }
 
         // Just attempt to speak the description.
-        if (key.getCode() != Constants.CODE_UNSPECIFIED) {
+        if (code != Constants.CODE_UNSPECIFIED) {
             return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
         }
         return null;
@@ -139,7 +132,7 @@ public final class KeyCodeDescriptionMapper {
      * @param keyboard The keyboard on which the key resides.
      * @return a character sequence describing the action performed by pressing the key
      */
-    private String getDescriptionForSwitchAlphaSymbol(final Context context,
+    private static String getDescriptionForSwitchAlphaSymbol(final Context context,
             final Keyboard keyboard) {
         final KeyboardId keyboardId = keyboard.mId;
         final int elementId = keyboardId.mElementId;
@@ -177,7 +170,8 @@ public final class KeyCodeDescriptionMapper {
      * @param keyboard The keyboard on which the key resides.
      * @return A context-sensitive description of the "Shift" key.
      */
-    private String getDescriptionForShiftKey(final Context context, final Keyboard keyboard) {
+    private static String getDescriptionForShiftKey(final Context context,
+            final Keyboard keyboard) {
         final KeyboardId keyboardId = keyboard.mId;
         final int elementId = keyboardId.mElementId;
         final int resId;
@@ -211,7 +205,7 @@ public final class KeyCodeDescriptionMapper {
      * @param key The key to describe.
      * @return Returns a context-sensitive description of the "Enter" action key.
      */
-    private String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
+    private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
             final Key key) {
         final KeyboardId keyboardId = keyboard.mId;
         final int actionId = keyboardId.imeAction();
@@ -280,6 +274,13 @@ public final class KeyCodeDescriptionMapper {
         if (mKeyCodeMap.indexOfKey(code) >= 0) {
             return context.getString(mKeyCodeMap.get(code));
         }
+        final int spokenEmojiId = getSpokenDescriptionId(
+                context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
+        if (spokenEmojiId != 0) {
+            final String spokenEmoji = context.getString(spokenEmojiId);
+            mKeyCodeMap.append(code, spokenEmojiId);
+            return spokenEmoji;
+        }
         if (isDefinedNonCtrl) {
             return Character.toString((char) code);
         }
@@ -288,4 +289,13 @@ public final class KeyCodeDescriptionMapper {
         }
         return context.getString(R.string.spoken_description_unknown, code);
     }
+
+    private static int getSpokenDescriptionId(final Context context, final int code,
+            final String resourceNameFormat) {
+        final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
+        final Resources resources = context.getResources();
+        final String packageName = resources.getResourcePackageName(
+                R.string.spoken_description_unknown);
+        return resources.getIdentifier(resourceName, "string", packageName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
index d14ffeef9bdbe7a7a411b23eff2dd524ba7eb946..48efa17ade3c678ee1ab61101d68694db92328f6 100644
--- a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
@@ -22,6 +22,8 @@ import android.util.AttributeSet;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.accessibility.KeyboardAccessibilityDelegate;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
@@ -53,6 +55,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
     private OnKeyEventListener mListener = EMPTY_LISTENER;
     private final KeyDetector mKeyDetector = new KeyDetector();
     private final GestureDetector mGestureDetector;
+    private final KeyboardAccessibilityDelegate<EmojiPageKeyboardView> mAccessibilityDelegate;
 
     public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) {
         this(context, attrs, R.attr.keyboardViewStyle);
@@ -64,6 +67,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
         mGestureDetector = new GestureDetector(context, this);
         mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */);
         mHandler = new Handler();
+        mAccessibilityDelegate = new KeyboardAccessibilityDelegate<>(this, mKeyDetector);
     }
 
     public void setOnKeyEventListener(final OnKeyEventListener listener) {
@@ -79,6 +83,15 @@ final class EmojiPageKeyboardView extends KeyboardView implements
         mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
     }
 
+    @Override
+    public boolean dispatchHoverEvent(final MotionEvent event) {
+        if (!AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+            // Reflection doesn't support calling superclass methods.
+            return false;
+        }
+        return mAccessibilityDelegate.dispatchHoverEvent(event);
+    }
+
     /**
      * {@inheritDoc}
      */