Merge "Put SuggestionSpan as the indicater of the auto-correction"
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index a1f3488..8bc97f6 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -142,6 +142,9 @@
     <!-- Label for "Wait" key of phone number keyboard.  Must be short to fit on key! [CHAR LIMIT=5]-->
     <string name="label_wait_key">Wait</string>
 
+    <!-- Spoken description to let the user know that when typing in a password, they can plug in a headset in to hear spoken descriptions of the keys they type. [CHAR LIMIT=NONE] -->
+    <string name="spoken_use_headphones">Plug in a headset to hear password keys spoken aloud.</string>
+
     <!-- Spoken description for the currently entered text -->
     <string name="spoken_current_text_is">Current text is "%s"</string>
     <!-- Spoken description when there is no text entered -->
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index 7e71b5f..4a2542d 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -19,15 +19,19 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.inputmethodservice.InputMethodService;
+import android.media.AudioManager;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.EditorInfo;
 
-import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
 import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
+import com.android.inputmethod.compat.AudioManagerCompatWrapper;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
 import com.android.inputmethod.compat.MotionEventCompatUtils;
+import com.android.inputmethod.latin.R;
 
 public class AccessibilityUtils {
     private static final String TAG = AccessibilityUtils.class.getSimpleName();
@@ -37,8 +41,10 @@
 
     private static final AccessibilityUtils sInstance = new AccessibilityUtils();
 
+    private Context mContext;
     private AccessibilityManager mAccessibilityManager;
     private AccessibilityManagerCompatWrapper mCompatManager;
+    private AudioManagerCompatWrapper mAudioManager;
 
     /*
      * Setting this constant to {@code false} will disable all keyboard
@@ -67,9 +73,14 @@
     }
 
     private void initInternal(Context context, SharedPreferences prefs) {
+        mContext = context;
         mAccessibilityManager = (AccessibilityManager) context
                 .getSystemService(Context.ACCESSIBILITY_SERVICE);
         mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);
+
+        final AudioManager audioManager = (AudioManager) context
+                .getSystemService(Context.AUDIO_SERVICE);
+        mAudioManager = new AudioManagerCompatWrapper(audioManager);
     }
 
     /**
@@ -102,6 +113,22 @@
     }
 
     /**
+     * @return {@code true} if the device should not speak text (eg.
+     *         non-control) characters
+     */
+    public boolean shouldObscureInput(EditorInfo attribute) {
+        if (attribute == null)
+            return false;
+
+        // Always speak if the user is listening through headphones.
+        if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
+            return false;
+
+        // Don't speak if the IME is connected to a password field.
+        return InputTypeCompatUtils.isPasswordInputType(attribute.inputType);
+    }
+
+    /**
      * Sends the specified text to the {@link AccessibilityManager} to be
      * spoken.
      *
@@ -117,7 +144,7 @@
         // class. Instead, we're just forcing a fake AccessibilityEvent into
         // the screen reader to make it speak.
         final AccessibilityEvent event = AccessibilityEvent
-                .obtain(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
+                .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
 
         event.setPackageName(PACKAGE);
         event.setClassName(CLASS);
@@ -127,4 +154,18 @@
 
         mAccessibilityManager.sendAccessibilityEvent(event);
     }
+
+    /**
+     * Handles speaking the "connect a headset to hear passwords" notification
+     * when connecting to a password field.
+     *
+     * @param attribute The input connection's editor info attribute.
+     * @param restarting Whether the connection is being restarted.
+     */
+    public void onStartInputViewInternal(EditorInfo attribute, boolean restarting) {
+        if (shouldObscureInput(attribute)) {
+            final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
+            speak(text);
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index e1b7781..4c109c7 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -21,7 +21,6 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.inputmethodservice.InputMethodService;
-import android.media.AudioManager;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -29,8 +28,6 @@
 import android.view.inputmethod.EditorInfo;
 
 import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
-import com.android.inputmethod.compat.AudioManagerCompatWrapper;
-import com.android.inputmethod.compat.InputTypeCompatUtils;
 import com.android.inputmethod.compat.MotionEventCompatUtils;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
@@ -48,7 +45,6 @@
     private FlickGestureDetector mGestureDetector;
     private LatinKeyboardView mView;
     private AccessibleKeyboardActionListener mListener;
-    private AudioManagerCompatWrapper mAudioManager;
 
     private int mScaledEdgeSlop;
     private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
@@ -82,26 +78,6 @@
         mInputMethod = inputMethod;
         mGestureDetector = new KeyboardFlickGestureDetector(inputMethod);
         mScaledEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
-
-        final AudioManager audioManager = (AudioManager) inputMethod
-                .getSystemService(Context.AUDIO_SERVICE);
-        mAudioManager = new AudioManagerCompatWrapper(audioManager);
-    }
-
-    /**
-     * @return {@code true} if the device should not speak text (eg. non-control) characters
-     */
-    private boolean shouldObscureInput() {
-        // Always speak if the user is listening through headphones.
-        if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
-            return false;
-
-        final EditorInfo info = mInputMethod.getCurrentInputEditorInfo();
-        if (info == null)
-            return false;
-
-        // Don't speak if the IME is connected to a password field.
-        return InputTypeCompatUtils.isPasswordInputType(info.inputType);
     }
 
     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
@@ -118,7 +94,8 @@
             if (key == null)
                 break;
 
-            final boolean shouldObscure = shouldObscureInput();
+            final EditorInfo info = mInputMethod.getCurrentInputEditorInfo();
+            final boolean shouldObscure = AccessibilityUtils.getInstance().shouldObscureInput(info);
             final CharSequence description = KeyCodeDescriptionMapper.getInstance()
                     .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key,
                             shouldObscure);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 49e92fd..83871a6 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -649,7 +649,7 @@
     }
 
     public boolean isVibrateAndSoundFeedbackRequired() {
-        return mKeyboardView == null || !mKeyboardView.isInSlidingKeyInput();
+        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
     }
 
     private int getPointerCount() {
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index fc97710..485ec51 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -82,14 +82,16 @@
         return false;
     }
 
-    public static boolean isValidWordForAutoCorrection(
+    public static boolean allowsToBeAutoCorrected(
             Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) {
-        final Dictionary whiteList = dictionaries.get(Suggest.DICT_KEY_WHITELIST);
+        final WhitelistDictionary whitelistDictionary =
+                (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_WHITELIST);
         // If "word" is in the whitelist dictionary, it should not be auto corrected.
-        if (whiteList != null && whiteList.isValidWord(word)) {
-            return false;
+        if (whitelistDictionary != null
+                && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
+            return true;
         }
-        return isValidWord(dictionaries, word, ignoreCase);
+        return !isValidWord(dictionaries, word, ignoreCase);
     }
 
     private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
@@ -100,8 +102,8 @@
             WordComposer wordComposer, ArrayList<CharSequence> suggestions, CharSequence typedWord,
             int correctionMode) {
         if (TextUtils.isEmpty(typedWord)) return false;
-        boolean isValidWord = isValidWordForAutoCorrection(dictionaries, typedWord, false);
-        return wordComposer.size() > 1 && suggestions.size() > 0 && isValidWord
+        boolean allowsAutoCorrect = allowsToBeAutoCorrected(dictionaries, typedWord, false);
+        return wordComposer.size() > 1 && suggestions.size() > 0 && !allowsAutoCorrect
                 && (correctionMode == Suggest.CORRECTION_FULL
                 || correctionMode == Suggest.CORRECTION_FULL_BIGRAM);
     }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index c5f336b..958092b 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -693,6 +693,12 @@
             return;
         }
 
+        // Forward this event to the accessibility utilities, if enabled.
+        final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
+        if (accessUtils.isTouchExplorationEnabled()) {
+            accessUtils.onStartInputViewInternal(attribute, restarting);
+        }
+
         mSubtypeSwitcher.updateParametersOnStartInputView();
 
         TextEntryState.reset();
@@ -1259,6 +1265,7 @@
             break;
         case Keyboard.CODE_CAPSLOCK:
             switcher.toggleCapsLock();
+            vibrate();
             break;
         case Keyboard.CODE_SHORTCUT:
             mSubtypeSwitcher.switchToShortcutIME();
@@ -1645,11 +1652,16 @@
         boolean autoCorrectionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
         final CharSequence typedWord = wordComposer.getTypedWord();
         // Here, we want to promote a whitelisted word if exists.
-        final boolean typedWordValid = AutoCorrection.isValidWordForAutoCorrection(
+        // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
+        // but still autocorrected from - in the case the whitelist only capitalizes the word.
+        // The whitelist should be case-insensitive, so it's not possible to be consistent with
+        // a boolean flag. Right now this is handled with a slight hack in
+        // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
+        final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
                 mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
         if (mCorrectionMode == Suggest.CORRECTION_FULL
                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
-            autoCorrectionAvailable |= typedWordValid;
+            autoCorrectionAvailable |= (!allowsToBeAutoCorrected);
         }
         // Don't auto-correct words with multiple capital letter
         autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
@@ -1662,9 +1674,9 @@
         // need to clear the previous state when the user starts typing a word (i.e. typed word's
         // length == 1).
         if (typedWord != null) {
-            if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid
+            if (builder.size() > 1 || typedWord.length() == 1 || (!allowsToBeAutoCorrected)
                     || mSuggestionsView.isShowingAddToDictionaryHint()) {
-                builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(
+                builder.setTypedWordValid(!allowsToBeAutoCorrected).setHasMinimalSuggestion(
                         autoCorrectionAvailable);
             } else {
                 SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index f6f5581..caa5aac 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -67,10 +67,10 @@
     public static final int DIC_USER_UNIGRAM = 3;
     public static final int DIC_CONTACTS = 4;
     public static final int DIC_USER_BIGRAM = 5;
+    public static final int DIC_WHITELIST = 6;
     // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
     // TODO: this value seems unused. Remove it?
-    public static final int DIC_TYPE_LAST_ID = 5;
-
+    public static final int DIC_TYPE_LAST_ID = 6;
     public static final String DICT_KEY_MAIN = "main";
     public static final String DICT_KEY_CONTACTS = "contacts";
     // User dictionary, the system-managed one.
@@ -360,7 +360,7 @@
         final String typedWordString = typedWord == null ? null : typedWord.toString();
 
         CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized,
-                mWhiteListDictionary.getWhiteListedWord(typedWordString));
+                mWhiteListDictionary.getWhitelistedWord(typedWordString));
 
         mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer,
                 mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode,
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index b177d14..005db36 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -175,6 +175,19 @@
         public CharSequence getWord(int pos) {
             return mWords.get(pos);
         }
+
+        public String toString() {
+            // Pretty-print method to help debug
+            final StringBuilder sb = new StringBuilder("StringBuilder: mTypedWordValid = "
+                    + mTypedWordValid + " ; mHasMinimalSuggestion = " + mHasMinimalSuggestion
+                    + " ; mIsPunctuationSuggestions = " + mIsPunctuationSuggestions
+                    + " --- ");
+            for (CharSequence s : mWords) {
+                sb.append(s);
+                sb.append(" ; ");
+            }
+            return sb.toString();
+        }
     }
 
     public static class SuggestedWordInfo {
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
index 93474b6..8f349ce 100644
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
@@ -27,7 +27,7 @@
 import java.util.HashMap;
 import java.util.Locale;
 
-public class WhitelistDictionary extends Dictionary {
+public class WhitelistDictionary extends ExpandableDictionary {
 
     private static final boolean DBG = LatinImeLogger.sDBG;
     private static final String TAG = WhitelistDictionary.class.getSimpleName();
@@ -35,7 +35,9 @@
     private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
             new HashMap<String, Pair<Integer, String>>();
 
+    // TODO: Conform to the async load contact of ExpandableDictionary
     public WhitelistDictionary(final Context context, final Locale locale) {
+        super(context, Suggest.DIC_WHITELIST);
         final Resources res = context.getResources();
         final Locale previousLocale = LocaleUtils.setSystemLocale(res, locale);
         if (context != null) {
@@ -61,6 +63,7 @@
                 if (before != null && after != null) {
                     mWhitelistWords.put(
                             before.toLowerCase(), new Pair<Integer, String>(score, after));
+                    addWord(after, score);
                 }
             }
         } catch (NumberFormatException e) {
@@ -70,27 +73,34 @@
         }
     }
 
-    public String getWhiteListedWord(String before) {
+    public String getWhitelistedWord(String before) {
         if (before == null) return null;
         final String lowerCaseBefore = before.toLowerCase();
         if(mWhitelistWords.containsKey(lowerCaseBefore)) {
             if (DBG) {
-                Log.d(TAG, "--- found whiteListedWord: " + lowerCaseBefore);
+                Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore);
             }
             return mWhitelistWords.get(lowerCaseBefore).second;
         }
         return null;
     }
 
-    // Not used for WhitelistDictionary.  We use getWhitelistedWord() in Suggest.java instead
-    @Override
-    public void getWords(final WordComposer composer, final WordCallback callback,
-            final ProximityInfo proximityInfo) {
+    // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist
+    // lists that word a should autocorrect to word b, and word c would autocorrect to
+    // an upper-cased version of a. In this case, the way this return value is used would
+    // remove the first candidate when the user typed the upper-cased version of A.
+    // Example : abc -> def  and  xyz -> Abc
+    // A user typing Abc would experience it being autocorrected to something else (not
+    // necessarily def).
+    // There is no such combination in the whitelist at the time and there probably won't
+    // ever be - it doesn't make sense. But still.
+    public boolean shouldForciblyAutoCorrectFrom(CharSequence word) {
+        if (TextUtils.isEmpty(word)) return false;
+        final String correction = getWhitelistedWord(word.toString());
+        if (TextUtils.isEmpty(correction)) return false;
+        return !correction.equals(word);
     }
 
-    @Override
-    public boolean isValidWord(CharSequence word) {
-        if (TextUtils.isEmpty(word)) return false;
-        return !TextUtils.isEmpty(getWhiteListedWord(word.toString()));
-    }
+    // Leave implementation of getWords and isValidWord to the superclass.
+    // The words have been added to the ExpandableDictionary with addWord() inside initWordlist.
 }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 4d569b8..f9e6a5e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -37,6 +37,7 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
 import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.WhitelistDictionary;
 import com.android.inputmethod.latin.WordComposer;
 
 import java.util.ArrayList;
@@ -79,6 +80,8 @@
             Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
     private Map<String, Dictionary> mUserDictionaries =
             Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+    private Map<String, Dictionary> mWhitelistDictionaries =
+            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
 
     // The threshold for a candidate to be offered as a suggestion.
     private double mSuggestionThreshold;
@@ -253,12 +256,17 @@
         mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
         final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
         mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+        final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
+        mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
         for (DictionaryPool pool : oldPools.values()) {
             pool.close();
         }
         for (Dictionary dict : oldUserDictionaries.values()) {
             dict.close();
         }
+        for (Dictionary dict : oldWhitelistDictionaries.values()) {
+            dict.close();
+        }
         return false;
     }
 
@@ -280,12 +288,18 @@
                 DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId,
                         USE_FULL_EDIT_DISTANCE_FLAG_ARRAY);
         final String localeStr = locale.toString();
-        Dictionary userDict = mUserDictionaries.get(localeStr);
-        if (null == userDict) {
-            userDict = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
-            mUserDictionaries.put(localeStr, userDict);
+        Dictionary userDictionary = mUserDictionaries.get(localeStr);
+        if (null == userDictionary) {
+            userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
+            mUserDictionaries.put(localeStr, userDictionary);
         }
-        dictionaryCollection.addDictionary(userDict);
+        dictionaryCollection.addDictionary(userDictionary);
+        Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
+        if (null == whitelistDictionary) {
+            whitelistDictionary = new WhitelistDictionary(this, locale);
+            mWhitelistDictionaries.put(localeStr, whitelistDictionary);
+        }
+        dictionaryCollection.addDictionary(whitelistDictionary);
         return new DictAndProximity(dictionaryCollection, proximityInfo);
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/SuggestHelper.java b/tests/src/com/android/inputmethod/latin/SuggestHelper.java
index a089335..464930f 100644
--- a/tests/src/com/android/inputmethod/latin/SuggestHelper.java
+++ b/tests/src/com/android/inputmethod/latin/SuggestHelper.java
@@ -89,7 +89,7 @@
     }
 
     public boolean isValidWord(CharSequence typed) {
-        return AutoCorrection.isValidWordForAutoCorrection(mSuggest.getUnigramDictionaries(),
+        return AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(),
                 typed, false);
     }