Merge "Do the transposed correction and the excessive correction by one loop"
diff --git a/java/res/values-en/whitelist.xml b/java/res/values-en/whitelist.xml
index 9395f4c..4078b4d 100644
--- a/java/res/values-en/whitelist.xml
+++ b/java/res/values-en/whitelist.xml
@@ -31,8 +31,8 @@
         <item>I\'ll</item>
 
         <item>255</item>
-        <item>thisd</item>
-        <item>this\'d</item>
+        <item>lets</item>
+        <item>let\'s</item>
 
     </string-array>
 </resources>
diff --git a/java/res/values-ru/donottranslate-altchars.xml b/java/res/values-ru/donottranslate-altchars.xml
index 2da8b84..2a24e56 100644
--- a/java/res/values-ru/donottranslate-altchars.xml
+++ b/java/res/values-ru/donottranslate-altchars.xml
@@ -20,4 +20,5 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="alternates_for_cyrillic_e">5,ё</string>
     <string name="alternates_for_cyrillic_soft_sign">ъ</string>
+    <string name="alternates_for_cyrillic_ha">ъ</string>
 </resources>
diff --git a/java/res/values/donottranslate-altchars.xml b/java/res/values/donottranslate-altchars.xml
index fbde4b9..acd4b37 100644
--- a/java/res/values/donottranslate-altchars.xml
+++ b/java/res/values/donottranslate-altchars.xml
@@ -44,6 +44,7 @@
     <string name="alternates_for_scandinavia_row2_11"></string>
     <string name="alternates_for_cyrillic_e"></string>
     <string name="alternates_for_cyrillic_soft_sign"></string>
+    <string name="alternates_for_cyrillic_ha"></string>
     <string name="alternates_for_currency_dollar">¢,£,€,¥,₱</string>
     <string name="alternates_for_currency_euro">¢,£,$,¥,₱</string>
     <string name="alternates_for_currency_pound">¢,$,€,¥,₱</string>
diff --git a/java/res/xml/kbd_rows_russian.xml b/java/res/xml/kbd_rows_russian.xml
index 3aeb52b..0c7a237 100644
--- a/java/res/xml/kbd_rows_russian.xml
+++ b/java/res/xml/kbd_rows_russian.xml
@@ -69,7 +69,7 @@
             latin:popupCharacters="0" />
         <Key
             latin:keyLabel="х"
-            latin:popupCharacters="@string/alternates_for_cyrillic_soft_sign"
+            latin:popupCharacters="@string/alternates_for_cyrillic_ha"
             latin:keyWidth="fillRight" />
     </Row>
     <Row
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 21477a9..9937937 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -545,9 +545,8 @@
     private void setAutomaticTemporaryUpperCase() {
         if (mKeyboardView == null) return;
         final Keyboard keyboard = mKeyboardView.getKeyboard();
-        if (keyboard != null) {
-            keyboard.setAutomaticTemporaryUpperCase();
-        }
+        if (keyboard == null) return;
+        keyboard.setAutomaticTemporaryUpperCase();
         mKeyboardView.invalidateAllKeys();
     }
 
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 3da670e..ed5f83b 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -98,23 +98,32 @@
      * @throw IOException if the provider-returned data could not be read.
      */
     public static List<AssetFileAddress> cacheDictionariesFromContentProvider(final Locale locale,
-            final Context context) throws FileNotFoundException, IOException {
+            final Context context) {
         final ContentResolver resolver = context.getContentResolver();
         final List<String> idList = getDictIdList(locale, context);
         final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
         for (String id : idList) {
             final Uri wordListUri = getProviderUri(id);
-            final AssetFileDescriptor afd =
-                    resolver.openAssetFileDescriptor(wordListUri, "r");
-            if (null == afd) continue;
-            final String fileName = copyFileTo(afd.createInputStream(),
-                    BinaryDictionaryGetter.getCacheFileName(id, locale, context));
-            afd.close();
-            if (0 >= resolver.delete(wordListUri, null, null)) {
-                // I'd rather not print the word list ID to the log here out of security concerns
-                Log.e(TAG, "Could not have the dictionary pack delete a word list");
+            AssetFileDescriptor afd = null;
+            try {
+                afd = resolver.openAssetFileDescriptor(wordListUri, "r");
+            } catch (FileNotFoundException e) {
+                // leave null inside afd and continue
             }
-            fileAddressList.add(AssetFileAddress.makeFromFileName(fileName));
+            if (null == afd) continue;
+            try {
+                final String fileName = copyFileTo(afd.createInputStream(),
+                        BinaryDictionaryGetter.getCacheFileName(id, locale, context));
+                afd.close();
+                if (0 >= resolver.delete(wordListUri, null, null)) {
+                    // I'd rather not print the word list ID to the log out of security concerns
+                    Log.e(TAG, "Could not have the dictionary pack delete a word list");
+                }
+                fileAddressList.add(AssetFileAddress.makeFromFileName(fileName));
+            } catch (IOException e) {
+                // Can't read the file for some reason. Continue onto the next file.
+                Log.e(TAG, "Cannot read a word list from the dictionary pack : " + e);
+            }
         }
         return fileAddressList;
     }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 360cf21..5d2dab0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -42,6 +42,11 @@
     private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
 
     /**
+     * Used to return empty lists
+     */
+    private static final File[] EMPTY_FILE_ARRAY = new File[0];
+
+    /**
      * Name of the common preferences name to know which word list are on and which are off.
      */
     private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
@@ -158,46 +163,61 @@
                 context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
     }
 
+    static private class DictPackSettings {
+        final SharedPreferences mDictPreferences;
+        public DictPackSettings(final Context context) {
+            Context dictPackContext = null;
+            try {
+                final String dictPackName =
+                        context.getString(R.string.dictionary_pack_package_name);
+                dictPackContext = context.createPackageContext(dictPackName, 0);
+            } catch (NameNotFoundException e) {
+                // The dictionary pack is not installed...
+                // TODO: fallback on the built-in dict, see the TODO above
+                Log.e(TAG, "Could not find a dictionary pack");
+            }
+            mDictPreferences = null == dictPackContext ? null
+                    : dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME,
+                            Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);
+        }
+        public boolean isWordListActive(final String dictId) {
+            if (null == mDictPreferences) {
+                // If we don't have preferences it basically means we can't find the dictionary
+                // pack - either it's not installed, or it's disabled, or there is some strange
+                // bug. Either way, a word list with no settings should be on by default: default
+                // dictionaries in LatinIME are on if there is no settings at all, and if for some
+                // reason some dictionaries have been installed BUT the dictionary pack can't be
+                // found anymore it's safer to actually supply installed dictionaries.
+                return true;
+            } else {
+                // The default is true here for the same reasons as above. We got the dictionary
+                // pack but if we don't have any settings for it it means the user has never been
+                // to the settings yet. So by default, the main dictionaries should be on.
+                return mDictPreferences.getBoolean(dictId, true);
+            }
+        }
+    }
+
     /**
      * Returns the list of cached files for a specific locale.
      *
      * @param locale the locale to find the dictionary files for.
      * @param context the context on which to open the files upon.
-     * @return a list of binary dictionary files, which may be null but may not be empty.
+     * @return an array of binary dictionary files, which may be empty but may not be null.
      */
-    private static List<AssetFileAddress> getCachedDictionaryList(final Locale locale,
+    private static File[] getCachedDictionaryList(final Locale locale,
             final Context context) {
         final String directoryName = getCacheDirectoryForLocale(locale, context);
         final File[] cacheFiles = new File(directoryName).listFiles();
-        // TODO: Never return null. Fallback on the built-in dictionary, and if that's
-        // not present or disabled, then return an empty list.
-        if (null == cacheFiles) return null;
+        if (null == cacheFiles) return EMPTY_FILE_ARRAY;
+        return cacheFiles;
+    }
 
-        final SharedPreferences dictPackSettings;
-        try {
-            final String dictPackName = context.getString(R.string.dictionary_pack_package_name);
-            final Context dictPackContext = context.createPackageContext(dictPackName, 0);
-            dictPackSettings = dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME,
-                    Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);
-        } catch (NameNotFoundException e) {
-            // The dictionary pack is not installed...
-            // TODO: fallback on the built-in dict, see the TODO above
-            Log.e(TAG, "Could not find a dictionary pack");
-            return null;
-        }
-
-        final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
-        for (File f : cacheFiles) {
-            final String wordListId = getWordListIdFromFileName(f.getName());
-            final boolean isActive = dictPackSettings.getBoolean(wordListId, true);
-            if (!isActive) continue;
-            if (f.canRead()) {
-                fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
-            } else {
-                Log.e(TAG, "Found a cached dictionary file but cannot read it");
-            }
-        }
-        return fileList.size() > 0 ? fileList : null;
+    /**
+     * Returns the id of the main dict for a specified locale.
+     */
+    private static String getMainDictId(final Locale locale) {
+        return locale.toString();
     }
 
     /**
@@ -214,26 +234,41 @@
      */
     public static List<AssetFileAddress> getDictionaryFiles(final Locale locale,
             final Context context, final int fallbackResId) {
-        try {
-            // cacheDictionariesFromContentProvider returns the list of files it copied to local
-            // storage, but we don't really care about what was copied NOW: what we want is the
-            // list of everything we ever cached, so we ignore the return value.
-            BinaryDictionaryFileDumper.cacheDictionariesFromContentProvider(locale, context);
-            List<AssetFileAddress> cachedDictionaryList = getCachedDictionaryList(locale, context);
-            if (null != cachedDictionaryList) {
-                return cachedDictionaryList;
+
+        // cacheDictionariesFromContentProvider returns the list of files it copied to local
+        // storage, but we don't really care about what was copied NOW: what we want is the
+        // list of everything we ever cached, so we ignore the return value.
+        BinaryDictionaryFileDumper.cacheDictionariesFromContentProvider(locale, context);
+        final File[] cachedDictionaryList = getCachedDictionaryList(locale, context);
+
+        final String mainDictId = getMainDictId(locale);
+
+        final DictPackSettings dictPackSettings = new DictPackSettings(context);
+
+        boolean foundMainDict = false;
+        final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+        // cachedDictionaryList may not be null, see doc for getCachedDictionaryList
+        for (final File f : cachedDictionaryList) {
+            final String wordListId = getWordListIdFromFileName(f.getName());
+            if (wordListId.equals(mainDictId)) {
+                foundMainDict = true;
             }
-            // If the list is null, fall through and return the fallback
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "Unable to create dictionary file from provider for locale "
-                    + locale.toString() + ": falling back to internal dictionary");
-        } catch (IOException e) {
-            Log.e(TAG, "Unable to read source data for locale "
-                    + locale.toString() + ": falling back to internal dictionary");
+            if (!dictPackSettings.isWordListActive(wordListId)) continue;
+            if (f.canRead()) {
+                fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
+            } else {
+                Log.e(TAG, "Found a cached dictionary file but cannot read it");
+            }
         }
-        final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId,
-                locale);
-        if (null == fallbackAsset) return null;
-        return Arrays.asList(fallbackAsset);
+
+        if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
+            final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId,
+                    locale);
+            if (null != fallbackAsset) {
+                fileList.add(fallbackAsset);
+            }
+        }
+
+        return fileList;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/StringBuilderPool.java b/java/src/com/android/inputmethod/latin/StringBuilderPool.java
new file mode 100644
index 0000000..66f1237
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/StringBuilderPool.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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 com.android.inputmethod.latin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A pool of string builders to be used from anywhere.
+ */
+public class StringBuilderPool {
+    // Singleton
+    private static final StringBuilderPool sInstance = new StringBuilderPool();
+    private StringBuilderPool() {}
+    // TODO: Make this a normal array with a size of 20
+    private final List<StringBuilder> mPool =
+            Collections.synchronizedList(new ArrayList<StringBuilder>());
+
+    public static StringBuilder getStringBuilder(final int initialSize) {
+        final int poolSize = sInstance.mPool.size();
+        final StringBuilder sb = poolSize > 0 ? (StringBuilder) sInstance.mPool.remove(poolSize - 1)
+                : new StringBuilder(initialSize);
+        sb.setLength(0);
+        return sb;
+    }
+
+    public static void recycle(final StringBuilder garbage) {
+        sInstance.mPool.add(garbage);
+    }
+
+    public static void ensureCapacity(final int capacity, final int initialSize) {
+        for (int i = sInstance.mPool.size(); i < capacity; ++i) {
+            final StringBuilder sb = new StringBuilder(initialSize);
+            sInstance.mPool.add(sb);
+        }
+    }
+
+    public static int getSize() {
+        return sInstance.mPool.size();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index a2d66f3..c3caae4 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -105,9 +105,6 @@
 
     private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
     ArrayList<CharSequence> mBigramSuggestions  = new ArrayList<CharSequence>();
-    // TODO: maybe this should be synchronized, it's quite scary as it is.
-    // TODO: if it becomes synchronized, also move initPool in the thread in initAsynchronously
-    private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
     private CharSequence mTypedWord;
 
     // TODO: Remove these member variables by passing more context to addWord() callback method
@@ -130,7 +127,7 @@
         mWhiteListDictionary = WhitelistDictionary.init(context);
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
         mAutoCorrection = new AutoCorrection();
-        initPool();
+        StringBuilderPool.ensureCapacity(mPrefMaxSuggestions, getApproxMaxWordLength());
     }
 
     private void initAsynchronously(final Context context, final int dictionaryResId,
@@ -138,7 +135,7 @@
         resetMainDict(context, dictionaryResId, locale);
 
         // TODO: read the whitelist and init the pool asynchronously too.
-        // initPool should be done asynchronously but the pool is not thread-safe at the moment.
+        // initPool should be done asynchronously now that the pool is thread-safe.
         initWhitelistAndAutocorrectAndPool(context);
     }
 
@@ -173,12 +170,6 @@
         }.start();
     }
 
-    private void initPool() {
-        for (int i = 0; i < mPrefMaxSuggestions; i++) {
-            StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-            mStringPool.add(sb);
-        }
-    }
 
     public void setQuickFixesEnabled(boolean enabled) {
         mQuickFixesEnabled = enabled;
@@ -259,10 +250,7 @@
         mScores = new int[mPrefMaxSuggestions];
         mBigramScores = new int[PREF_MAX_BIGRAMS];
         collectGarbage(mSuggestions, mPrefMaxSuggestions);
-        while (mStringPool.size() < mPrefMaxSuggestions) {
-            StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
-            mStringPool.add(sb);
-        }
+        StringBuilderPool.ensureCapacity(mPrefMaxSuggestions, getApproxMaxWordLength());
     }
 
     /**
@@ -282,11 +270,7 @@
     private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) {
         if (TextUtils.isEmpty(word) || !(all || first)) return word;
         final int wordLength = word.length();
-        final int poolSize = mStringPool.size();
-        final StringBuilder sb =
-                poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
-                        : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
+        final StringBuilder sb = StringBuilderPool.getStringBuilder(getApproxMaxWordLength());
         // TODO: Must pay attention to locale when changing case.
         if (all) {
             sb.append(word.toString().toUpperCase());
@@ -300,13 +284,7 @@
     }
 
     protected void addBigramToSuggestions(CharSequence bigram) {
-        final int poolSize = mStringPool.size();
-        final StringBuilder sb = poolSize > 0 ?
-                (StringBuilder) mStringPool.remove(poolSize - 1)
-                        : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
-        sb.append(bigram);
-        mSuggestions.add(sb);
+        mSuggestions.add(bigram);
     }
 
     // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
@@ -426,7 +404,7 @@
         if (typedWord != null) {
             mSuggestions.add(0, typedWordString);
         }
-        removeDupes();
+        removeDupes(mSuggestions);
 
         if (DBG) {
             double normalizedScore = mAutoCorrection.getNormalizedScore();
@@ -453,8 +431,7 @@
         return new SuggestedWords.Builder().addWords(mSuggestions, null);
     }
 
-    private void removeDupes() {
-        final ArrayList<CharSequence> suggestions = mSuggestions;
+    private static void removeDupes(final ArrayList<CharSequence> suggestions) {
         if (suggestions.size() < 2) return;
         int i = 1;
         // Don't cache suggestions.size(), since we may be removing items
@@ -464,7 +441,7 @@
             for (int j = 0; j < i; j++) {
                 CharSequence previous = suggestions.get(j);
                 if (TextUtils.equals(cur, previous)) {
-                    removeFromSuggestions(i);
+                    removeFromSuggestions(suggestions, i);
                     i--;
                     break;
                 }
@@ -473,10 +450,11 @@
         }
     }
 
-    private void removeFromSuggestions(int index) {
-        CharSequence garbage = mSuggestions.remove(index);
-        if (garbage != null && garbage instanceof StringBuilder) {
-            mStringPool.add(garbage);
+    private static void removeFromSuggestions(final ArrayList<CharSequence> suggestions,
+            final int index) {
+        final CharSequence garbage = suggestions.remove(index);
+        if (garbage instanceof StringBuilder) {
+            StringBuilderPool.recycle((StringBuilder)garbage);
         }
     }
 
@@ -554,10 +532,7 @@
 
         System.arraycopy(sortedScores, pos, sortedScores, pos + 1, prefMaxSuggestions - pos - 1);
         sortedScores[pos] = score;
-        int poolSize = mStringPool.size();
-        StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
-                : new StringBuilder(getApproxMaxWordLength());
-        sb.setLength(0);
+        final StringBuilder sb = StringBuilderPool.getStringBuilder(getApproxMaxWordLength());
         // TODO: Must pay attention to locale when changing case.
         if (mIsAllUpperCase) {
             sb.append(new String(word, offset, length).toUpperCase());
@@ -571,9 +546,9 @@
         }
         suggestions.add(pos, sb);
         if (suggestions.size() > prefMaxSuggestions) {
-            CharSequence garbage = suggestions.remove(prefMaxSuggestions);
+            final CharSequence garbage = suggestions.remove(prefMaxSuggestions);
             if (garbage instanceof StringBuilder) {
-                mStringPool.add(garbage);
+                StringBuilderPool.recycle((StringBuilder)garbage);
             }
         } else {
             LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
@@ -602,12 +577,12 @@
     }
 
     private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
-        int poolSize = mStringPool.size();
+        int poolSize = StringBuilderPool.getSize();
         int garbageSize = suggestions.size();
         while (poolSize < prefMaxSuggestions && garbageSize > 0) {
-            CharSequence garbage = suggestions.get(garbageSize - 1);
-            if (garbage != null && garbage instanceof StringBuilder) {
-                mStringPool.add(garbage);
+            final CharSequence garbage = suggestions.get(garbageSize - 1);
+            if (garbage instanceof StringBuilder) {
+                StringBuilderPool.recycle((StringBuilder)garbage);
                 poolSize++;
             }
             garbageSize--;