Merge "Fix an IOOB exception"
diff --git a/java/res/values/strings-emoji-descriptions.xml b/java/res/values/strings-emoji-descriptions.xml
index 7952a7f..8cbde26 100644
--- a/java/res/values/strings-emoji-descriptions.xml
+++ b/java/res/values/strings-emoji-descriptions.xml
@@ -17,6 +17,10 @@
 ** limitations under the License.
 */
 -->
+<!--
+    These Emoji symbols are unsupported by TTS.
+    TODO: Remove this file when TTS/TalkBack support these Emoji symbols.
+-->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
      <!-- Spoken description for Unicode code point U+00A9: "©" COPYRIGHT SIGN -->
      <string name="spoken_emoji_00A9">Copyright sign</string>
diff --git a/java/res/values/strings-letter-descriptions.xml b/java/res/values/strings-letter-descriptions.xml
index fbf4671..297b6be 100644
--- a/java/res/values/strings-letter-descriptions.xml
+++ b/java/res/values/strings-letter-descriptions.xml
@@ -17,7 +17,11 @@
 ** limitations under the License.
 */
 -->
-<!-- TODO: Remove this file when TTS/TalkBack support these letters. -->
+<!--
+    These accented letters (spoken_accented_letter_*) are unsupported by TTS.
+    These symbols (spoken_symbol_*) are also unsupported by TTS.
+    TODO: Remove these string resources when TTS/TalkBack support these letters.
+-->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Spoken description for Unicode code point U+00AA: "ª" FEMININE ORDINAL INDICATOR -->
     <string name="spoken_accented_letter_00AA">Feminine ordinal indicator</string>
@@ -319,4 +323,62 @@
     <string name="spoken_accented_letter_1EF7">Y, hook above</string>
     <!-- Spoken description for Unicode code point U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE -->
     <string name="spoken_accented_letter_1EF9">Y, tilde</string>
+    <!-- Spoken description for Unicode code point U+00A1: "¡" INVERTED EXCLAMATION MARK -->
+    <string name="spoken_symbol_00A1">Inverted exclamation mark</string>
+    <!-- Spoken description for Unicode code point U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK -->
+    <string name="spoken_symbol_00AB">Left-pointing double angle quotation mark</string>
+    <!-- Spoken description for Unicode code point U+00B7: "·" MIDDLE DOT -->
+    <string name="spoken_symbol_00B7">Middle dot</string>
+    <!-- Spoken description for Unicode code point U+00B9: "¹" SUPERSCRIPT ONE -->
+    <string name="spoken_symbol_00B9">Superscript one</string>
+    <!-- Spoken description for Unicode code point U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK -->
+    <string name="spoken_symbol_00BB">Right-pointing double angle quotation mark</string>
+    <!-- Spoken description for Unicode code point U+00BF: "¿" INVERTED QUESTION MARK -->
+    <string name="spoken_symbol_00BF">Inverted question mark</string>
+    <!-- Spoken description for Unicode code point U+2018: "‘" LEFT SINGLE QUOTATION MARK -->
+    <string name="spoken_symbol_2018">Left single quotation mark</string>
+    <!-- Spoken description for Unicode code point U+2019: "’" RIGHT SINGLE QUOTATION MARK -->
+    <string name="spoken_symbol_2019">Right single quotation mark</string>
+    <!-- Spoken description for Unicode code point U+201A: "‚" SINGLE LOW-9 QUOTATION MARK -->
+    <string name="spoken_symbol_201A">Single low-9 quotation mark</string>
+    <!-- Spoken description for Unicode code point U+201C: "“" LEFT DOUBLE QUOTATION MARK -->
+    <string name="spoken_symbol_201C">Left double quotation mark</string>
+    <!-- Spoken description for Unicode code point U+201D: "”" RIGHT DOUBLE QUOTATION MARK -->
+    <string name="spoken_symbol_201D">Right double quotation mark</string>
+    <!-- Spoken description for Unicode code point U+2020: "†" DAGGER -->
+    <string name="spoken_symbol_2020">Dagger</string>
+    <!-- Spoken description for Unicode code point U+2021: "‡" DOUBLE DAGGER -->
+    <string name="spoken_symbol_2021">Double dagger</string>
+    <!-- Spoken description for Unicode code point U+2030: "‰" PER MILLE SIGN -->
+    <string name="spoken_symbol_2030">Per mille sign</string>
+    <!-- Spoken description for Unicode code point U+2032: "′" PRIME -->
+    <string name="spoken_symbol_2032">Prime</string>
+    <!-- Spoken description for Unicode code point U+2033: "″" DOUBLE PRIME -->
+    <string name="spoken_symbol_2033">Double prime</string>
+    <!-- Spoken description for Unicode code point U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK -->
+    <string name="spoken_symbol_2039">Single left-pointing angle quotation mark</string>
+    <!-- Spoken description for Unicode code point U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK -->
+    <string name="spoken_symbol_203A">Single right-pointing angle quotation mark</string>
+    <!-- Spoken description for Unicode code point U+2074: "⁴" SUPERSCRIPT FOUR -->
+    <string name="spoken_symbol_2074">Superscript four</string>
+    <!-- Spoken description for Unicode code point U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N -->
+    <string name="spoken_symbol_207F">Superscript latin small letter n</string>
+    <!-- Spoken description for Unicode code point U+20B1: "₱" PESO SIGN -->
+    <string name="spoken_symbol_20B1">Peso sign</string>
+    <!-- Spoken description for Unicode code point U+2105: "℅" CARE OF -->
+    <string name="spoken_symbol_2105">Care of</string>
+    <!-- Spoken description for Unicode code point U+2192: "→" RIGHTWARDS ARROW -->
+    <string name="spoken_symbol_2192">Rightwards arrow</string>
+    <!-- Spoken description for Unicode code point U+2193: "↓" DOWNWARDS ARROW -->
+    <string name="spoken_symbol_2193">Downwards arrow</string>
+    <!-- Spoken description for Unicode code point U+2205: "∅" EMPTY SET -->
+    <string name="spoken_symbol_2205">Empty set</string>
+    <!-- Spoken description for Unicode code point U+2206: "∆" INCREMENT -->
+    <string name="spoken_symbol_2206">Increment</string>
+    <!-- Spoken description for Unicode code point U+2264: "≤" LESS-THAN OR EQUAL TO -->
+    <string name="spoken_symbol_2264">Less-than or equal to</string>
+    <!-- Spoken description for Unicode code point U+2265: "≥" GREATER-THAN OR EQUAL TO -->
+    <string name="spoken_symbol_2265">Greater-than or equal to</string>
+    <!-- Spoken description for Unicode code point U+2605: "★" BLACK STAR -->
+    <string name="spoken_symbol_2605">Black star</string>
 </resources>
diff --git a/java/res/values/strings-talkback-descriptions.xml b/java/res/values/strings-talkback-descriptions.xml
index d7978b0..fa06362 100644
--- a/java/res/values/strings-talkback-descriptions.xml
+++ b/java/res/values/strings-talkback-descriptions.xml
@@ -32,7 +32,7 @@
     <string name="spoken_auto_correct_obscured"><xliff:g id="KEY_NAME" example="Space">%1$s</xliff:g> performs auto-correction</string>
 
     <!-- Spoken description for unknown keyboard keys. -->
-    <string name="spoken_description_unknown">Key code %d</string>
+    <string name="spoken_description_unknown">Unknown character</string>
     <!-- Spoken description for the "Shift" keyboard key when "Shift" is off. -->
     <string name="spoken_description_shift">Shift</string>
     <!-- Spoken description for the "Shift" keyboard key in symbols mode. -->
@@ -135,6 +135,8 @@
     <!-- Spoken description for Unicode code point U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
          Note that depending on locale, the lower-case of this letter is U+0069 or U+0131. -->
     <string name="spoken_letter_0130">Capital I, dot above</string>
+    <!-- Spoken description for unknown symbol code point. -->
+    <string name="spoken_symbol_unknown">Unknown symbol</string>
     <!-- Spoken description for unknown emoji code point. -->
     <string name="spoken_emoji_unknown">Unknown emoji</string>
 </resources>
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 58672ac..27c4732 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -34,6 +34,7 @@
 public final class KeyCodeDescriptionMapper {
     private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
     private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
+    private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
     private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
 
     // The resource ID of the string spoken for obscured keys
@@ -290,6 +291,10 @@
             return accentedLetter;
         }
         // Here, <code>code</code> may be a base (non-accented) letter.
+        final String unsupportedSymbol = getSpokenSymbolDescription(context, code);
+        if (unsupportedSymbol != null) {
+            return unsupportedSymbol;
+        }
         final String emojiDescription = getSpokenEmojiDescription(context, code);
         if (emojiDescription != null) {
             return emojiDescription;
@@ -303,6 +308,7 @@
         return context.getString(R.string.spoken_description_unknown, code);
     }
 
+    // TODO: Remove this method once TTS supports those accented letters' verbalization.
     private String getSpokenAccentedLetterDescription(final Context context, final int code) {
         final boolean isUpperCase = Character.isUpperCase(code);
         final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
@@ -317,14 +323,32 @@
                 : spokenText;
     }
 
+    // TODO: Remove this method once TTS supports those symbols' verbalization.
+    private String getSpokenSymbolDescription(final Context context, final int code) {
+        final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
+        if (resId == 0) {
+            return null;
+        }
+        final String spokenText = context.getString(resId);
+        if (!TextUtils.isEmpty(spokenText)) {
+            return spokenText;
+        }
+        // If a translated description is empty, fall back to unknown symbol description.
+        return context.getString(R.string.spoken_symbol_unknown);
+    }
+
+    // TODO: Remove this method once TTS supports emoji verbalization.
     private String getSpokenEmojiDescription(final Context context, final int code) {
         final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
         if (resId == 0) {
             return null;
         }
         final String spokenText = context.getString(resId);
-        return TextUtils.isEmpty(spokenText) ? context.getString(R.string.spoken_emoji_unknown)
-                : spokenText;
+        if (!TextUtils.isEmpty(spokenText)) {
+            return spokenText;
+        }
+        // If a translated description is empty, fall back to unknown emoji description.
+        return context.getString(R.string.spoken_emoji_unknown);
     }
 
     private int getSpokenDescriptionId(final Context context, final int code,
diff --git a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java
index 7cae986..398a933 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityDelegate.java
@@ -39,7 +39,7 @@
     protected final KeyDetector mKeyDetector;
     private Keyboard mKeyboard;
     private KeyboardAccessibilityNodeProvider mAccessibilityNodeProvider;
-    private Key mCurrentHoverKey;
+    private Key mLastHoverKey;
 
     public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
         super();
@@ -71,12 +71,12 @@
         return mKeyboard;
     }
 
-    protected final void setCurrentHoverKey(final Key key) {
-        mCurrentHoverKey = key;
+    protected final void setLastHoverKey(final Key key) {
+        mLastHoverKey = key;
     }
 
-    protected final Key getCurrentHoverKey() {
-        return mCurrentHoverKey;
+    protected final Key getLastHoverKey() {
+        return mLastHoverKey;
     }
 
     /**
@@ -142,7 +142,7 @@
      * @param event The hover event.
      * @return key The key that the <code>event</code> is on.
      */
-    protected final Key getHoverKey(final MotionEvent event) {
+    protected final Key getHoverKeyOf(final MotionEvent event) {
         final int actionIndex = event.getActionIndex();
         final int x = (int)event.getX(actionIndex);
         final int y = (int)event.getY(actionIndex);
@@ -179,11 +179,11 @@
      * @param event A hover enter event.
      */
     protected void onHoverEnter(final MotionEvent event) {
-        final Key key = getHoverKey(event);
+        final Key key = getHoverKeyOf(event);
         if (key != null) {
-            onHoverEnterKey(key);
+            onHoverEnterKey(key, event);
         }
-        setCurrentHoverKey(key);
+        setLastHoverKey(key);
     }
 
     /**
@@ -192,20 +192,20 @@
      * @param event A hover move event.
      */
     protected void onHoverMove(final MotionEvent event) {
-        final Key previousKey = getCurrentHoverKey();
-        final Key key = getHoverKey(event);
-        if (key != previousKey) {
-            if (previousKey != null) {
-                onHoverExitKey(previousKey);
+        final Key lastKey = getLastHoverKey();
+        final Key key = getHoverKeyOf(event);
+        if (key != lastKey) {
+            if (lastKey != null) {
+                onHoverExitKey(lastKey, event);
             }
             if (key != null) {
-                onHoverEnterKey(key);
+                onHoverEnterKey(key, event);
             }
         }
         if (key != null) {
-            onHoverMoveKey(key);
+            onHoverMoveKey(key, event);
         }
-        setCurrentHoverKey(key);
+        setLastHoverKey(key);
     }
 
     /**
@@ -214,15 +214,19 @@
      * @param event A hover exit event.
      */
     protected void onHoverExit(final MotionEvent event) {
-        final Key key = getHoverKey(event);
+        final Key lastKey = getLastHoverKey();
+        if (lastKey != null) {
+            onHoverExitKey(lastKey, event);
+        }
+        final Key key = getHoverKeyOf(event);
         // Make sure we're not getting an EXIT event because the user slid
         // off the keyboard area, then force a key press.
         if (key != null) {
             simulateTouchEvent(MotionEvent.ACTION_DOWN, event);
             simulateTouchEvent(MotionEvent.ACTION_UP, event);
-            onHoverExitKey(key);
+            onHoverExitKey(key, event);
         }
-        setCurrentHoverKey(null);
+        setLastHoverKey(null);
     }
 
     /**
@@ -263,8 +267,9 @@
      * Handles a hover enter event on a key.
      *
      * @param key The currently hovered key.
+     * @param event The hover event that triggers a call to this method.
      */
-    protected void onHoverEnterKey(final Key key) {
+    protected void onHoverEnterKey(final Key key, final MotionEvent event) {
         key.onPressed();
         mKeyboardView.invalidateKey(key);
         final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();
@@ -276,15 +281,17 @@
      * Handles a hover move event on a key.
      *
      * @param key The currently hovered key.
+     * @param event The hover event that triggers a call to this method.
      */
-    protected void onHoverMoveKey(final Key key) { }
+    protected void onHoverMoveKey(final Key key, final MotionEvent event) { }
 
     /**
      * Handles a hover exit event on a key.
      *
      * @param key The currently hovered key.
+     * @param event The hover event that triggers a call to this method.
      */
-    protected void onHoverExitKey(final Key key) {
+    protected void onHoverExitKey(final Key key, final MotionEvent event) {
         key.onReleased();
         mKeyboardView.invalidateKey(key);
         final KeyboardAccessibilityNodeProvider provider = getAccessibilityNodeProvider();