Create a dictionary collection and a dictionary factory.

The dictionary collection is a class complying to the Dictionary
interface that acts as a front end to a collection of arbitrarily many
dictionaries of any type.
The dictionary factory is a helper class for creating various
dictionaries and get some meta information about them.

At the same time, this change makes the BinaryDictionary class
not a singleton any more.

This also needs I9afe61a9 to not break the build.

Change-Id: I61fdcc4867fcda18342807bf1865e6e46979e5d5
diff --git a/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
index 6678082..a1b49b4 100644
--- a/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
+++ b/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
@@ -17,12 +17,11 @@
 package com.android.inputmethod.deprecated.languageswitcher;
 
 import com.android.inputmethod.keyboard.KeyboardParser;
-import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.DictionaryFactory;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.Settings;
 import com.android.inputmethod.latin.SharedPreferencesCompat;
 import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.Utils;
 
 import org.xmlpull.v1.XmlPullParserException;
@@ -123,20 +122,10 @@
         if (locale == null) return new Pair<Boolean, Boolean>(false, false);
         final Resources res = getResources();
         final Locale saveLocale = Utils.setSystemLocale(res, locale);
-        boolean hasDictionary = false;
+        final boolean hasDictionary = DictionaryFactory.isDictionaryAvailable(this, locale);
         boolean hasLayout = false;
 
         try {
-            BinaryDictionary bd = BinaryDictionary.initDictionaryFromManager(this, Suggest.DIC_MAIN,
-                    locale, Utils.getMainDictionaryResourceId(res));
-
-            // Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of
-            // 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words.
-            if (bd.getSize() > Suggest.LARGE_DICTIONARY_THRESHOLD / 4) {
-                hasDictionary = true;
-            }
-            bd.close();
-
             final String localeStr = locale.toString();
             final String[] layoutCountryCodes = KeyboardParser.parseKeyboardLocale(
                     this, R.xml.kbd_qwerty).split(",", -1);
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 5416cd5..d95fb96 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -21,12 +21,8 @@
 import com.android.inputmethod.keyboard.ProximityInfo;
 
 import android.content.Context;
-import android.content.res.AssetFileDescriptor;
-import android.util.Log;
 
-import java.io.File;
 import java.util.Arrays;
-import java.util.Locale;
 
 /**
  * Implements a static, compacted, binary dictionary of standard words.
@@ -45,16 +41,15 @@
     public static final int MAX_WORD_LENGTH = 48;
     public static final int MAX_WORDS = 18;
 
+    @SuppressWarnings("unused")
     private static final String TAG = "BinaryDictionary";
     private static final int MAX_PROXIMITY_CHARS_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
     private static final int MAX_BIGRAMS = 60;
 
     private static final int TYPED_LETTER_MULTIPLIER = 2;
 
-    private static final BinaryDictionary sInstance = new BinaryDictionary();
     private int mDicTypeId;
     private int mNativeDict;
-    private long mDictLength;
     private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_PROXIMITY_CHARS_SIZE];
     private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
     private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
@@ -79,95 +74,32 @@
 
     private int mFlags = 0;
 
-    private BinaryDictionary() {
-    }
-
     /**
-     * Initializes a dictionary from a raw resource file
-     * @param context application context for reading resources
-     * @param resId the resource containing the raw binary dictionary
-     * @param dicTypeId the type of the dictionary being created, out of the list in Suggest.DIC_*
-     * @return an initialized instance of BinaryDictionary
+     * Constructor for the binary dictionary. This is supposed to be called from the
+     * dictionary factory.
+     * All implementations should pass null into flagArray, except for testing purposes.
+     * @param context the context to access the environment from.
+     * @param filename the name of the file to read through native code.
+     * @param offset the offset of the dictionary data within the file.
+     * @param length the length of the binary data.
+     * @param flagArray the flags to limit the dictionary to, or null for default.
      */
-    public static BinaryDictionary initDictionary(Context context, int resId, int dicTypeId) {
-        synchronized (sInstance) {
-            sInstance.closeInternal();
-            try {
-                final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId);
-                if (afd == null) {
-                    Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
-                    return null;
-                }
-                final String sourceDir = context.getApplicationInfo().sourceDir;
-                final File packagePath = new File(sourceDir);
-                // TODO: Come up with a way to handle a directory.
-                if (!packagePath.isFile()) {
-                    Log.e(TAG, "sourceDir is not a file: " + sourceDir);
-                    return null;
-                }
-                sInstance.loadDictionary(sourceDir, afd.getStartOffset(), afd.getLength());
-                sInstance.mDicTypeId = dicTypeId;
-            } catch (android.content.res.Resources.NotFoundException e) {
-                Log.e(TAG, "Could not find the resource. resId=" + resId);
-                return null;
-            }
-        }
-        sInstance.mFlags = Flag.initFlags(ALL_FLAGS, context, SubtypeSwitcher.getInstance());
-        return sInstance;
-    }
-
-    /* package for test */ static BinaryDictionary initDictionary(Context context, File dictionary,
-            long startOffset, long length, int dicTypeId, Flag[] flagArray) {
-        synchronized (sInstance) {
-            sInstance.closeInternal();
-            if (dictionary.isFile()) {
-                sInstance.loadDictionary(dictionary.getAbsolutePath(), startOffset, length);
-                sInstance.mDicTypeId = dicTypeId;
-            } else {
-                Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
-                return null;
-            }
-        }
-        sInstance.mFlags = Flag.initFlags(flagArray, context, null);
-        return sInstance;
+    public BinaryDictionary(final Context context,
+            final String filename, final long offset, final long length, Flag[] flagArray) {
+        // Note: at the moment a binary dictionary is always of the "main" type.
+        // Initializing this here will help transitioning out of the scheme where
+        // the Suggest class knows everything about every single dictionary.
+        mDicTypeId = Suggest.DIC_MAIN;
+        // TODO: Stop relying on the state of SubtypeSwitcher, get it as a parameter
+        mFlags = Flag.initFlags(null == flagArray ? ALL_FLAGS : flagArray, context,
+                SubtypeSwitcher.getInstance());
+        loadDictionary(filename, offset, length);
     }
 
     static {
         Utils.loadNativeLibrary();
     }
 
-    /**
-     * Initializes a dictionary from a dictionary pack.
-     *
-     * This searches for a content provider providing a dictionary pack for the specified
-     * locale. If none is found, it falls back to using the resource passed as fallBackResId
-     * as a dictionary.
-     * @param context application context for reading resources
-     * @param dicTypeId the type of the dictionary being created, out of the list in Suggest.DIC_*
-     * @param locale the locale for which to create the dictionary
-     * @param fallbackResId the id of the resource to use as a fallback if no pack is found
-     * @return an initialized instance of BinaryDictionary
-     */
-    public static BinaryDictionary initDictionaryFromManager(Context context, int dicTypeId,
-            Locale locale, int fallbackResId) {
-        if (null == locale) {
-            Log.e(TAG, "No locale defined for dictionary");
-            return initDictionary(context, fallbackResId, dicTypeId);
-        }
-        synchronized (sInstance) {
-            sInstance.closeInternal();
-
-            final AssetFileAddress dictFile = BinaryDictionaryGetter.getDictionaryFile(locale,
-                    context, fallbackResId);
-            if (null != dictFile) {
-                sInstance.loadDictionary(dictFile.mFilename, dictFile.mOffset, dictFile.mLength);
-                sInstance.mDicTypeId = dicTypeId;
-            }
-        }
-        sInstance.mFlags = Flag.initFlags(ALL_FLAGS, context, SubtypeSwitcher.getInstance());
-        return sInstance;
-    }
-
     private native int openNative(String sourceDir, long dictOffset, long dictSize,
             int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength,
             int maxWords, int maxAlternatives);
@@ -184,7 +116,6 @@
         mNativeDict = openNative(path, startOffset, length,
                     TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER,
                     MAX_WORD_LENGTH, MAX_WORDS, MAX_PROXIMITY_CHARS_SIZE);
-        mDictLength = length;
     }
 
     @Override
@@ -278,10 +209,6 @@
         return isValidWordNative(mNativeDict, chars, chars.length);
     }
 
-    public long getSize() {
-        return mDictLength; // This value is initialized in loadDictionary()
-    }
-
     @Override
     public synchronized void close() {
         closeInternal();
@@ -291,7 +218,6 @@
         if (mNativeDict != 0) {
             closeNative(mNativeDict);
             mNativeDict = 0;
-            mDictLength = 0;
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
new file mode 100644
index 0000000..4b64e53
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -0,0 +1,66 @@
+/*
+ * 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.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Class for a collection of dictionaries that behave like one dictionary.
+ */
+public class DictionaryCollection extends Dictionary {
+
+    protected final List<Dictionary> mDictionaries;
+
+    public DictionaryCollection() {
+        mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+    }
+
+    public DictionaryCollection(Dictionary... dictionaries) {
+        mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+    }
+
+    @Override
+    public void getWords(final WordComposer composer, final WordCallback callback) {
+        for (final Dictionary dict : mDictionaries)
+            dict.getWords(composer, callback);
+    }
+
+    @Override
+    public void getBigrams(final WordComposer composer, final CharSequence previousWord,
+            final WordCallback callback) {
+        for (final Dictionary dict : mDictionaries)
+            dict.getBigrams(composer, previousWord, callback);
+    }
+
+    @Override
+    public boolean isValidWord(CharSequence word) {
+        for (final Dictionary dict : mDictionaries)
+            if (dict.isValidWord(word)) return true;
+        return false;
+    }
+
+    @Override
+    public void close() {
+        for (final Dictionary dict : mDictionaries)
+            dict.close();
+    }
+
+    public void addDictionary(Dictionary newDict) {
+        mDictionaries.add(newDict);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
new file mode 100644
index 0000000..cd42d7c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -0,0 +1,159 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.util.Log;
+
+import java.io.File;
+import java.util.Locale;
+
+/**
+ * Factory for dictionary instances.
+ */
+public class DictionaryFactory {
+
+    private static String TAG = DictionaryFactory.class.getSimpleName();
+
+    /**
+     * Initializes a dictionary from a dictionary pack.
+     *
+     * This searches for a content provider providing a dictionary pack for the specified
+     * locale. If none is found, it falls back to using the resource passed as fallBackResId
+     * as a dictionary.
+     * @param context application context for reading resources
+     * @param locale the locale for which to create the dictionary
+     * @param fallbackResId the id of the resource to use as a fallback if no pack is found
+     * @return an initialized instance of Dictionary
+     */
+    public static Dictionary createDictionaryFromManager(Context context, Locale locale,
+            int fallbackResId) {
+        if (null == locale) {
+            Log.e(TAG, "No locale defined for dictionary");
+            return new DictionaryCollection(createBinaryDictionary(context, fallbackResId));
+        }
+
+        final AssetFileAddress dictFile = BinaryDictionaryGetter.getDictionaryFile(locale,
+                context, fallbackResId);
+        if (null == dictFile) return null;
+        return new DictionaryCollection(new BinaryDictionary(context,
+                dictFile.mFilename, dictFile.mOffset, dictFile.mLength, null));
+    }
+
+    /**
+     * Initializes a dictionary from a raw resource file
+     * @param context application context for reading resources
+     * @param resId the resource containing the raw binary dictionary
+     * @return an initialized instance of BinaryDictionary
+     */
+    protected static BinaryDictionary createBinaryDictionary(Context context, int resId) {
+        AssetFileDescriptor afd = null;
+        try {
+            // TODO: IMPORTANT: Do not create a dictionary from a placeholder.
+            afd = context.getResources().openRawResourceFd(resId);
+            if (afd == null) {
+                Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
+                return null;
+            }
+            if (!isFullDictionary(afd)) return null;
+            final String sourceDir = context.getApplicationInfo().sourceDir;
+            final File packagePath = new File(sourceDir);
+            // TODO: Come up with a way to handle a directory.
+            if (!packagePath.isFile()) {
+                Log.e(TAG, "sourceDir is not a file: " + sourceDir);
+                return null;
+            }
+            return new BinaryDictionary(context,
+                    sourceDir, afd.getStartOffset(), afd.getLength(), null);
+        } catch (android.content.res.Resources.NotFoundException e) {
+            Log.e(TAG, "Could not find the resource. resId=" + resId);
+            return null;
+        } finally {
+            if (null != afd) {
+                try {
+                    afd.close();
+                } catch (java.io.IOException e) {
+                    /* IOException on close ? What am I supposed to do ? */
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a dictionary from passed data. This is intended for unit tests only.
+     * @param context the test context to create this data from.
+     * @param dictionary the file to read
+     * @param startOffset the offset in the file where the data starts
+     * @param length the length of the data
+     * @param flagArray the flags to use with this data for testing
+     * @return the created dictionary, or null.
+     */
+    public static Dictionary createDictionaryForTest(Context context, File dictionary,
+            long startOffset, long length, Flag[] flagArray) {
+        if (dictionary.isFile()) {
+            return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
+                    flagArray);
+        } else {
+            Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
+            return null;
+        }
+    }
+
+    /**
+     * Find out whether a dictionary is available for this locale.
+     * @param context the context on which to check resources.
+     * @param locale the locale to check for.
+     * @return whether a (non-placeholder) dictionary is available or not.
+     */
+    public static boolean isDictionaryAvailable(Context context, Locale locale) {
+        final Resources res = context.getResources();
+        final Configuration conf = res.getConfiguration();
+        final Locale saveLocale = conf.locale;
+        conf.locale = locale;
+        res.updateConfiguration(conf, res.getDisplayMetrics());
+
+        final int resourceId = Utils.getMainDictionaryResourceId(res);
+        final AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
+        final boolean hasDictionary = isFullDictionary(afd);
+        try {
+            if (null != afd) afd.close();
+        } catch (java.io.IOException e) {
+            /* Um, what can we do here exactly? */
+        }
+
+        conf.locale = saveLocale;
+        res.updateConfiguration(conf, res.getDisplayMetrics());
+        return hasDictionary;
+    }
+
+    // TODO: Find the Right Way to find out whether the resource is a placeholder or not.
+    // Suggestion : strip the locale, open the placeholder file and store its offset.
+    // Upon opening the file, if it's the same offset, then it's the placeholder.
+    private static final long PLACEHOLDER_LENGTH = 34;
+    /**
+     * Finds out whether the data pointed out by an AssetFileDescriptor is a full
+     * dictionary (as opposed to null, or to a place holder).
+     * @param afd the file descriptor to test, or null
+     * @return true if the dictionary is a real full dictionary, false if it's null or a placeholder
+     */
+    protected static boolean isFullDictionary(final AssetFileDescriptor afd) {
+        return (afd != null && afd.getLength() > PLACEHOLDER_LENGTH);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 1f6146a..ca75866 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -75,13 +75,11 @@
     public static final String DICT_KEY_USER_BIGRAM = "user_bigram";
     public static final String DICT_KEY_WHITELIST ="whitelist";
 
-    public static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000;
-
     private static final boolean DBG = LatinImeLogger.sDBG;
 
     private AutoCorrection mAutoCorrection;
 
-    private BinaryDictionary mMainDict;
+    private Dictionary mMainDict;
     private WhitelistDictionary mWhiteListDictionary;
     private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>();
     private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>();
@@ -108,17 +106,17 @@
     private int mCorrectionMode = CORRECTION_BASIC;
 
     public Suggest(Context context, int dictionaryResId, Locale locale) {
-        init(context, BinaryDictionary.initDictionaryFromManager(context, DIC_MAIN, locale,
+        init(context, DictionaryFactory.createDictionaryFromManager(context, locale,
                 dictionaryResId));
     }
 
     /* package for test */ Suggest(Context context, File dictionary, long startOffset, long length,
             Flag[] flagArray) {
-        init(null, BinaryDictionary.initDictionary(context, dictionary, startOffset, length,
-                DIC_MAIN, flagArray));
+        init(null, DictionaryFactory.createDictionaryForTest(context, dictionary, startOffset,
+                length, flagArray));
     }
 
-    private void init(Context context, BinaryDictionary mainDict) {
+    private void init(Context context, Dictionary mainDict) {
         if (mainDict != null) {
             mMainDict = mainDict;
             mUnigramDictionaries.put(DICT_KEY_MAIN, mainDict);
@@ -133,8 +131,8 @@
     }
 
     public void resetMainDict(Context context, int dictionaryResId, Locale locale) {
-        final BinaryDictionary newMainDict = BinaryDictionary.initDictionaryFromManager(context,
-                DIC_MAIN, locale, dictionaryResId);
+        final Dictionary newMainDict = DictionaryFactory.createDictionaryFromManager(
+                context, locale, dictionaryResId);
         mMainDict = newMainDict;
         if (null == newMainDict) {
             mUnigramDictionaries.remove(DICT_KEY_MAIN);
@@ -165,7 +163,7 @@
     }
 
     public boolean hasMainDictionary() {
-        return mMainDict != null && mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD;
+        return mMainDict != null;
     }
 
     public Map<String, Dictionary> getUnigramDictionaries() {