From edd1992ed329a84f0e9ef7056fda99f78eeb92b4 Mon Sep 17 00:00:00 2001
From: Keisuke Kuroynagi <ksk@google.com>
Date: Thu, 4 Jul 2013 21:06:04 +0900
Subject: [PATCH] DictionaryWriter to abstract binary dictionary writing.

UserHistoryDictionary should become to use ExpandableBinaryDictionary.

Bug: 6669677

Change-Id: I6831c7dd2d84207d1a8f51ba15d52e0a72205d0c
---
 .../latin/AbstractDictionaryWriter.java       |  78 ++++++++++
 .../latin/ContactsBinaryDictionary.java       |   6 +-
 .../inputmethod/latin/DictionaryWriter.java   | 107 +++++++++++++
 .../latin/ExpandableBinaryDictionary.java     | 145 +++++++-----------
 .../latin/UserBinaryDictionary.java           |   6 +-
 5 files changed, 254 insertions(+), 88 deletions(-)
 create mode 100644 java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
 create mode 100644 java/src/com/android/inputmethod/latin/DictionaryWriter.java

diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
new file mode 100644
index 0000000000..ebbcedc965
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.util.Log;
+
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+// TODO: Quit extending Dictionary after implementing dynamic binary dictionary.
+abstract public class AbstractDictionaryWriter extends Dictionary {
+    /** Used for Log actions from this class */
+    private static final String TAG = AbstractDictionaryWriter.class.getSimpleName();
+
+    private final Context mContext;
+
+    public AbstractDictionaryWriter(final Context context, final String dictType) {
+        super(dictType);
+        mContext = context;
+    }
+
+    abstract public void clear();
+
+    abstract public void addUnigramWord(final String word, final String shortcutTarget,
+            final int frequency, final boolean isNotAWord);
+
+    abstract public void addBigramWords(final String word0, final String word1,
+            final int frequency, final boolean isValid);
+
+    abstract public void removeBigramWords(final String word0, final String word1);
+
+    abstract protected void writeBinaryDictionary(final FileOutputStream out)
+            throws IOException, UnsupportedFormatException;
+
+    public void write(final String fileName) {
+        final String tempFileName = fileName + ".temp";
+        final File file = new File(mContext.getFilesDir(), fileName);
+        final File tempFile = new File(mContext.getFilesDir(), tempFileName);
+        FileOutputStream out = null;
+        try {
+            out = new FileOutputStream(tempFile);
+            writeBinaryDictionary(out);
+            out.flush();
+            out.close();
+            tempFile.renameTo(file);
+        } catch (IOException e) {
+            Log.e(TAG, "IO exception while writing file", e);
+        } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Unsupported format", e);
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    // ignore
+                }
+            }
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 110be9db3a..c99d0e2ea3 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -109,7 +109,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
 
     @Override
     public void loadDictionaryAsync() {
-        clearFusionDictionary();
         loadDeviceAccountsEmailAddresses();
         loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
         // TODO: Switch this URL to the newer ContactsContract too
@@ -235,6 +234,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
         return end;
     }
 
+    @Override
+    protected boolean needsToReloadBeforeWriting() {
+        return true;
+    }
+
     @Override
     protected boolean hasContentChanged() {
         final long startTime = SystemClock.uptimeMillis();
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
new file mode 100644
index 0000000000..8be04c1c0d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 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 com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * An in memory dictionary for memorizing entries and writing a binary dictionary.
+ */
+public class DictionaryWriter extends AbstractDictionaryWriter {
+    // TODO: Regenerate version 3 binary dictionary.
+    private static final int BINARY_DICT_VERSION = 2;
+    private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
+            new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
+
+    private FusionDictionary mFusionDictionary;
+
+    public DictionaryWriter(final Context context, final String dictType) {
+        super(context, dictType);
+        clear();
+    }
+
+    @Override
+    public void clear() {
+        final HashMap<String, String> attributes = CollectionUtils.newHashMap();
+        mFusionDictionary = new FusionDictionary(new Node(),
+                new FusionDictionary.DictionaryOptions(attributes, false, false));
+    }
+
+    /**
+     * Adds a word unigram to the fusion dictionary.
+     */
+    // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
+    // considering performance regression.
+    @Override
+    public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
+            final boolean isNotAWord) {
+        if (shortcutTarget == null) {
+            mFusionDictionary.add(word, frequency, null, isNotAWord);
+        } else {
+            // TODO: Do this in the subclass, with this class taking an arraylist.
+            final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
+            shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
+            mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
+        }
+    }
+
+    @Override
+    public void addBigramWords(final String word0, final String word1, final int frequency,
+            final boolean isValid) {
+        mFusionDictionary.setBigram(word0, word1, frequency);
+    }
+
+    @Override
+    public void removeBigramWords(final String word0, final String word1) {
+        // This class don't support removing bigram words.
+    }
+
+    @Override
+    protected void writeBinaryDictionary(final FileOutputStream out)
+            throws IOException, UnsupportedFormatException {
+        BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
+    }
+
+    @Override
+    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+            final String prevWord, final ProximityInfo proximityInfo,
+            boolean blockOffensiveWords) {
+        // This class doesn't support suggestion.
+        return null;
+    }
+
+    @Override
+    public boolean isValidWord(String word) {
+        // This class doesn't support dictionary retrieval.
+        return false;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index f4c4c65334..a563c7686e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -22,17 +22,9 @@ import android.util.Log;
 
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.makedict.FusionDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -76,8 +68,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
      */
     private BinaryDictionary mBinaryDictionary;
 
-    /** The expandable fusion dictionary used to generate the binary dictionary. */
-    private FusionDictionary mFusionDictionary;
+    /** The in-memory dictionary used to generate the binary dictionary. */
+    private AbstractDictionaryWriter mDictionaryWriter;
 
     /**
      * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
@@ -92,11 +84,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
     /** Controls access to the local binary dictionary for this instance. */
     private final DictionaryController mLocalDictionaryController = new DictionaryController();
 
-    // TODO: Regenerate version 3 binary dictionary.
-    private static final int BINARY_DICT_VERSION = 2;
-    private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
-            new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
-
     /**
      * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
      * thread.
@@ -138,7 +125,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
         mContext = context;
         mBinaryDictionary = null;
         mSharedDictionaryController = getSharedDictionaryController(filename);
-        clearFusionDictionary();
+        mDictionaryWriter = new DictionaryWriter(context, dictType);
     }
 
     protected static String getFilenameWithLocale(final String name, final String localeStr) {
@@ -157,47 +144,51 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
                 mBinaryDictionary.close();
                 mBinaryDictionary = null;
             }
+            mDictionaryWriter.close();
         } finally {
             mLocalDictionaryController.writeLock().unlock();
         }
     }
 
     /**
-     * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on
-     * the native side.
+     * Adds a word unigram to the dictionary. Used for loading a dictionary.
      */
-    public void clearFusionDictionary() {
-        final HashMap<String, String> attributes = CollectionUtils.newHashMap();
-        mFusionDictionary = new FusionDictionary(new Node(),
-                new FusionDictionary.DictionaryOptions(attributes, false, false));
+    protected void addWord(final String word, final String shortcutTarget,
+            final int frequency, final boolean isNotAWord) {
+        mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
     }
 
     /**
-     * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
-     * are done to update the binary dictionary.
+     * Sets a word bigram in the dictionary. Used for loading a dictionary.
      */
-    // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
-    // considering performance regression.
-    protected void addWord(final String word, final String shortcutTarget, final int frequency,
-            final boolean isNotAWord) {
-        if (shortcutTarget == null) {
-            mFusionDictionary.add(word, frequency, null, isNotAWord);
-        } else {
-            // TODO: Do this in the subclass, with this class taking an arraylist.
-            final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
-            shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
-            mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
+    protected void setBigram(final String prevWord, final String word, final int frequency) {
+        mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
+    }
+
+    /**
+     * Dynamically adds a word unigram to the dictionary.
+     */
+    protected void addWordDynamically(final String word, final String shortcutTarget,
+            final int frequency, final boolean isNotAWord) {
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
         }
     }
 
     /**
-     * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are
-     * done to update the binary dictionary.
+     * Dynamically sets a word bigram in the dictionary.
      */
-    // TODO: Create "cache dictionary" to cache fresh bigrams for frequently updated dictionaries,
-    // considering performance regression.
-    protected void setBigram(final String prevWord, final String word, final int frequency) {
-        mFusionDictionary.setBigram(prevWord, word, frequency);
+    protected void setBigramDynamically(final String prevWord, final String word,
+            final int frequency) {
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
+        }
     }
 
     @Override
@@ -207,9 +198,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
         asyncReloadDictionaryIfRequired();
         if (mLocalDictionaryController.readLock().tryLock()) {
             try {
+                final ArrayList<SuggestedWordInfo> inMemDictSuggestion =
+                        mDictionaryWriter.getSuggestions(composer, prevWord, proximityInfo,
+                                blockOffensiveWords);
                 if (mBinaryDictionary != null) {
-                    return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
-                            blockOffensiveWords);
+                    final ArrayList<SuggestedWordInfo> binarySuggestion =
+                            mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
+                                    blockOffensiveWords);
+                    if (inMemDictSuggestion == null) {
+                        return binarySuggestion;
+                    } else if (binarySuggestion == null) {
+                        return inMemDictSuggestion;
+                    } else {
+                        binarySuggestion.addAll(binarySuggestion);
+                        return binarySuggestion;
+                    }
+                } else {
+                    return inMemDictSuggestion;
                 }
             } finally {
                 mLocalDictionaryController.readLock().unlock();
@@ -240,22 +245,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
         return mBinaryDictionary.isValidWord(word);
     }
 
-    protected boolean isValidBigram(final String word1, final String word2) {
-        if (mBinaryDictionary == null) return false;
-        return mBinaryDictionary.isValidBigram(word1, word2);
-    }
-
-    protected boolean isValidBigramInner(final String word1, final String word2) {
-        if (mLocalDictionaryController.readLock().tryLock()) {
-            try {
-                return isValidBigramLocked(word1, word2);
-            } finally {
-                mLocalDictionaryController.readLock().unlock();
-            }
-        }
-        return false;
-    }
-
     protected boolean isValidBigramLocked(final String word1, final String word2) {
         if (mBinaryDictionary == null) return false;
         return mBinaryDictionary.isValidBigram(word1, word2);
@@ -274,7 +263,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
      * Loads the current binary dictionary from internal storage. Assumes the dictionary file
      * exists.
      */
-    protected void loadBinaryDictionary() {
+    private void loadBinaryDictionary() {
         if (DEBUG) {
             Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
                     + mSharedDictionaryController.mLastUpdateRequestTime + " update="
@@ -305,6 +294,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
         }
     }
 
+    /**
+     * Abstract method for checking if it is required to reload the dictionary before writing
+     * a binary dictionary.
+     */
+    abstract protected boolean needsToReloadBeforeWriting();
+
     /**
      * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
      */
@@ -314,33 +309,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
                     + mSharedDictionaryController.mLastUpdateRequestTime + " update="
                     + mSharedDictionaryController.mLastUpdateTime);
         }
-
-        loadDictionaryAsync();
-
-        final String tempFileName = mFilename + ".temp";
-        final File file = new File(mContext.getFilesDir(), mFilename);
-        final File tempFile = new File(mContext.getFilesDir(), tempFileName);
-        FileOutputStream out = null;
-        try {
-            out = new FileOutputStream(tempFile);
-            BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
-            out.flush();
-            out.close();
-            tempFile.renameTo(file);
-            clearFusionDictionary();
-        } catch (IOException e) {
-            Log.e(TAG, "IO exception while writing file", e);
-        } catch (UnsupportedFormatException e) {
-            Log.e(TAG, "Unsupported format", e);
-        } finally {
-            if (out != null) {
-                try {
-                    out.close();
-                } catch (IOException e) {
-                    // ignore
-                }
-            }
+        if (needsToReloadBeforeWriting()) {
+            mDictionaryWriter.clear();
+            loadDictionaryAsync();
         }
+        mDictionaryWriter.write(mFilename);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index ba84c1ad36..ab8f348937 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -240,7 +240,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
 
     private void addWords(final Cursor cursor) {
         final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
-        clearFusionDictionary();
         if (cursor == null) return;
         if (cursor.moveToFirst()) {
             final int indexWord = cursor.getColumnIndex(Words.WORD);
@@ -267,4 +266,9 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
     protected boolean hasContentChanged() {
         return true;
     }
+
+    @Override
+    protected boolean needsToReloadBeforeWriting() {
+        return true;
+    }
 }
-- 
GitLab