From ecd2ac93bc321fdd932930c43851a92859d4775d Mon Sep 17 00:00:00 2001
From: Tom Ouyang <ouyang@google.com>
Date: Sat, 24 Mar 2012 15:13:40 +0900
Subject: [PATCH] Add an expandable binary dictionary that can be modified at
 runtime and works with native algorithms.

Bug: 6188977
Change-Id: Iec5c4e7d1d3918ac645187bd32dc3f82a95fec1e
---
 .../latin/ExpandableBinaryDictionary.java     | 393 ++++++++++++++++++
 1 file changed, 393 insertions(+)
 create mode 100644 java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java

diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
new file mode 100644
index 0000000000..53e8b74de7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2012 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.os.SystemClock;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Abstract base class for an expandable dictionary that can be created and updated dynamically
+ * during runtime. When updated it automatically generates a new binary dictionary to handle future
+ * queries in native code. This binary dictionary is written to internal storage, and potentially
+ * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename
+ * are controlled across multiple instances to ensure that only one instance can update the same
+ * dictionary at the same time.
+ */
+abstract public class ExpandableBinaryDictionary extends Dictionary {
+
+    /** Used for Log actions from this class */
+    private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
+
+    /** Whether to print debug output to log */
+    private static boolean DEBUG = false;
+
+    /**
+     * The maximum length of a word in this dictionary. This is the same value as the binary
+     * dictionary.
+     */
+    protected static final int MAX_WORD_LENGTH = BinaryDictionary.MAX_WORD_LENGTH;
+
+    /**
+     * A static map of locks, each of which controls access to a single binary dictionary file. They
+     * ensure that only one instance can update the same dictionary at the same time. The key for
+     * this map is the filename and the value is the shared dictionary controller associated with
+     * that filename.
+     */
+    private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
+            new HashMap<String, DictionaryController>();
+
+    /** The application context. */
+    protected final Context mContext;
+
+    /**
+     * The binary dictionary generated dynamically from the fusion dictionary. This is used to
+     * answer unigram and bigram queries.
+     */
+    private BinaryDictionary mBinaryDictionary;
+
+    /** The expandable fusion dictionary used to generate the binary dictionary. */
+    private FusionDictionary mFusionDictionary;
+
+    /** The dictionary type id. */
+    public final int mDicTypeId;
+
+    /**
+     * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
+     * dictionary instances with the same filename is supported, with access controlled by
+     * DictionaryController.
+     */
+    private final String mFilename;
+
+    /** Controls access to the shared binary dictionary file across multiple instances. */
+    private final DictionaryController mSharedDictionaryController;
+
+    /** Controls access to the local binary dictionary for this instance. */
+    private final DictionaryController mLocalDictionaryController = new DictionaryController();
+
+    /**
+     * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
+     * thread.
+     */
+    protected abstract void loadDictionaryAsync();
+
+    /**
+     * Gets the shared dictionary controller for the given filename.
+     */
+    private static synchronized DictionaryController getSharedDictionaryController(
+            String filename) {
+        DictionaryController controller = sSharedDictionaryControllers.get(filename);
+        if (controller == null) {
+            controller = new DictionaryController();
+            sSharedDictionaryControllers.put(filename, controller);
+        }
+        return controller;
+    }
+
+    /**
+     * Creates a new expandable binary dictionary.
+     *
+     * @param context The application context of the parent.
+     * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
+     *        filename is supported.
+     * @param dictType The type of this dictionary.
+     */
+    public ExpandableBinaryDictionary(
+            final Context context, final String filename, final int dictType) {
+        mDicTypeId = dictType;
+        mFilename = filename;
+        mContext = context;
+        mBinaryDictionary = null;
+        mSharedDictionaryController = getSharedDictionaryController(filename);
+        clearFusionDictionary();
+    }
+
+    /**
+     * Closes and cleans up the binary dictionary.
+     */
+    @Override
+    public void close() {
+        // Ensure that no other threads are accessing the local binary dictionary.
+        mLocalDictionaryController.lock();
+        try {
+            if (mBinaryDictionary != null) {
+                mBinaryDictionary.close();
+                mBinaryDictionary = null;
+            }
+        } finally {
+            mLocalDictionaryController.unlock();
+        }
+    }
+
+    /**
+     * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on
+     * the native side.
+     */
+    public void clearFusionDictionary() {
+        mFusionDictionary = new FusionDictionary(new Node(), new FusionDictionary.DictionaryOptions(
+                new HashMap<String, String>(), false, false));
+    }
+
+    /**
+     * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
+     * are done to update the binary dictionary.
+     */
+    // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
+    // considering performance regression.
+    protected void addWord(final String word, final int frequency) {
+        mFusionDictionary.add(word, frequency, null, null);
+    }
+
+    /**
+     * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are
+     * done to update the binary 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);
+    }
+
+    @Override
+    public void getWords(final WordComposer codes, final WordCallback callback,
+            final ProximityInfo proximityInfo) {
+        asyncReloadDictionaryIfRequired();
+        getWordsInner(codes, callback, proximityInfo);
+    }
+
+    protected final void getWordsInner(final WordComposer codes, final WordCallback callback,
+            final ProximityInfo proximityInfo) {
+        // Ensure that there are no concurrent calls to getWords. If there are, do nothing and
+        // return.
+        if (mLocalDictionaryController.tryLock()) {
+            try {
+                if (mBinaryDictionary != null) {
+                    mBinaryDictionary.getWords(codes, callback, proximityInfo);
+                }
+            } finally {
+                mLocalDictionaryController.unlock();
+            }
+        }
+    }
+
+    @Override
+    public void getBigrams(final WordComposer codes, final CharSequence previousWord,
+            final WordCallback callback) {
+        asyncReloadDictionaryIfRequired();
+        getBigramsInner(codes, previousWord, callback);
+    }
+
+    protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord,
+            final WordCallback callback) {
+        if (mLocalDictionaryController.tryLock()) {
+            try {
+                if (mBinaryDictionary != null) {
+                    mBinaryDictionary.getBigrams(codes, previousWord, callback);
+                }
+            } finally {
+                mLocalDictionaryController.unlock();
+            }
+        }
+    }
+
+    @Override
+    public boolean isValidWord(final CharSequence word) {
+        asyncReloadDictionaryIfRequired();
+        return isValidWordInner(word);
+    }
+
+    protected boolean isValidWordInner(final CharSequence word) {
+        if (mLocalDictionaryController.tryLock()) {
+            try {
+                if (mBinaryDictionary != null) {
+                    return mBinaryDictionary.isValidWord(word);
+                }
+            } finally {
+                mLocalDictionaryController.unlock();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Load the current binary dictionary from internal storage in a background thread. If no binary
+     * dictionary exists, this method will generate one.
+     */
+    protected void loadDictionary() {
+        mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis();
+        asyncReloadDictionaryIfRequired();
+    }
+
+    /**
+     * Loads the current binary dictionary from internal storage. Assumes the dictionary file
+     * exists.
+     */
+    protected void loadBinaryDictionary() {
+        if (DEBUG) {
+            Log.d(TAG, "Loading binary dictionary: request="
+                    + mSharedDictionaryController.mLastUpdateRequestTime + " update="
+                    + mSharedDictionaryController.mLastUpdateTime);
+        }
+
+        final File file = new File(mContext.getFilesDir(), mFilename);
+        final String filename = file.getAbsolutePath();
+        final long length = file.length();
+
+        // Build the new binary dictionary
+        final BinaryDictionary newBinaryDictionary =
+                new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */,
+                        null);
+
+        if (mBinaryDictionary != null) {
+            // Ensure all threads accessing the current dictionary have finished before swapping in
+            // the new one.
+            final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
+            mLocalDictionaryController.lock();
+            mBinaryDictionary = newBinaryDictionary;
+            mLocalDictionaryController.unlock();
+            oldBinaryDictionary.close();
+        } else {
+            mBinaryDictionary = newBinaryDictionary;
+        }
+    }
+
+    /**
+     * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
+     */
+    private void generateBinaryDictionary() {
+        if (DEBUG) {
+            Log.d(TAG, "Generating binary dictionary: request="
+                    + mSharedDictionaryController.mLastUpdateRequestTime + " update="
+                    + mSharedDictionaryController.mLastUpdateTime);
+        }
+
+        loadDictionaryAsync();
+
+        final File file = new File(mContext.getFilesDir(), mFilename);
+        FileOutputStream out = null;
+        try {
+            out = new FileOutputStream(file);
+            BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1);
+            out.flush();
+            out.close();
+            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
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets whether or not the dictionary is out of date and requires a reload.
+     */
+    protected void setRequiresReload(final boolean reload) {
+        final long time = reload ? SystemClock.uptimeMillis() : 0;
+        mSharedDictionaryController.mLastUpdateRequestTime = time;
+        mLocalDictionaryController.mLastUpdateRequestTime = time;
+        if (DEBUG) {
+            Log.d(TAG, "Reload request: request=" + time + " update="
+                    + mSharedDictionaryController.mLastUpdateTime);
+        }
+    }
+
+    /**
+     * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread.
+     */
+    void asyncReloadDictionaryIfRequired() {
+        new AsyncReloadDictionaryTask().start();
+    }
+
+    /**
+     * Reloads the dictionary if required. Access is controlled on a per dictionary file basis and
+     * supports concurrent calls from multiple instances that share the same dictionary file.
+     */
+    protected final void syncReloadDictionaryIfRequired() {
+        if (mBinaryDictionary != null && !mLocalDictionaryController.isOutOfDate()) {
+            return;
+        }
+
+        // Ensure that only one thread attempts to read or write to the shared binary dictionary
+        // file at the same time.
+        mSharedDictionaryController.lock();
+        try {
+            final long time = SystemClock.uptimeMillis();
+            if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists()) {
+                // If the shared dictionary file does not exist or is out of date, the first
+                // instance that acquires the lock will generate a new one.
+                mSharedDictionaryController.mLastUpdateTime = time;
+                mLocalDictionaryController.mLastUpdateTime = time;
+                generateBinaryDictionary();
+                loadBinaryDictionary();
+            } else if (mLocalDictionaryController.isOutOfDate()) {
+                // Otherwise, if only the local dictionary for this instance is out of date, load
+                // the shared dictionary from file.
+                mLocalDictionaryController.mLastUpdateTime = time;
+                loadBinaryDictionary();
+            }
+        } finally {
+            mSharedDictionaryController.unlock();
+        }
+    }
+
+    private boolean dictionaryFileExists() {
+        final File file = new File(mContext.getFilesDir(), mFilename);
+        return file.exists();
+    }
+
+    /**
+     * Thread class for asynchronously reloading and rewriting the binary dictionary.
+     */
+    private class AsyncReloadDictionaryTask extends Thread {
+        @Override
+        public void run() {
+            syncReloadDictionaryIfRequired();
+        }
+    }
+
+    /**
+     * Lock for controlling access to a given binary dictionary and for tracking whether the
+     * dictionary is out of date. Can be shared across multiple dictionary instances that access the
+     * same filename.
+     */
+    private static class DictionaryController extends ReentrantLock {
+        private volatile long mLastUpdateTime = 0;
+        private volatile long mLastUpdateRequestTime = 0;
+
+        private boolean isOutOfDate() {
+            return (mLastUpdateRequestTime > mLastUpdateTime);
+        }
+    }
+}
-- 
GitLab