Merge "Import translations. DO NOT MERGE"
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index be9f879..3bed2c7 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -28,7 +28,7 @@
 
 import com.android.inputmethod.compat.DownloadManagerCompatUtils;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LogUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 import com.android.inputmethod.latin.utils.Utils;
 
 import java.util.LinkedList;
@@ -100,7 +100,7 @@
         final boolean mForceStartNow;
         public StartDownloadAction(final String clientId,
                 final WordListMetadata wordList, final boolean forceStartNow) {
-            LogUtils.l("New download action for client ", clientId, " : ", wordList);
+            DebugLogUtils.l("New download action for client ", clientId, " : ", wordList);
             mClientId = clientId;
             mWordList = wordList;
             mForceStartNow = forceStartNow;
@@ -112,7 +112,7 @@
                 Log.e(TAG, "UpdateAction with a null parameter!");
                 return;
             }
-            LogUtils.l("Downloading word list");
+            DebugLogUtils.l("Downloading word list");
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -134,7 +134,7 @@
                         + " for an upgrade action. Fall back to download.");
             }
             // Download it.
-            LogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
+            DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
 
             // TODO: if DownloadManager is disabled or not installed, download by ourselves
             if (null == manager) return;
@@ -180,7 +180,7 @@
 
             final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
                     mWordList.mId, mWordList.mVersion);
-            LogUtils.l("Starting download of", uri, "with id", downloadId);
+            DebugLogUtils.l("Starting download of", uri, "with id", downloadId);
             PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
         }
     }
@@ -197,7 +197,7 @@
 
         public InstallAfterDownloadAction(final String clientId,
                 final ContentValues wordListValues) {
-            LogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
+            DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
                     wordListValues);
             mClientId = clientId;
             mWordListValues = wordListValues;
@@ -216,7 +216,7 @@
                         + " for an InstallAfterDownload action. Bailing out.");
                 return;
             }
-            LogUtils.l("Setting word list as installed");
+            DebugLogUtils.l("Setting word list as installed");
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues);
         }
@@ -232,7 +232,7 @@
         final WordListMetadata mWordList;
 
         public EnableAction(final String clientId, final WordListMetadata wordList) {
-            LogUtils.l("New EnableAction for client ", clientId, " : ", wordList);
+            DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList);
             mClientId = clientId;
             mWordList = wordList;
         }
@@ -243,7 +243,7 @@
                 Log.e(TAG, "EnableAction with a null parameter!");
                 return;
             }
-            LogUtils.l("Enabling word list");
+            DebugLogUtils.l("Enabling word list");
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -267,7 +267,7 @@
         // The word list to disable. May not be null.
         final WordListMetadata mWordList;
         public DisableAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New Disable action for client ", clientId, " : ", wordlist);
+            DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -278,7 +278,7 @@
                 Log.e(TAG, "DisableAction with a null word list!");
                 return;
             }
-            LogUtils.l("Disabling word list : " + mWordList);
+            DebugLogUtils.l("Disabling word list : " + mWordList);
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -314,7 +314,7 @@
         // The word list to make available. May not be null.
         final WordListMetadata mWordList;
         public MakeAvailableAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New MakeAvailable action", clientId, " : ", wordlist);
+            DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -331,7 +331,7 @@
                 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
                         + " for a makeavailable action. Marking as available anyway.");
             }
-            LogUtils.l("Making word list available : " + mWordList);
+            DebugLogUtils.l("Making word list available : " + mWordList);
             // If mLocalFilename is null, then it's a remote file that hasn't been downloaded
             // yet, so we set the local filename to the empty string.
             final ContentValues values = MetadataDbHelper.makeContentValues(0,
@@ -363,7 +363,7 @@
         // The word list to mark pre-installed. May not be null.
         final WordListMetadata mWordList;
         public MarkPreInstalledAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
+            DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -380,7 +380,7 @@
                 Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
                         + " for a markpreinstalled action. Marking as preinstalled anyway.");
             }
-            LogUtils.l("Marking word list preinstalled : " + mWordList);
+            DebugLogUtils.l("Marking word list preinstalled : " + mWordList);
             // This word list is pre-installed : we don't have its file. We should reset
             // the local file name to the empty string so that we don't try to open it
             // accidentally. The remote filename may be set by the application if it so wishes.
@@ -404,7 +404,7 @@
         private final String mClientId;
         final WordListMetadata mWordList;
         public UpdateDataAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist);
+            DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -422,7 +422,7 @@
                 Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out.");
                 return;
             }
-            LogUtils.l("Updating data about a word list : " + mWordList);
+            DebugLogUtils.l("Updating data about a word list : " + mWordList);
             final ContentValues values = MetadataDbHelper.makeContentValues(
                     oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN),
                     oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN),
@@ -456,7 +456,7 @@
         final boolean mHasNewerVersion;
         public ForgetAction(final String clientId, final WordListMetadata wordlist,
                 final boolean hasNewerVersion) {
-            LogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist);
+            DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
             mHasNewerVersion = hasNewerVersion;
@@ -468,7 +468,7 @@
                 Log.e(TAG, "TryRemoveAction with a null word list!");
                 return;
             }
-            LogUtils.l("Trying to remove word list : " + mWordList);
+            DebugLogUtils.l("Trying to remove word list : " + mWordList);
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -528,7 +528,7 @@
         // The word list to delete. May not be null.
         final WordListMetadata mWordList;
         public StartDeleteAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist);
+            DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -539,7 +539,7 @@
                 Log.e(TAG, "StartDeleteAction with a null word list!");
                 return;
             }
-            LogUtils.l("Trying to delete word list : " + mWordList);
+            DebugLogUtils.l("Trying to delete word list : " + mWordList);
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -567,7 +567,7 @@
         // The word list to delete. May not be null.
         final WordListMetadata mWordList;
         public FinishDeleteAction(final String clientId, final WordListMetadata wordlist) {
-            LogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist);
+            DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist);
             mClientId = clientId;
             mWordList = wordlist;
         }
@@ -578,7 +578,7 @@
                 Log.e(TAG, "FinishDeleteAction with a null word list!");
                 return;
             }
-            LogUtils.l("Trying to delete word list : " + mWordList);
+            DebugLogUtils.l("Trying to delete word list : " + mWordList);
             final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
@@ -635,7 +635,7 @@
      * @param reporter a Reporter to send errors to.
      */
     public void execute(final Context context, final ProblemReporter reporter) {
-        LogUtils.l("Executing a batch of actions");
+        DebugLogUtils.l("Executing a batch of actions");
         Queue<Action> remainingActions = mActions;
         while (!remainingActions.isEmpty()) {
             final Action a = remainingActions.poll();
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index 4aa4d4b..62b905d 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -31,7 +31,7 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LogUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -220,7 +220,7 @@
     @Override
     public Cursor query(final Uri uri, final String[] projection, final String selection,
             final String[] selectionArgs, final String sortOrder) {
-        LogUtils.l("Uri =", uri);
+        DebugLogUtils.l("Uri =", uri);
         PrivateLog.log("Query : " + uri);
         final String clientId = getClientId(uri);
         final int match = matchUri(uri);
@@ -228,7 +228,7 @@
             case DICTIONARY_V1_WHOLE_LIST:
             case DICTIONARY_V2_WHOLE_LIST:
                 final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
-                LogUtils.l("List of dictionaries with count", c.getCount());
+                DebugLogUtils.l("List of dictionaries with count", c.getCount());
                 PrivateLog.log("Returned a list of " + c.getCount() + " items");
                 return c;
             case DICTIONARY_V2_DICT_INFO:
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index c6cadb8..99cc5b9 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -25,7 +25,7 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LogUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -773,13 +773,13 @@
         if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri
                 || null == valuesMetadataAdditionalId) {
             // We need all these columns to be filled in
-            LogUtils.l("Missing parameter for updateClientInfo");
+            DebugLogUtils.l("Missing parameter for updateClientInfo");
             return;
         }
         if (!clientId.equals(valuesClientId)) {
             // Mismatch! The client violates the protocol.
-            LogUtils.l("Received an updateClientInfo request for ", clientId, " but the values "
-                    + "contain a different ID : ", valuesClientId);
+            DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
+                    " but the values " + "contain a different ID : ", valuesClientId);
             return;
         }
         final SQLiteDatabase defaultDb = getDb(context, "");
@@ -848,7 +848,7 @@
             final ContentValues r) {
         switch (r.getAsInteger(TYPE_COLUMN)) {
             case TYPE_BULK:
-                LogUtils.l("Ended processing a wordlist");
+                DebugLogUtils.l("Ended processing a wordlist");
                 // Updating a bulk word list is a three-step operation:
                 // - Add the new entry to the table
                 // - Remove the old entry from the table
@@ -870,7 +870,7 @@
                         // the phone is suddenly cut during an update.
                         final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
                         do {
-                            LogUtils.l("Setting for removal", c.getString(filenameIndex));
+                            DebugLogUtils.l("Setting for removal", c.getString(filenameIndex));
                             filenames.add(c.getString(filenameIndex));
                         } while (c.moveToNext());
                     }
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 936d00d..719f24e 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -38,7 +38,7 @@
 import com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
 import com.android.inputmethod.compat.DownloadManagerCompatUtils;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LogUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 import com.android.inputmethod.latin.utils.Utils;
 
 import java.io.File;
@@ -185,8 +185,8 @@
                 final String clientId = cursor.getString(0);
                 final String metadataUri =
                         MetadataDbHelper.getMetadataUriAsString(context, clientId);
-                PrivateLog.log("Update for clientId " + LogUtils.s(clientId));
-                LogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
+                PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId));
+                DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
                 uris.add(metadataUri);
             } while (cursor.moveToNext());
         } finally {
@@ -213,14 +213,14 @@
      */
     private static void updateClientsWithMetadataUri(final Context context,
             final boolean updateNow, final String metadataUri) {
-        PrivateLog.log("Update for metadata URI " + LogUtils.s(metadataUri));
+        PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri));
         // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
         // DownloadManager also stupidly cuts the extension to replace with its own that it
         // gets from the content-type. We need to circumvent this.
         final String disambiguator = "#" + System.currentTimeMillis()
                 + Utils.getVersionName(context) + ".json";
         final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
-        LogUtils.l("Request =", metadataRequest);
+        DebugLogUtils.l("Request =", metadataRequest);
 
         final Resources res = context.getResources();
         // By default, download over roaming is allowed and all network types are allowed too.
@@ -256,7 +256,7 @@
         final long downloadId;
         synchronized (sSharedIdProtector) {
             downloadId = manager.enqueue(metadataRequest);
-            LogUtils.l("Metadata download requested with id", downloadId);
+            DebugLogUtils.l("Metadata download requested with id", downloadId);
             // If there is already a download in progress, it's been there for a while and
             // there is probably something wrong with download manager. It's best to just
             // overwrite the id and request it again. If the old one happens to finish
@@ -328,11 +328,11 @@
      */
     public static long registerDownloadRequest(final DownloadManager manager, final Request request,
             final SQLiteDatabase db, final String id, final int version) {
-        LogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
+        DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
         final long downloadId;
         synchronized (sSharedIdProtector) {
             downloadId = manager.enqueue(request);
-            LogUtils.l("Download requested with id", downloadId);
+            DebugLogUtils.l("Download requested with id", downloadId);
             MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId);
         }
         return downloadId;
@@ -418,7 +418,7 @@
         // Get and check the ID of the file that was downloaded
         final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
         PrivateLog.log("Download finished with id " + fileId);
-        LogUtils.l("DownloadFinished with id", fileId);
+        DebugLogUtils.l("DownloadFinished with id", fileId);
         if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
 
         final DownloadManager manager =
@@ -428,7 +428,7 @@
         final ArrayList<DownloadRecord> recordList =
                 getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo);
         if (null == recordList) return; // It was someone else's download.
-        LogUtils.l("Received result for download ", fileId);
+        DebugLogUtils.l("Received result for download ", fileId);
 
         // TODO: handle gracefully a null pointer here. This is practically impossible because
         // we come here only when DownloadManager explicitly called us when it ended a
@@ -505,7 +505,7 @@
     private static void publishUpdateCycleCompletedEvent(final Context context) {
         // Even if this is not successful, we have to publish the new state.
         PrivateLog.log("Publishing update cycle completed event");
-        LogUtils.l("Publishing update cycle completed event");
+        DebugLogUtils.l("Publishing update cycle completed event");
         for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
             listener.updateCycleCompleted();
         }
@@ -519,12 +519,12 @@
             // {@link handleWordList(Context,InputStream,ContentValues)}.
             // Handle the downloaded file according to its type
             if (downloadRecord.isMetadata()) {
-                LogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
+                DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
                 // #handleMetadata() closes its InputStream argument
                 handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream(
                         manager.openDownloadedFile(fileId)), downloadRecord.mClientId);
             } else {
-                LogUtils.l("Data D/L'd is a word list");
+                DebugLogUtils.l("Data D/L'd is a word list");
                 final int wordListStatus = downloadRecord.mAttributes.getAsInteger(
                         MetadataDbHelper.STATUS_COLUMN);
                 if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) {
@@ -584,7 +584,7 @@
      */
     private static void handleMetadata(final Context context, final InputStream stream,
             final String clientId) throws IOException, BadFormatException {
-        LogUtils.l("Entering handleMetadata");
+        DebugLogUtils.l("Entering handleMetadata");
         final List<WordListMetadata> newMetadata;
         final InputStreamReader reader = new InputStreamReader(stream);
         try {
@@ -594,7 +594,7 @@
             reader.close();
         }
 
-        LogUtils.l("Downloaded metadata :", newMetadata);
+        DebugLogUtils.l("Downloaded metadata :", newMetadata);
         PrivateLog.log("Downloaded metadata\n" + newMetadata);
 
         final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
@@ -619,7 +619,7 @@
         // DownloadManager does not have the ability to put the file directly where we want
         // it, so we had it download to a temporary place. Now we move it. It will be deleted
         // automatically by DownloadManager.
-        LogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
+        DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
                 MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
         PrivateLog.log("Downloaded a new word list with description : "
                 + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
@@ -678,9 +678,9 @@
      */
     private static void copyFile(final InputStream in, final OutputStream out)
             throws IOException {
-        LogUtils.l("Copying files");
+        DebugLogUtils.l("Copying files");
         if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) {
-            LogUtils.l("Not the right types");
+            DebugLogUtils.l("Not the right types");
             copyFileFallback(in, out);
         } else {
             try {
@@ -689,7 +689,7 @@
                 sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel);
             } catch (IOException e) {
                 // Can't work with channels, or something went wrong. Copy by hand.
-                LogUtils.l("Won't work");
+                DebugLogUtils.l("Won't work");
                 copyFileFallback(in, out);
             }
         }
@@ -704,7 +704,7 @@
      */
     private static void copyFileFallback(final InputStream in, final OutputStream out)
             throws IOException {
-        LogUtils.l("Falling back to slow copy");
+        DebugLogUtils.l("Falling back to slow copy");
         final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE];
         for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer))
             out.write(buffer, 0, readBytes);
@@ -719,10 +719,10 @@
      */
     private static String getTempFileName(final Context context, final String locale)
             throws IOException {
-        LogUtils.l("Entering openTempFileOutput");
+        DebugLogUtils.l("Entering openTempFileOutput");
         final File dir = context.getFilesDir();
         final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir);
-        LogUtils.l("File name is", f.getName());
+        DebugLogUtils.l("File name is", f.getName());
         return f.getName();
     }
 
@@ -743,7 +743,7 @@
             final String clientId, List<WordListMetadata> from, List<WordListMetadata> to) {
         final ActionBatch actions = new ActionBatch();
         // Upgrade existing word lists
-        LogUtils.l("Comparing dictionaries");
+        DebugLogUtils.l("Comparing dictionaries");
         final Set<String> wordListIds = new TreeSet<String>();
         // TODO: Can these be null?
         if (null == from) from = new ArrayList<WordListMetadata>();
@@ -758,7 +758,7 @@
             final WordListMetadata newInfo = null == metadataInfo
                     || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION
                             ? null : metadataInfo;
-            LogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
+            DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
 
             if (null == currentInfo && null == newInfo) {
                 // This may happen if a new word list appeared that we can't handle.
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 05b1a2e..f36c9e8 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -78,20 +78,23 @@
      * @param length the length of the binary data.
      * @param useFullEditDistance whether to use the full edit distance in suggestions
      * @param dictType the dictionary type, as a human-readable string
+     * @param isUpdatable whether to open the dictionary file in writable mode.
      */
     public BinaryDictionary(final String filename, final long offset, final long length,
-            final boolean useFullEditDistance, final Locale locale, final String dictType) {
+            final boolean useFullEditDistance, final Locale locale, final String dictType,
+            final boolean isUpdatable) {
         super(dictType);
         mLocale = locale;
         mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
-        loadDictionary(filename, offset, length);
+        loadDictionary(filename, offset, length, isUpdatable);
     }
 
     static {
         JniUtils.loadNativeLibrary();
     }
 
-    private static native long openNative(String sourceDir, long dictOffset, long dictSize);
+    private static native long openNative(String sourceDir, long dictOffset, long dictSize,
+            boolean isUpdatable);
     private static native void closeNative(long dict);
     private static native int getProbabilityNative(long dict, int[] word);
     private static native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
@@ -105,8 +108,8 @@
 
     // TODO: Move native dict into session
     private final void loadDictionary(final String path, final long startOffset,
-            final long length) {
-        mNativeDict = openNative(path, startOffset, length);
+            final long length, final boolean isUpdatable) {
+        mNativeDict = openNative(path, startOffset, length, isUpdatable);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 5b98613..3721132 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -60,7 +60,8 @@
         if (null != assetFileList) {
             for (final AssetFileAddress f : assetFileList) {
                 final BinaryDictionary binaryDictionary = new BinaryDictionary(f.mFilename,
-                        f.mOffset, f.mLength, useFullEditDistance, locale, Dictionary.TYPE_MAIN);
+                        f.mOffset, f.mLength, useFullEditDistance, locale, Dictionary.TYPE_MAIN,
+                        false /* isUpdatable */);
                 if (binaryDictionary.isValidDictionary()) {
                     dictList.add(binaryDictionary);
                 }
@@ -113,7 +114,8 @@
                 return null;
             }
             return new BinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(),
-                    false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
+                    false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN,
+                    false /* isUpdatable */);
         } catch (android.content.res.Resources.NotFoundException e) {
             Log.e(TAG, "Could not find the resource");
             return null;
@@ -142,7 +144,7 @@
         for (final AssetFileAddress address : dictionaryList) {
             final BinaryDictionary binaryDictionary = new BinaryDictionary(address.mFilename,
                     address.mOffset, address.mLength, useFullEditDistance, locale,
-                    Dictionary.TYPE_MAIN);
+                    Dictionary.TYPE_MAIN, false /* isUpdatable */);
             dictionaryCollection.addDictionary(binaryDictionary);
         }
         return dictionaryCollection;
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index f357e2a..9cdb86c 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -286,7 +286,7 @@
 
         // Build the new binary dictionary
         final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
-                true /* useFullEditDistance */, null, mDictType);
+                true /* useFullEditDistance */, null, mDictType, false /* isUpdatable */);
 
         if (mBinaryDictionary != null) {
             // Ensure all threads accessing the current dictionary have finished before swapping in
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 0efe0eb..6fcac9a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -926,19 +926,15 @@
             }
         }
 
-        // TODO: refactor the following code to be less contrived.
-        // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
-        // that the cursor is not at the end of the composing span, or there is a selection.
-        // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
-        // as last time we were called (if there is a selection, it means the start hasn't
-        // changed, so it's the end that did).
-        final boolean selectionChanged = (newSelStart != composingSpanEnd
-                || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
+        final boolean selectionChanged = mLastSelectionStart != newSelStart
+                || mLastSelectionEnd != newSelEnd;
         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
         // span in the view - we can use that to narrow down whether the cursor was moved
         // by us or not. If we are composing a word but there is no composing span, then
         // we know for sure the cursor moved while we were composing and we should reset
-        // the state.
+        // the state. TODO: rescind this policy: the framework never removes the composing
+        // span on its own accord while editing. This test is useless.
+
         final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
         // will be reset when the keyboard shows up anyway.
@@ -979,6 +975,7 @@
             if (isSuggestionsStripVisible()) {
                 mHandler.postResumeSuggestions();
             }
+            mConnection.userMovedCursor(newSelEnd);
             // Reset the last recapitalization.
             mRecapitalizeStatus.deactivate();
             mKeyboardSwitcher.updateShiftState();
@@ -2655,11 +2652,6 @@
         }
     }
 
-    // Used by the RingCharBuffer
-    public boolean isWordSeparator(final int code) {
-        return mSettings.getCurrent().isWordSeparator(code);
-    }
-
     // TODO: Make this private
     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
     @UsedForTesting
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index d431ad6..6e3e7b2 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -28,7 +28,7 @@
 
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
-import com.android.inputmethod.latin.utils.LogUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 import com.android.inputmethod.research.ResearchLogger;
 
@@ -107,7 +107,7 @@
                     + "\nActual text = " + reference.length() + " " + reference;
             ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
         } else {
-            Log.e(TAG, LogUtils.getStackTrace(2));
+            Log.e(TAG, DebugLogUtils.getStackTrace(2));
             Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
         }
     }
@@ -156,7 +156,7 @@
         if (mNestLevel != 1) {
             // TODO: exception instead
             Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
-            Log.e(TAG, LogUtils.getStackTrace(4));
+            Log.e(TAG, DebugLogUtils.getStackTrace(4));
         }
     }
 
@@ -340,7 +340,6 @@
     public void setComposingRegion(final int start, final int end) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
-        mCurrentCursorPosition = end;
         final CharSequence textBeforeCursor =
                 getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0);
         mCommittedTextBeforeComposingText.setLength(0);
@@ -731,6 +730,14 @@
     }
 
     /**
+     * The user moved the cursor by hand. Take a note of it.
+     * @param newCursorPosition The new cursor position.
+     */
+    public void userMovedCursor(final int newCursorPosition) {
+        mCurrentCursorPosition = newCursorPosition;
+    }
+
+    /**
      * Looks at the text just before the cursor to find out if it looks like a URL.
      *
      * The weakest point here is, if we don't have enough text bufferized, we may fail to realize
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 084d330..adc92d0 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -152,6 +152,10 @@
         return mSettingsValues.mWordSeparators;
     }
 
+    public boolean isWordSeparator(final int code) {
+        return mSettingsValues.isWordSeparator(code);
+    }
+
     public Locale getCurrentLocale() {
         return mCurrentLocale;
     }
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 3f94aca..30df2eb 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -26,9 +26,9 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils.RunInLocale;
-import com.android.inputmethod.latin.utils.LogUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.util.HashMap;
@@ -242,7 +242,7 @@
                             + " nameResId=" + subtype.getNameResId()
                             + " locale=" + subtype.getLocale()
                             + " extra=" + subtype.getExtraValue()
-                            + "\n" + LogUtils.getStackTrace());
+                            + "\n" + DebugLogUtils.getStackTrace());
                     return "";
                 }
             }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index b6caf9b..9565f63 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -86,6 +86,7 @@
     private final float mAlphaObsoleted;
     private final float mCenterSuggestionWeight;
     private final int mCenterPositionInStrip;
+    private final int mTypedWordPositionWhenAutocorrect;
     private final Drawable mMoreSuggestionsHint;
     private static final String MORE_SUGGESTIONS_HINT = "\u2026";
     private static final String LEFTWARDS_ARROW = "\u2190";
@@ -159,6 +160,10 @@
         mMoreSuggestionsHint = getMoreSuggestionsHint(res,
                 res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
         mCenterPositionInStrip = mSuggestionsCountInStrip / 2;
+        // Assuming there are at least three suggestions. Also, note that the suggestions are
+        // laid out according to script direction, so this is left of the center for LTR scripts
+        // and right of the center for RTL scripts.
+        mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1;
         mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
                 R.dimen.more_suggestions_bottom_gap);
         mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height);
@@ -233,24 +238,31 @@
         return spannedWord;
     }
 
-    private int getIndexInSuggestedWords(final int positionInStrip,
+    private int getPositionInSuggestionStrip(final int indexInSuggestedWords,
             final SuggestedWords suggestedWords) {
-        // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
-        // suggestions.
-        final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect()
-                ? SuggestedWords.INDEX_OF_AUTO_CORRECTION : SuggestedWords.INDEX_OF_TYPED_WORD;
-        if (positionInStrip == mCenterPositionInStrip) {
-            return mostImportantIndexInSuggestedWords;
+        final int indexToDisplayMostImportantSuggestion;
+        final int indexToDisplaySecondMostImportantSuggestion;
+        if (suggestedWords.willAutoCorrect()) {
+            indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
+            indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
+        } else {
+            indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
+            indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
         }
-        if (positionInStrip == mostImportantIndexInSuggestedWords) {
+        if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) {
             return mCenterPositionInStrip;
         }
-        return positionInStrip;
+        if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) {
+            return mTypedWordPositionWhenAutocorrect;
+        }
+        // If neither of those, the order in the suggestion strip is the same as in SuggestedWords.
+        return indexInSuggestedWords;
     }
 
-    private int getSuggestionTextColor(final int positionInStrip,
+    private int getSuggestionTextColor(final int indexInSuggestedWords,
             final SuggestedWords suggestedWords) {
-        final int indexInSuggestedWords = getIndexInSuggestedWords(positionInStrip, suggestedWords);
+        final int positionInStrip =
+                getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords);
         // TODO: Need to revisit this logic with bigram suggestions
         final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD);
 
@@ -352,7 +364,7 @@
      * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
      * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This
      * usually doesn't match the index in <code>suggedtedWords</code> -- see
-     * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
+     * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}.
      *
      * @param positionInStrip the position in the suggestion strip.
      * @param width the maximum width for layout in pixels.
@@ -413,10 +425,19 @@
 
     private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords,
             final int countInStrip) {
+        // Clear all suggestions first
+        for (int positionInStrip = 0; positionInStrip < countInStrip; ++positionInStrip) {
+            mWordViews.get(positionInStrip).setText(null);
+            // Make this inactive for touches in {@link #layoutWord(int,int)}.
+            if (SuggestionStripView.DBG) {
+                mDebugInfoViews.get(positionInStrip).setText(null);
+            }
+        }
         final int count = Math.min(suggestedWords.size(), countInStrip);
-        for (int positionInStrip = 0; positionInStrip < count; positionInStrip++) {
-            final int indexInSuggestedWords =
-                    getIndexInSuggestedWords(positionInStrip, suggestedWords);
+        for (int indexInSuggestedWords = 0; indexInSuggestedWords < count;
+                indexInSuggestedWords++) {
+            final int positionInStrip =
+                    getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords);
             final TextView wordView = mWordViews.get(positionInStrip);
             // {@link TextView#getTag()} is used to get the index in suggestedWords at
             // {@link SuggestionStripView#onClick(View)}.
@@ -428,13 +449,6 @@
                         Utils.getDebugInfo(suggestedWords, indexInSuggestedWords));
             }
         }
-        for (int positionInStrip = count; positionInStrip < countInStrip; positionInStrip++) {
-            mWordViews.get(positionInStrip).setText(null);
-            // Make this inactive for touches in {@link #layoutWord(int,int)}.
-            if (SuggestionStripView.DBG) {
-                mDebugInfoViews.get(positionInStrip).setText(null);
-            }
-        }
     }
 
     private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
diff --git a/java/src/com/android/inputmethod/latin/utils/LogUtils.java b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
similarity index 97%
rename from java/src/com/android/inputmethod/latin/utils/LogUtils.java
rename to java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
index a0d2e04..c4ead0a 100644
--- a/java/src/com/android/inputmethod/latin/utils/LogUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
@@ -23,8 +23,8 @@
 /**
  * A class for logging and debugging utility methods.
  */
-public final class LogUtils {
-    private final static String TAG = LogUtils.class.getSimpleName();
+public final class DebugLogUtils {
+    private final static String TAG = DebugLogUtils.class.getSimpleName();
     private final static boolean sDBG = LatinImeLogger.sDBG;
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
new file mode 100644
index 0000000..3e67e82
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
@@ -0,0 +1,134 @@
+/*
+ * 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.utils;
+
+import android.inputmethodservice.InputMethodService;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.utils.Utils.UsabilityStudyLogUtils;
+
+public final class UserLogRingCharBuffer {
+    public /* for test */ static final int BUFSIZE = 20;
+    public /* for test */ int mLength = 0;
+
+    private static UserLogRingCharBuffer sUserLogRingCharBuffer = new UserLogRingCharBuffer();
+    private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
+    private static final int INVALID_COORDINATE = -2;
+    private boolean mEnabled = false;
+    private int mEnd = 0;
+    private char[] mCharBuf = new char[BUFSIZE];
+    private int[] mXBuf = new int[BUFSIZE];
+    private int[] mYBuf = new int[BUFSIZE];
+
+    private UserLogRingCharBuffer() {
+        // Intentional empty constructor for singleton.
+    }
+
+    @UsedForTesting
+    public static UserLogRingCharBuffer getInstance() {
+        return sUserLogRingCharBuffer;
+    }
+
+    public static UserLogRingCharBuffer init(final InputMethodService context,
+            final boolean enabled, final boolean usabilityStudy) {
+        if (!(enabled || usabilityStudy)) {
+            return null;
+        }
+        sUserLogRingCharBuffer.mEnabled = true;
+        UsabilityStudyLogUtils.getInstance().init(context);
+        return sUserLogRingCharBuffer;
+    }
+
+    private static int normalize(final int in) {
+        int ret = in % BUFSIZE;
+        return ret < 0 ? ret + BUFSIZE : ret;
+    }
+
+    // TODO: accept code points
+    @UsedForTesting
+    public void push(final char c, final int x, final int y) {
+        if (!mEnabled) {
+            return;
+        }
+        mCharBuf[mEnd] = c;
+        mXBuf[mEnd] = x;
+        mYBuf[mEnd] = y;
+        mEnd = normalize(mEnd + 1);
+        if (mLength < BUFSIZE) {
+            ++mLength;
+        }
+    }
+
+    public char pop() {
+        if (mLength < 1) {
+            return PLACEHOLDER_DELIMITER_CHAR;
+        }
+        mEnd = normalize(mEnd - 1);
+        --mLength;
+        return mCharBuf[mEnd];
+    }
+
+    public char getBackwardNthChar(final int n) {
+        if (mLength <= n || n < 0) {
+            return PLACEHOLDER_DELIMITER_CHAR;
+        }
+        return mCharBuf[normalize(mEnd - n - 1)];
+    }
+
+    public int getPreviousX(final char c, final int back) {
+        final int index = normalize(mEnd - 2 - back);
+        if (mLength <= back
+                || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+            return INVALID_COORDINATE;
+        }
+        return mXBuf[index];
+    }
+
+    public int getPreviousY(final char c, final int back) {
+        int index = normalize(mEnd - 2 - back);
+        if (mLength <= back
+                || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+            return INVALID_COORDINATE;
+        }
+        return mYBuf[index];
+    }
+
+    public String getLastWord(final int ignoreCharCount) {
+        final StringBuilder sb = new StringBuilder();
+        int i = ignoreCharCount;
+        for (; i < mLength; ++i) {
+            final char c = mCharBuf[normalize(mEnd - 1 - i)];
+            if (!Settings.getInstance().isWordSeparator(c)) {
+                break;
+            }
+        }
+        for (; i < mLength; ++i) {
+            char c = mCharBuf[normalize(mEnd - 1 - i)];
+            if (!Settings.getInstance().isWordSeparator(c)) {
+                sb.append(c);
+            } else {
+                break;
+            }
+        }
+        return sb.reverse().toString();
+    }
+
+    public void reset() {
+        mLength = 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/Utils.java b/java/src/com/android/inputmethod/latin/utils/Utils.java
index 390d306..c4e18ed 100644
--- a/java/src/com/android/inputmethod/latin/utils/Utils.java
+++ b/java/src/com/android/inputmethod/latin/utils/Utils.java
@@ -26,7 +26,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.inputmethodservice.InputMethodService;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -34,9 +33,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -63,135 +60,6 @@
         // This utility class is not publicly instantiable.
     }
 
-    /**
-     * Cancel an {@link AsyncTask}.
-     *
-     * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
-     *        task should be interrupted; otherwise, in-progress tasks are allowed
-     *        to complete.
-     */
-    public static void cancelTask(final AsyncTask<?, ?, ?> task,
-            final boolean mayInterruptIfRunning) {
-        if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
-            task.cancel(mayInterruptIfRunning);
-        }
-    }
-
-    // TODO: Make this an external class
-    public /* for test */ static final class RingCharBuffer {
-        public /* for test */ static final int BUFSIZE = 20;
-        public /* for test */ int mLength = 0;
-
-        private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
-        private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
-        private static final int INVALID_COORDINATE = -2;
-        private InputMethodService mContext;
-        private boolean mEnabled = false;
-        private int mEnd = 0;
-        private char[] mCharBuf = new char[BUFSIZE];
-        private int[] mXBuf = new int[BUFSIZE];
-        private int[] mYBuf = new int[BUFSIZE];
-
-        private RingCharBuffer() {
-            // Intentional empty constructor for singleton.
-        }
-
-        @UsedForTesting
-        public static RingCharBuffer getInstance() {
-            return sRingCharBuffer;
-        }
-
-        public static RingCharBuffer init(final InputMethodService context, final boolean enabled,
-                final boolean usabilityStudy) {
-            if (!(enabled || usabilityStudy)) {
-                return null;
-            }
-            sRingCharBuffer.mContext = context;
-            sRingCharBuffer.mEnabled = true;
-            UsabilityStudyLogUtils.getInstance().init(context);
-            return sRingCharBuffer;
-        }
-
-        private static int normalize(final int in) {
-            int ret = in % BUFSIZE;
-            return ret < 0 ? ret + BUFSIZE : ret;
-        }
-
-        // TODO: accept code points
-        @UsedForTesting
-        public void push(final char c, final int x, final int y) {
-            if (!mEnabled) {
-                return;
-            }
-            mCharBuf[mEnd] = c;
-            mXBuf[mEnd] = x;
-            mYBuf[mEnd] = y;
-            mEnd = normalize(mEnd + 1);
-            if (mLength < BUFSIZE) {
-                ++mLength;
-            }
-        }
-
-        public char pop() {
-            if (mLength < 1) {
-                return PLACEHOLDER_DELIMITER_CHAR;
-            }
-            mEnd = normalize(mEnd - 1);
-            --mLength;
-            return mCharBuf[mEnd];
-        }
-
-        public char getBackwardNthChar(final int n) {
-            if (mLength <= n || n < 0) {
-                return PLACEHOLDER_DELIMITER_CHAR;
-            }
-            return mCharBuf[normalize(mEnd - n - 1)];
-        }
-
-        public int getPreviousX(final char c, final int back) {
-            final int index = normalize(mEnd - 2 - back);
-            if (mLength <= back
-                    || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
-                return INVALID_COORDINATE;
-            }
-            return mXBuf[index];
-        }
-
-        public int getPreviousY(final char c, final int back) {
-            int index = normalize(mEnd - 2 - back);
-            if (mLength <= back
-                    || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
-                return INVALID_COORDINATE;
-            }
-            return mYBuf[index];
-        }
-
-        public String getLastWord(final int ignoreCharCount) {
-            final StringBuilder sb = new StringBuilder();
-            final LatinIME latinIme = (LatinIME)mContext;
-            int i = ignoreCharCount;
-            for (; i < mLength; ++i) {
-                final char c = mCharBuf[normalize(mEnd - 1 - i)];
-                if (!latinIme.isWordSeparator(c)) {
-                    break;
-                }
-            }
-            for (; i < mLength; ++i) {
-                char c = mCharBuf[normalize(mEnd - 1 - i)];
-                if (!latinIme.isWordSeparator(c)) {
-                    sb.append(c);
-                } else {
-                    break;
-                }
-            }
-            return sb.reverse().toString();
-        }
-
-        public void reset() {
-            mLength = 0;
-        }
-    }
-
     // TODO: Make this an external class
     public static final class UsabilityStudyLogUtils {
         // TODO: remove code duplication with ResearchLog class
@@ -409,7 +277,7 @@
     // TODO: Make this an external class
     public static final class Stats {
         public static void onNonSeparator(final char code, final int x, final int y) {
-            RingCharBuffer.getInstance().push(code, x, y);
+            UserLogRingCharBuffer.getInstance().push(code, x, y);
             LatinImeLogger.logOnInputChar();
         }
 
@@ -424,7 +292,7 @@
             for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
                 int codePoint = Character.codePointAt(separator, i);
                 // TODO: accept code points
-                RingCharBuffer.getInstance().push((char)codePoint, x, y);
+                UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y);
             }
             LatinImeLogger.logOnInputSeparator();
         }
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 1225e7f..b856718 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -40,7 +40,7 @@
 static void releaseDictBuf(const void *dictBuf, const size_t length, const int fd);
 
 static jlong latinime_BinaryDictionary_open(JNIEnv *env, jclass clazz, jstring sourceDir,
-        jlong dictOffset, jlong dictSize) {
+        jlong dictOffset, jlong dictSize, jboolean isUpdatable) {
     PROF_OPEN;
     PROF_START(66);
     const jsize sourceDirUtf8Length = env->GetStringUTFLength(sourceDir);
@@ -53,22 +53,25 @@
     sourceDirChars[sourceDirUtf8Length] = '\0';
     int fd = 0;
     void *dictBuf = 0;
-    int adjust = 0;
-    fd = open(sourceDirChars, O_RDONLY);
+    int offset = 0;
+    const bool updatableMmap = (isUpdatable == JNI_TRUE);
+    const int openMode = updatableMmap ? O_RDWR : O_RDONLY;
+    fd = open(sourceDirChars, openMode);
     if (fd < 0) {
         AKLOGE("DICT: Can't open sourceDir. sourceDirChars=%s errno=%d", sourceDirChars, errno);
         return 0;
     }
     int pagesize = getpagesize();
-    adjust = static_cast<int>(dictOffset) % pagesize;
-    int adjDictOffset = static_cast<int>(dictOffset) - adjust;
-    int adjDictSize = static_cast<int>(dictSize) + adjust;
-    dictBuf = mmap(0, adjDictSize, PROT_READ, MAP_PRIVATE, fd, adjDictOffset);
+    offset = static_cast<int>(dictOffset) % pagesize;
+    int adjDictOffset = static_cast<int>(dictOffset) - offset;
+    int adjDictSize = static_cast<int>(dictSize) + offset;
+    const int protMode = updatableMmap ? PROT_READ | PROT_WRITE : PROT_READ;
+    dictBuf = mmap(0, adjDictSize, protMode, MAP_PRIVATE, fd, adjDictOffset);
     if (dictBuf == MAP_FAILED) {
         AKLOGE("DICT: Can't mmap dictionary. errno=%d", errno);
         return 0;
     }
-    dictBuf = static_cast<char *>(dictBuf) + adjust;
+    dictBuf = static_cast<char *>(dictBuf) + offset;
     if (!dictBuf) {
         AKLOGE("DICT: dictBuf is null");
         return 0;
@@ -78,9 +81,10 @@
             == BinaryDictionaryFormat::detectFormatVersion(static_cast<uint8_t *>(dictBuf),
                     static_cast<int>(dictSize))) {
         AKLOGE("DICT: dictionary format is unknown, bad magic number");
-        releaseDictBuf(static_cast<const char *>(dictBuf) - adjust, adjDictSize, fd);
+        releaseDictBuf(static_cast<const char *>(dictBuf) - offset, adjDictSize, fd);
     } else {
-        dictionary = new Dictionary(dictBuf, static_cast<int>(dictSize), fd, adjust);
+        dictionary = new Dictionary(
+                dictBuf, static_cast<int>(dictSize), fd, offset, updatableMmap);
     }
     PROF_END(66);
     PROF_CLOSE;
@@ -221,10 +225,13 @@
 static void latinime_BinaryDictionary_close(JNIEnv *env, jclass clazz, jlong dict) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return;
-    const void *dictBuf = dictionary->getBinaryDictionaryInfo()->getDictBuf();
+    const BinaryDictionaryInfo *const binaryDictionaryInfo = dictionary->getBinaryDictionaryInfo();
+    const int dictBufOffset = binaryDictionaryInfo->getDictBufOffset();
+    const void *dictBuf = binaryDictionaryInfo->getDictBuf();
     if (!dictBuf) return;
-    releaseDictBuf(static_cast<const char *>(dictBuf) - dictionary->getDictBufAdjust(),
-            dictionary->getDictSize() + dictionary->getDictBufAdjust(), dictionary->getMmapFd());
+    releaseDictBuf(static_cast<const char *>(dictBuf) - dictBufOffset,
+            binaryDictionaryInfo->getDictSize() + dictBufOffset,
+            binaryDictionaryInfo->getMmapFd());
     delete dictionary;
 }
 
@@ -242,7 +249,7 @@
 static const JNINativeMethod sMethods[] = {
     {
         const_cast<char *>("openNative"),
-        const_cast<char *>("(Ljava/lang/String;JJ)J"),
+        const_cast<char *>("(Ljava/lang/String;JJZ)J"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_open)
     },
     {
diff --git a/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h b/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
index 0b77e5e..e0b5835 100644
--- a/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
+++ b/native/jni/src/suggest/core/dictionary/binary_dictionary_info.h
@@ -29,15 +29,29 @@
 
 class BinaryDictionaryInfo {
  public:
-    BinaryDictionaryInfo(const uint8_t *const dictBuf, const int dictSize)
-            : mDictBuf(dictBuf),
-              mDictionaryFormat(BinaryDictionaryFormat::detectFormatVersion(mDictBuf, dictSize)),
+    BinaryDictionaryInfo(const uint8_t *const dictBuf, const int dictSize, const int mmapFd,
+            const int dictBufOffset, const bool isUpdatable)
+            : mDictBuf(dictBuf), mDictSize(dictSize), mMmapFd(mmapFd),
+              mDictBufOffset(dictBufOffset), mIsUpdatable(isUpdatable),
+              mDictionaryFormat(BinaryDictionaryFormat::detectFormatVersion(mDictBuf, mDictSize)),
               mDictionaryHeader(this), mDictRoot(mDictBuf + mDictionaryHeader.getSize()) {}
 
     AK_FORCE_INLINE const uint8_t *getDictBuf() const {
         return mDictBuf;
     }
 
+    AK_FORCE_INLINE int getDictSize() const {
+        return mDictSize;
+    }
+
+    AK_FORCE_INLINE int getMmapFd() const {
+        return mMmapFd;
+    }
+
+    AK_FORCE_INLINE int getDictBufOffset() const {
+        return mDictBufOffset;
+    }
+
     AK_FORCE_INLINE const uint8_t *getDictRoot() const {
         return mDictRoot;
     }
@@ -54,10 +68,20 @@
         return &mDictionaryHeader;
     }
 
+    AK_FORCE_INLINE bool isDynamicallyUpdatable() const {
+        // TODO: Support dynamic dictionary formats.
+        const bool isUpdatableDictionaryFormat = false;
+        return mIsUpdatable && isUpdatableDictionaryFormat;
+    }
+
  private:
     DISALLOW_COPY_AND_ASSIGN(BinaryDictionaryInfo);
 
     const uint8_t *const mDictBuf;
+    const int mDictSize;
+    const int mMmapFd;
+    const int mDictBufOffset;
+    const bool mIsUpdatable;
     const BinaryDictionaryFormat::FORMAT_VERSION mDictionaryFormat;
     const BinaryDictionaryHeader mDictionaryHeader;
     const uint8_t *const mDictRoot;
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 27b052b..028b615 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -32,10 +32,9 @@
 
 namespace latinime {
 
-Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust)
-        : mBinaryDictionaryInfo(static_cast<const uint8_t *>(dict), dictSize),
-          mDictSize(dictSize),
-          mMmapFd(mmapFd), mDictBufAdjust(dictBufAdjust),
+Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufOffset, bool isUpdatable)
+        : mBinaryDictionaryInfo(static_cast<const uint8_t *>(dict), dictSize, mmapFd,
+                dictBufOffset, isUpdatable),
           mBigramDictionary(new BigramDictionary(&mBinaryDictionaryInfo)),
           mGestureSuggest(new Suggest(GestureSuggestPolicyFactory::getGestureSuggestPolicy())),
           mTypingSuggest(new Suggest(TypingSuggestPolicyFactory::getTypingSuggestPolicy())) {
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 151f261..afd0818 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -52,7 +52,7 @@
     static const int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
     static const int KIND_FLAG_EXACT_MATCH = 0x40000000;
 
-    Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust);
+    Dictionary(void *dict, int dictSize, int mmapFd, int dictBufOffset, bool isUpdatable);
 
     int getSuggestions(ProximityInfo *proximityInfo, DicTraverseSession *traverseSession,
             int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
@@ -68,21 +68,12 @@
     const BinaryDictionaryInfo *getBinaryDictionaryInfo() const {
         return &mBinaryDictionaryInfo;
     }
-    int getDictSize() const { return mDictSize; }
-    int getMmapFd() const { return mMmapFd; }
-    int getDictBufAdjust() const { return mDictBufAdjust; }
     virtual ~Dictionary();
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
 
     const BinaryDictionaryInfo mBinaryDictionaryInfo;
-    // Used only for the mmap version of dictionary loading, but we use these as dummy variables
-    // also for the malloc version.
-    const int mDictSize;
-    const int mMmapFd;
-    const int mDictBufAdjust;
-
     const BigramDictionary *mBigramDictionary;
     SuggestInterface *mGestureSuggest;
     SuggestInterface *mTypingSuggest;