Merge "Use RichInputConnection to get the previous word."
diff --git a/java/res/values-kk/strings-talkback-descriptions.xml b/java/res/values-kk/strings-talkback-descriptions.xml
index 13adf83..39388a4 100644
--- a/java/res/values-kk/strings-talkback-descriptions.xml
+++ b/java/res/values-kk/strings-talkback-descriptions.xml
@@ -27,7 +27,8 @@
     <skip />
     <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
     <skip />
-    <string name="spoken_description_unknown" msgid="3197434010402179157">"Перне коды %d"</string>
+    <!-- no translation found for spoken_description_unknown (5139930082759824442) -->
+    <skip />
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift қосулы (өшіру үшін түрту)"</string>
     <string name="spoken_description_caps_lock" msgid="3276478269526304432">"Caps lock қосулы (өшіру үшін түрту)"</string>
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 3a64531..7a3510e 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -28,6 +28,7 @@
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.util.Locale;
 
@@ -79,14 +80,6 @@
     /**
      * Returns the localized description of the action performed by a specified
      * key based on the current keyboard state.
-     * <p>
-     * The order of precedence for key descriptions is:
-     * <ol>
-     * <li>Manually-defined based on the key label</li>
-     * <li>Automatic or manually-defined based on the key code</li>
-     * <li>Automatically based on the key label</li>
-     * <li>{code null} for keys with no label or key code defined</li>
-     * </p>
      *
      * @param context The package's context.
      * @param keyboard The keyboard on which the key resides.
@@ -121,7 +114,20 @@
 
         // Just attempt to speak the description.
         if (code != Constants.CODE_UNSPECIFIED) {
-            return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
+            // If the key description should be obscured, now is the time to do it.
+            final boolean isDefinedNonCtrl = Character.isDefined(code)
+                    && !Character.isISOControl(code);
+            if (shouldObscure && isDefinedNonCtrl) {
+                return context.getString(OBSCURED_KEY_RES_ID);
+            }
+            final String description = getDescriptionForCodePoint(context, code);
+            if (description != null) {
+                return description;
+            }
+            if (!TextUtils.isEmpty(key.getLabel())) {
+                return key.getLabel();
+            }
+            return context.getString(R.string.spoken_description_unknown);
         }
         return null;
     }
@@ -247,57 +253,35 @@
 
     /**
      * Returns a localized character sequence describing what will happen when
-     * the specified key is pressed based on its key code.
-     * <p>
-     * The order of precedence for key code descriptions is:
-     * <ol>
-     * <li>Manually-defined shift-locked description</li>
-     * <li>Manually-defined shifted description</li>
-     * <li>Manually-defined normal description</li>
-     * <li>Automatic based on the character represented by the key code</li>
-     * <li>Fall-back for undefined or control characters</li>
-     * </ol>
-     * </p>
+     * the specified key is pressed based on its key code point.
      *
      * @param context The package's context.
-     * @param keyboard The keyboard on which the key resides.
-     * @param key The key from which to obtain a description.
-     * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
-     * @return a character sequence describing the action performed by pressing the key
+     * @param codePoint The code point from which to obtain a description.
+     * @return a character sequence describing the code point.
      */
-    private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard,
-            final Key key, final boolean shouldObscure) {
-        final int code = key.getCode();
-
+    public String getDescriptionForCodePoint(final Context context, final int codePoint) {
         // If the key description should be obscured, now is the time to do it.
-        final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code);
-        if (shouldObscure && isDefinedNonCtrl) {
-            return context.getString(OBSCURED_KEY_RES_ID);
-        }
-        final int index = mKeyCodeMap.indexOfKey(code);
+        final int index = mKeyCodeMap.indexOfKey(codePoint);
         if (index >= 0) {
             return context.getString(mKeyCodeMap.valueAt(index));
         }
-        final String accentedLetter = getSpokenAccentedLetterDescription(context, code);
+        final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
         if (accentedLetter != null) {
             return accentedLetter;
         }
         // Here, <code>code</code> may be a base (non-accented) letter.
-        final String unsupportedSymbol = getSpokenSymbolDescription(context, code);
+        final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
         if (unsupportedSymbol != null) {
             return unsupportedSymbol;
         }
-        final String emojiDescription = getSpokenEmojiDescription(context, code);
+        final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
         if (emojiDescription != null) {
             return emojiDescription;
         }
-        if (isDefinedNonCtrl) {
-            return Character.toString((char) code);
+        if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
+            return StringUtils.newSingleCodePointString(codePoint);
         }
-        if (!TextUtils.isEmpty(key.getLabel())) {
-            return key.getLabel();
-        }
-        return context.getString(R.string.spoken_description_unknown, code);
+        return null;
     }
 
     // TODO: Remove this method once TTS supports those accented letters' verbalization.
diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
index 4fdf5b8..96f84dd 100644
--- a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
+++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.accessibility;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.SparseIntArray;
@@ -58,7 +59,8 @@
     /** The most recently set keyboard mode. */
     private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
     private static final int KEYBOARD_IS_HIDDEN = -1;
-    private boolean mShouldIgnoreOnRegisterHoverKey;
+    // The rectangle region to ignore hover events.
+    private final Rect mBoundsToIgnoreHoverEvent = new Rect();
 
     private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
 
@@ -154,14 +156,28 @@
         case KeyboardId.ELEMENT_ALPHABET:
             if (lastElementId == KeyboardId.ELEMENT_ALPHABET
                     || lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+                // Transition between alphabet mode and automatic shifted mode should be silently
+                // ignored because it can be determined by each key's talk back announce.
                 return;
             }
             resId = R.string.spoken_description_mode_alpha;
             break;
         case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+            if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+                // Resetting automatic shifted mode by pressing the shift key causes the transition
+                // from automatic shifted to manual shifted that should be silently ignored.
+                return;
+            }
             resId = R.string.spoken_description_shiftmode_on;
             break;
         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+            if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
+                // Resetting caps locked mode by pressing the shift key causes the transition
+                // from shift locked to shift lock shifted that should be silently ignored.
+                return;
+            }
+            resId = R.string.spoken_description_shiftmode_locked;
+            break;
         case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
             resId = R.string.spoken_description_shiftmode_locked;
             break;
@@ -192,31 +208,49 @@
 
     @Override
     protected void onRegisterHoverKey(final Key key, final MotionEvent event) {
+        final int x = key.getHitBox().centerX();
+        final int y = key.getHitBox().centerY();
         if (DEBUG_HOVER) {
-            Log.d(TAG, "onRegisterHoverKey: key=" + key + " ignore="
-                    + mShouldIgnoreOnRegisterHoverKey);
+            Log.d(TAG, "onRegisterHoverKey: key=" + key
+                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
         }
-        if (!mShouldIgnoreOnRegisterHoverKey) {
-            super.onRegisterHoverKey(key, event);
+        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
+            // This hover exit event points to the key that should be ignored.
+            // Clear the ignoring region to handle further hover events.
+            mBoundsToIgnoreHoverEvent.setEmpty();
+            return;
         }
-        mShouldIgnoreOnRegisterHoverKey = false;
+        super.onRegisterHoverKey(key, event);
     }
 
     @Override
     protected void onHoverEnterTo(final Key key) {
+        final int x = key.getHitBox().centerX();
+        final int y = key.getHitBox().centerY();
         if (DEBUG_HOVER) {
-            Log.d(TAG, "onHoverEnterTo: key=" + key);
+            Log.d(TAG, "onHoverEnterTo: key=" + key
+                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
         }
         mAccessibilityLongPressTimer.cancelLongPress();
+        if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
+            return;
+        }
+        // This hover enter event points to the key that isn't in the ignoring region.
+        // Further hover events should be handled.
+        mBoundsToIgnoreHoverEvent.setEmpty();
         super.onHoverEnterTo(key);
         if (key.isLongPressEnabled()) {
             mAccessibilityLongPressTimer.startLongPress(key);
         }
     }
 
+    @Override
     protected void onHoverExitFrom(final Key key) {
+        final int x = key.getHitBox().centerX();
+        final int y = key.getHitBox().centerY();
         if (DEBUG_HOVER) {
-            Log.d(TAG, "onHoverExitFrom: key=" + key);
+            Log.d(TAG, "onHoverExitFrom: key=" + key
+                    + " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
         }
         mAccessibilityLongPressTimer.cancelLongPress();
         super.onHoverExitFrom(key);
@@ -246,6 +280,24 @@
         // or a key invokes IME switcher dialog, we should just ignore the next
         // {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
         // {@link PointerTracker} is in operation or not.
-        mShouldIgnoreOnRegisterHoverKey = !tracker.isInOperation();
+        if (tracker.isInOperation()) {
+            // This long press shows a more keys keyboard and further hover events should be
+            // handled.
+            mBoundsToIgnoreHoverEvent.setEmpty();
+            return;
+        }
+        // This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
+        // We should ignore further hover events on this key.
+        mBoundsToIgnoreHoverEvent.set(key.getHitBox());
+        if (key.hasNoPanelAutoMoreKey()) {
+            // This long press has registered a code point without showing a more keys keyboard.
+            // We should talk back the code point if possible.
+            final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
+            final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
+                    mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
+            if (text != null) {
+                sendWindowStateChanged(text);
+            }
+        }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index 48b6a46..bdf3923 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -574,6 +574,12 @@
             final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
         final ExpandableBinaryDictionary personalizationDict =
                 mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
+        if (personalizationDict == null) {
+            if (callback != null) {
+                callback.onFinished();
+            }
+            return;
+        }
         final ArrayList<LanguageModelParam> languageModelParams =
                 LanguageModelParam.createLanguageModelParamsFrom(
                         personalizationDataChunk.mTokens,
@@ -581,8 +587,7 @@
                         this /* dictionaryFacilitator */, spacingAndPunctuations,
                         new DistracterFilterCheckingIsInDictionary(
                                 mDistracterFilter, personalizationDict));
-        if (personalizationDict == null || languageModelParams == null
-                || languageModelParams.isEmpty()) {
+        if (languageModelParams == null || languageModelParams.isEmpty()) {
             if (callback != null) {
                 callback.onFinished();
             }
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java
index 1f1475a..0ee6236 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatches.java
@@ -22,6 +22,7 @@
 
 import android.content.Context;
 import android.util.Log;
+import android.util.LruCache;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.DictionaryFacilitator;
@@ -36,9 +37,11 @@
     private static final boolean DEBUG = false;
 
     private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120;
+    private static final int MAX_DISTRACTERS_CACHE_SIZE = 512;
 
     private final Context mContext;
     private final DictionaryFacilitator mDictionaryFacilitator;
+    private final LruCache<String, Boolean> mDistractersCache;
     private final Object mLock = new Object();
 
     /**
@@ -49,6 +52,7 @@
     public DistracterFilterCheckingExactMatches(final Context context) {
         mContext = context;
         mDictionaryFacilitator = new DictionaryFacilitator();
+        mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE);
     }
 
     @Override
@@ -87,6 +91,7 @@
             synchronized (mLock) {
                 // Reset dictionaries for the locale.
                 try {
+                    mDistractersCache.evictAll();
                     loadDictionariesForLocale(locale);
                 } catch (final InterruptedException e) {
                     Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter",
@@ -95,6 +100,15 @@
                 }
             }
         }
+
+        final Boolean isCachedDistracter = mDistractersCache.get(testedWord);
+        if (isCachedDistracter != null && isCachedDistracter) {
+            if (DEBUG) {
+                Log.d(TAG, "testedWord: " + testedWord);
+                Log.d(TAG, "isDistracter: true (cache hit)");
+            }
+            return true;
+        }
         // The tested word is a distracter when there is a word that is exact matched to the tested
         // word and its probability is higher than the tested word's probability.
         final int perfectMatchFreq = mDictionaryFacilitator.getFrequency(testedWord);
@@ -106,6 +120,10 @@
             Log.d(TAG, "exactMatchFreq: " + exactMatchFreq);
             Log.d(TAG, "isDistracter: " + isDistracter);
         }
+        if (isDistracter) {
+            // Add the word to the cache.
+            mDistractersCache.put(testedWord, Boolean.TRUE);
+        }
         return isDistracter;
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java b/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
index 4f4f01c..f9d98af 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
@@ -83,10 +83,14 @@
      * Test keyboard theme preference on the same platform version and the same keyboard version.
      */
 
-    private void assertKeyboardThemePreference(final int sdkVersion, final int oldThemeId,
+    private void assertKeyboardThemePreference(final int sdkVersion, final int previousThemeId,
             final int expectedThemeId) {
+        // Clear preferences before testing.
+        setKeyboardThemePreference(KeyboardTheme.KLP_KEYBOARD_THEME_KEY, THEME_ID_NULL);
+        setKeyboardThemePreference(KeyboardTheme.LXX_KEYBOARD_THEME_KEY, THEME_ID_NULL);
+        // Set the preference of the sdkVersion to previousThemeId.
         final String prefKey = KeyboardTheme.getPreferenceKey(sdkVersion);
-        setKeyboardThemePreference(prefKey, oldThemeId);
+        setKeyboardThemePreference(prefKey, previousThemeId);
         assertKeyboardTheme(sdkVersion, expectedThemeId);
     }
 
@@ -127,10 +131,10 @@
      * Test default keyboard theme based on the platform version.
      */
 
-    private void assertDefaultKeyboardTheme(final int sdkVersion, final int oldThemeId,
+    private void assertDefaultKeyboardTheme(final int sdkVersion, final int previousThemeId,
             final int expectedThemeId) {
         final String oldPrefKey = KeyboardTheme.KLP_KEYBOARD_THEME_KEY;
-        setKeyboardThemePreference(oldPrefKey, oldThemeId);
+        setKeyboardThemePreference(oldPrefKey, previousThemeId);
 
         final KeyboardTheme defaultTheme =
                 KeyboardTheme.getDefaultKeyboardTheme(mPrefs, sdkVersion);
@@ -139,7 +143,7 @@
         assertEquals(expectedThemeId, defaultTheme.mThemeId);
         if (sdkVersion <= VERSION_CODES.KITKAT) {
             // Old preference must be retained if it is valid. Otherwise it must be pruned.
-            assertEquals(isValidKeyboardThemeId(oldThemeId), mPrefs.contains(oldPrefKey));
+            assertEquals(isValidKeyboardThemeId(previousThemeId), mPrefs.contains(oldPrefKey));
             return;
         }
         // Old preference must be removed.
@@ -181,9 +185,9 @@
      * to the keyboard that supports LXX theme.
      */
 
-    private void assertUpgradeKeyboardToLxxOn(final int sdkVersion, final int oldThemeId,
+    private void assertUpgradeKeyboardToLxxOn(final int sdkVersion, final int previousThemeId,
             final int expectedThemeId) {
-        setKeyboardThemePreference(KeyboardTheme.KLP_KEYBOARD_THEME_KEY, oldThemeId);
+        setKeyboardThemePreference(KeyboardTheme.KLP_KEYBOARD_THEME_KEY, previousThemeId);
         // Clean up new keyboard theme preference to simulate "upgrade to LXX keyboard".
         setKeyboardThemePreference(KeyboardTheme.LXX_KEYBOARD_THEME_KEY, THEME_ID_NULL);
 
@@ -195,9 +199,9 @@
             // New preference must not exist.
             assertFalse(mPrefs.contains(KeyboardTheme.LXX_KEYBOARD_THEME_KEY));
             // Old preference must be retained if it is valid. Otherwise it must be pruned.
-            assertEquals(isValidKeyboardThemeId(oldThemeId),
+            assertEquals(isValidKeyboardThemeId(previousThemeId),
                     mPrefs.contains(KeyboardTheme.KLP_KEYBOARD_THEME_KEY));
-            if (isValidKeyboardThemeId(oldThemeId)) {
+            if (isValidKeyboardThemeId(previousThemeId)) {
                 // Old preference must have an expected value.
                 assertEquals(mPrefs.getString(KeyboardTheme.KLP_KEYBOARD_THEME_KEY, null),
                         Integer.toString(expectedThemeId));
@@ -247,7 +251,7 @@
      */
 
     private void assertUpgradePlatformFromTo(final int oldSdkVersion, final int newSdkVersion,
-            final int oldThemeId, final int expectedThemeId) {
+            final int previousThemeId, final int expectedThemeId) {
         if (newSdkVersion < oldSdkVersion) {
             // No need to test.
             return;
@@ -257,7 +261,7 @@
         setKeyboardThemePreference(KeyboardTheme.LXX_KEYBOARD_THEME_KEY, THEME_ID_NULL);
 
         final String oldPrefKey = KeyboardTheme.getPreferenceKey(oldSdkVersion);
-        setKeyboardThemePreference(oldPrefKey, oldThemeId);
+        setKeyboardThemePreference(oldPrefKey, previousThemeId);
 
         assertKeyboardTheme(newSdkVersion, expectedThemeId);
     }