Reorganize Utils class

Change-Id: I7294d1547def5dcfcae9d1d53b277cb3cc9f2d18
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index 3bed2c7..d5e638e 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -28,8 +28,8 @@
 
 import com.android.inputmethod.compat.DownloadManagerCompatUtils;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
-import com.android.inputmethod.latin.utils.Utils;
 
 import java.util.LinkedList;
 import java.util.Queue;
@@ -144,7 +144,7 @@
             // 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) + ".dict";
+                    + ApplicationUtils.getVersionName(context) + ".dict";
             final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
             final Request request = new Request(uri);
 
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 8a23acd..f66ef87 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -38,8 +38,8 @@
 import com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
 import com.android.inputmethod.compat.DownloadManagerCompatUtils;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
-import com.android.inputmethod.latin.utils.Utils;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -222,7 +222,7 @@
         // 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";
+                + ApplicationUtils.getVersionName(context) + ".json";
         final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
         DebugLogUtils.l("Request =", metadataRequest);
 
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index a4606f2..0af05b8 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -70,7 +70,7 @@
 import com.android.inputmethod.latin.utils.ResourceUtils;
 import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.utils.StringUtils;
-import com.android.inputmethod.latin.utils.Utils.UsabilityStudyLogUtils;
+import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils;
 import com.android.inputmethod.research.ResearchLogger;
 
 import java.util.Locale;
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index 01ec7f9..5dbc9b1 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -25,7 +25,7 @@
 import android.preference.PreferenceScreen;
 
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.latin.utils.Utils;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 
 public final class DebugSettings extends PreferenceFragment
         implements SharedPreferences.OnSharedPreferenceChangeListener {
@@ -118,7 +118,7 @@
         }
         boolean isDebugMode = mDebugMode.isChecked();
         final String version = getResources().getString(
-                R.string.version_text, Utils.getVersionName(getActivity()));
+                R.string.version_text, ApplicationUtils.getVersionName(getActivity()));
         if (!isDebugMode) {
             mDebugMode.setTitle(version);
             mDebugMode.setSummary("");
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f4f9dcc..8f5e571 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -76,19 +76,19 @@
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.CompletionInfoUtils;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
 import com.android.inputmethod.latin.utils.IntentUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
+import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
 import com.android.inputmethod.latin.utils.PositionalInfoForUserDictPendingAddition;
 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
 import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
 import com.android.inputmethod.latin.utils.TextRange;
-import com.android.inputmethod.latin.utils.Utils;
-import com.android.inputmethod.latin.utils.Utils.Stats;
 import com.android.inputmethod.research.ResearchLogger;
 
 import java.io.FileDescriptor;
@@ -1551,7 +1551,7 @@
             if (SPACE_STATE_PHANTOM == spaceState) {
                 if (mSettings.isInternal()) {
                     if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
-                        Stats.onAutoCorrection(
+                        LatinImeLoggerUtils.onAutoCorrection(
                                 "", mWordComposer.getTypedWord(), " ", mWordComposer);
                     }
                 }
@@ -1612,7 +1612,8 @@
         if (mWordComposer.isComposingWord()) {
             if (mSettings.isInternal()) {
                 if (mWordComposer.isBatchMode()) {
-                    Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer);
+                    LatinImeLoggerUtils.onAutoCorrection(
+                            "", mWordComposer.getTypedWord(), " ", mWordComposer);
                 }
             }
             final int wordComposerSize = mWordComposer.size();
@@ -1860,7 +1861,7 @@
         } else {
             if (mLastComposedWord.canRevertCommit()) {
                 if (mSettings.isInternal()) {
-                    Stats.onAutoCorrectionCancellation();
+                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
                 }
                 revertCommit();
                 return;
@@ -2031,7 +2032,7 @@
         }
         mHandler.postUpdateSuggestionStrip();
         if (mSettings.isInternal()) {
-            Utils.Stats.onNonSeparator((char)primaryCode, x, y);
+            LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
         }
     }
 
@@ -2136,7 +2137,7 @@
             setPunctuationSuggestions();
         }
         if (mSettings.isInternal()) {
-            Utils.Stats.onSeparator((char)primaryCode, x, y);
+            LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
         }
 
         mKeyboardSwitcher.updateShiftState();
@@ -2325,7 +2326,8 @@
                         + "is empty? Impossible! I must commit suicide.");
             }
             if (mSettings.isInternal()) {
-                Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer);
+                LatinImeLoggerUtils.onAutoCorrection(
+                        typedWord, autoCorrection, separatorString, mWordComposer);
             }
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 final SuggestedWords suggestedWords = mSuggestedWords;
@@ -2429,7 +2431,7 @@
                         && !AutoCorrection.isValidWord(mSuggest, suggestion, true);
 
         if (mSettings.isInternal()) {
-            Stats.onSeparator((char)Constants.CODE_SPACE,
+            LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
@@ -2633,7 +2635,7 @@
         }
         mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
         if (mSettings.isInternal()) {
-            Stats.onSeparator(mLastComposedWord.mSeparatorString,
+            LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -2787,7 +2789,7 @@
         final CharSequence[] items = new CharSequence[] {
                 // TODO: Should use new string "Select active input modes".
                 getString(R.string.language_selection_title),
-                getString(Utils.getAcitivityTitleResId(this, SettingsActivity.class)),
+                getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
         };
         final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
             @Override
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index f52e564..8c41cf8 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -42,8 +42,8 @@
 import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
 import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
 import com.android.inputmethod.latin.utils.AdditionalFeaturesSettingUtils;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.FeedbackUtils;
-import com.android.inputmethod.latin.utils.Utils;
 import com.android.inputmethod.research.ResearchLogger;
 import com.android.inputmethodcommon.InputMethodSettingsFragment;
 
@@ -90,7 +90,7 @@
         final PreferenceScreen preferenceScreen = getPreferenceScreen();
         if (preferenceScreen != null) {
             preferenceScreen.setTitle(
-                    Utils.getAcitivityTitleResId(getActivity(), SettingsActivity.class));
+                    ApplicationUtils.getAcitivityTitleResId(getActivity(), SettingsActivity.class));
         }
 
         final Resources res = getResources();
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 5b47dda..22beaef 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -75,6 +75,21 @@
         return mSuggestedWordInfoList.get(index);
     }
 
+    public String getDebugString(final int pos) {
+        if (!LatinImeLogger.sDBG) {
+            return null;
+        }
+        final SuggestedWordInfo wordInfo = getInfo(pos);
+        if (wordInfo == null) {
+            return null;
+        }
+        final String debugString = wordInfo.getDebugString();
+        if (TextUtils.isEmpty(debugString)) {
+            return null;
+        }
+        return debugString;
+    }
+
     public boolean willAutoCorrect() {
         return mWillAutoCorrect;
     }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
index da58126..999ca77 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -21,7 +21,7 @@
 import android.preference.PreferenceScreen;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.Utils;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
 
 /**
  * Preference screen.
@@ -39,7 +39,7 @@
         addPreferencesFromResource(R.xml.spell_checker_settings);
         final PreferenceScreen preferenceScreen = getPreferenceScreen();
         if (preferenceScreen != null) {
-            preferenceScreen.setTitle(Utils.getAcitivityTitleResId(
+            preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId(
                     getActivity(), SpellCheckerSettingsActivity.class));
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index 2218b3b..ff24e2d 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -31,7 +31,6 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.utils.Utils;
 
 public final class MoreSuggestions extends Keyboard {
     public static final int SUGGESTION_CODE_BASE = 1024;
@@ -207,7 +206,7 @@
                 final int y = params.getY(index);
                 final int width = params.getWidth(index);
                 final String word = mSuggestedWords.getWord(index);
-                final String info = Utils.getDebugInfo(mSuggestedWords, index);
+                final String info = mSuggestedWords.getDebugString(index);
                 final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE;
                 final Key key = new Key(
                         params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions,
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 9565f63..99372c3 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -51,7 +51,6 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.utils.ResourceUtils;
-import com.android.inputmethod.latin.utils.Utils;
 
 import java.util.ArrayList;
 
@@ -446,7 +445,7 @@
             wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords));
             if (SuggestionStripView.DBG) {
                 mDebugInfoViews.get(positionInStrip).setText(
-                        Utils.getDebugInfo(suggestedWords, indexInSuggestedWords));
+                        suggestedWords.getDebugString(indexInSuggestedWords));
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
new file mode 100644
index 0000000..08a2a8c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+public final class ApplicationUtils {
+    private static final String TAG = ApplicationUtils.class.getSimpleName();
+
+    private ApplicationUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static int getAcitivityTitleResId(final Context context,
+            final Class<? extends Activity> cls) {
+        final ComponentName cn = new ComponentName(context, cls);
+        try {
+            final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
+            if (ai != null) {
+                return ai.labelRes;
+            }
+        } catch (final NameNotFoundException e) {
+            Log.e(TAG, "Failed to get settings activity title res id.", e);
+        }
+        return 0;
+    }
+
+    /**
+     * A utility method to get the application's PackageInfo.versionName
+     * @return the application's PackageInfo.versionName
+     */
+    public static String getVersionName(final Context context) {
+        try {
+            if (context == null) {
+                return "";
+            }
+            final String packageName = context.getPackageName();
+            final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+            return info.versionName;
+        } catch (final NameNotFoundException e) {
+            Log.e(TAG, "Could not find version info.", e);
+        }
+        return "";
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
new file mode 100644
index 0000000..e958a7e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
@@ -0,0 +1,77 @@
+/*
+ * 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.text.TextUtils;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.WordComposer;
+
+public final class LatinImeLoggerUtils {
+    private LatinImeLoggerUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static void onNonSeparator(final char code, final int x, final int y) {
+        UserLogRingCharBuffer.getInstance().push(code, x, y);
+        LatinImeLogger.logOnInputChar();
+    }
+
+    public static void onSeparator(final int code, final int x, final int y) {
+        // Helper method to log a single code point separator
+        // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils
+        onSeparator(new String(new int[]{code}, 0, 1), x, y);
+    }
+
+    public static void onSeparator(final String separator, final int x, final int y) {
+        final int length = separator.length();
+        for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
+            int codePoint = Character.codePointAt(separator, i);
+            // TODO: accept code points
+            UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y);
+        }
+        LatinImeLogger.logOnInputSeparator();
+    }
+
+    public static void onAutoCorrection(final String typedWord, final String correctedWord,
+            final String separatorString, final WordComposer wordComposer) {
+        final boolean isBatchMode = wordComposer.isBatchMode();
+        if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
+            return;
+        }
+        // TODO: this fails when the separator is more than 1 code point long, but
+        // the backend can't handle it yet. The only case when this happens is with
+        // smileys and other multi-character keys.
+        final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
+                : separatorString.codePointAt(0);
+        if (!isBatchMode) {
+            LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
+        } else {
+            if (!TextUtils.isEmpty(correctedWord)) {
+                // We must make sure that InputPointer contains only the relative timestamps,
+                // not actual timestamps.
+                LatinImeLogger.logOnAutoCorrectionForGeometric(
+                        "", correctedWord, codePoint, wordComposer.getInputPointers());
+            }
+        }
+    }
+
+    public static void onAutoCorrectionCancellation() {
+        LatinImeLogger.logOnAutoCorrectionCancelled();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
new file mode 100644
index 0000000..ef9cacf
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 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.content.Intent;
+import android.content.pm.PackageManager;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.channels.FileChannel;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public final class UsabilityStudyLogUtils {
+    // TODO: remove code duplication with ResearchLog class
+    private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
+    private static final String FILENAME = "log.txt";
+    private final Handler mLoggingHandler;
+    private File mFile;
+    private File mDirectory;
+    private InputMethodService mIms;
+    private PrintWriter mWriter;
+    private final Date mDate;
+    private final SimpleDateFormat mDateFormat;
+
+    private UsabilityStudyLogUtils() {
+        mDate = new Date();
+        mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
+
+        HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+        mLoggingHandler = new Handler(handlerThread.getLooper());
+    }
+
+    // Initialization-on-demand holder
+    private static final class OnDemandInitializationHolder {
+        public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
+    }
+
+    public static UsabilityStudyLogUtils getInstance() {
+        return OnDemandInitializationHolder.sInstance;
+    }
+
+    public void init(final InputMethodService ims) {
+        mIms = ims;
+        mDirectory = ims.getFilesDir();
+    }
+
+    private void createLogFileIfNotExist() {
+        if ((mFile == null || !mFile.exists())
+                && (mDirectory != null && mDirectory.exists())) {
+            try {
+                mWriter = getPrintWriter(mDirectory, FILENAME, false);
+            } catch (final IOException e) {
+                Log.e(USABILITY_TAG, "Can't create log file.");
+            }
+        }
+    }
+
+    public static void writeBackSpace(final int x, final int y) {
+        UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
+    }
+
+    public static void writeChar(final char c, final int x, final int y) {
+        String inputChar = String.valueOf(c);
+        switch (c) {
+            case '\n':
+                inputChar = "<enter>";
+                break;
+            case '\t':
+                inputChar = "<tab>";
+                break;
+            case ' ':
+                inputChar = "<space>";
+                break;
+        }
+        UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
+        LatinImeLogger.onPrintAllUsabilityStudyLogs();
+    }
+
+    public void write(final String log) {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                createLogFileIfNotExist();
+                final long currentTime = System.currentTimeMillis();
+                mDate.setTime(currentTime);
+
+                final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
+                        mDateFormat.format(mDate), currentTime, log);
+                if (LatinImeLogger.sDBG) {
+                    Log.d(USABILITY_TAG, "Write: " + log);
+                }
+                mWriter.print(printString);
+            }
+        });
+    }
+
+    private synchronized String getBufferedLogs() {
+        mWriter.flush();
+        final StringBuilder sb = new StringBuilder();
+        final BufferedReader br = getBufferedReader();
+        String line;
+        try {
+            while ((line = br.readLine()) != null) {
+                sb.append('\n');
+                sb.append(line);
+            }
+        } catch (final IOException e) {
+            Log.e(USABILITY_TAG, "Can't read log file.");
+        } finally {
+            if (LatinImeLogger.sDBG) {
+                Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
+            }
+            try {
+                br.close();
+            } catch (final IOException e) {
+                // ignore.
+            }
+        }
+        return sb.toString();
+    }
+
+    public void emailResearcherLogsAll() {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                final Date date = new Date();
+                date.setTime(System.currentTimeMillis());
+                final String currentDateTimeString =
+                        new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
+                if (mFile == null) {
+                    Log.w(USABILITY_TAG, "No internal log file found.");
+                    return;
+                }
+                if (mIms.checkCallingOrSelfPermission(
+                            android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                                    != PackageManager.PERMISSION_GRANTED) {
+                    Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
+                    return;
+                }
+                mWriter.flush();
+                final String destPath = Environment.getExternalStorageDirectory()
+                        + "/research-" + currentDateTimeString + ".log";
+                final File destFile = new File(destPath);
+                try {
+                    final FileInputStream srcStream = new FileInputStream(mFile);
+                    final FileOutputStream destStream = new FileOutputStream(destFile);
+                    final FileChannel src = srcStream.getChannel();
+                    final FileChannel dest = destStream.getChannel();
+                    src.transferTo(0, src.size(), dest);
+                    src.close();
+                    srcStream.close();
+                    dest.close();
+                    destStream.close();
+                } catch (final FileNotFoundException e1) {
+                    Log.w(USABILITY_TAG, e1);
+                    return;
+                } catch (final IOException e2) {
+                    Log.w(USABILITY_TAG, e2);
+                    return;
+                }
+                if (destFile == null || !destFile.exists()) {
+                    Log.w(USABILITY_TAG, "Dest file doesn't exist.");
+                    return;
+                }
+                final Intent intent = new Intent(Intent.ACTION_SEND);
+                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                if (LatinImeLogger.sDBG) {
+                    Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
+                }
+                intent.setType("text/plain");
+                intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
+                intent.putExtra(Intent.EXTRA_SUBJECT,
+                        "[Research Logs] " + currentDateTimeString);
+                mIms.startActivity(intent);
+            }
+        });
+    }
+
+    public void printAll() {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
+            }
+        });
+    }
+
+    public void clearAll() {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mFile != null && mFile.exists()) {
+                    if (LatinImeLogger.sDBG) {
+                        Log.d(USABILITY_TAG, "Delete log file.");
+                    }
+                    mFile.delete();
+                    mWriter.close();
+                }
+            }
+        });
+    }
+
+    private BufferedReader getBufferedReader() {
+        createLogFileIfNotExist();
+        try {
+            return new BufferedReader(new FileReader(mFile));
+        } catch (final FileNotFoundException e) {
+            return null;
+        }
+    }
+
+    private PrintWriter getPrintWriter(final File dir, final String filename,
+            final boolean renew) throws IOException {
+        mFile = new File(dir, filename);
+        if (mFile.exists()) {
+            if (renew) {
+                mFile.delete();
+            }
+        }
+        return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
index 3e67e82..a2c6c45 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
@@ -20,7 +20,6 @@
 
 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;
diff --git a/java/src/com/android/inputmethod/latin/utils/Utils.java b/java/src/com/android/inputmethod/latin/utils/Utils.java
deleted file mode 100644
index c4e18ed..0000000
--- a/java/src/com/android/inputmethod/latin/utils/Utils.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright (C) 2010 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.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.inputmethodservice.InputMethodService;
-import android.net.Uri;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.WordComposer;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.channels.FileChannel;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-// TODO: Come up with a more descriptive class name
-public final class Utils {
-    private static final String TAG = Utils.class.getSimpleName();
-
-    private Utils() {
-        // This utility class is not publicly instantiable.
-    }
-
-    // TODO: Make this an external class
-    public static final class UsabilityStudyLogUtils {
-        // TODO: remove code duplication with ResearchLog class
-        private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
-        private static final String FILENAME = "log.txt";
-        private final Handler mLoggingHandler;
-        private File mFile;
-        private File mDirectory;
-        private InputMethodService mIms;
-        private PrintWriter mWriter;
-        private final Date mDate;
-        private final SimpleDateFormat mDateFormat;
-
-        private UsabilityStudyLogUtils() {
-            mDate = new Date();
-            mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
-
-            HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
-                    Process.THREAD_PRIORITY_BACKGROUND);
-            handlerThread.start();
-            mLoggingHandler = new Handler(handlerThread.getLooper());
-        }
-
-        // Initialization-on-demand holder
-        private static final class OnDemandInitializationHolder {
-            public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
-        }
-
-        public static UsabilityStudyLogUtils getInstance() {
-            return OnDemandInitializationHolder.sInstance;
-        }
-
-        public void init(final InputMethodService ims) {
-            mIms = ims;
-            mDirectory = ims.getFilesDir();
-        }
-
-        private void createLogFileIfNotExist() {
-            if ((mFile == null || !mFile.exists())
-                    && (mDirectory != null && mDirectory.exists())) {
-                try {
-                    mWriter = getPrintWriter(mDirectory, FILENAME, false);
-                } catch (final IOException e) {
-                    Log.e(USABILITY_TAG, "Can't create log file.");
-                }
-            }
-        }
-
-        public static void writeBackSpace(final int x, final int y) {
-            UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
-        }
-
-        public static void writeChar(final char c, final int x, final int y) {
-            String inputChar = String.valueOf(c);
-            switch (c) {
-                case '\n':
-                    inputChar = "<enter>";
-                    break;
-                case '\t':
-                    inputChar = "<tab>";
-                    break;
-                case ' ':
-                    inputChar = "<space>";
-                    break;
-            }
-            UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
-            LatinImeLogger.onPrintAllUsabilityStudyLogs();
-        }
-
-        public void write(final String log) {
-            mLoggingHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    createLogFileIfNotExist();
-                    final long currentTime = System.currentTimeMillis();
-                    mDate.setTime(currentTime);
-
-                    final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
-                            mDateFormat.format(mDate), currentTime, log);
-                    if (LatinImeLogger.sDBG) {
-                        Log.d(USABILITY_TAG, "Write: " + log);
-                    }
-                    mWriter.print(printString);
-                }
-            });
-        }
-
-        private synchronized String getBufferedLogs() {
-            mWriter.flush();
-            final StringBuilder sb = new StringBuilder();
-            final BufferedReader br = getBufferedReader();
-            String line;
-            try {
-                while ((line = br.readLine()) != null) {
-                    sb.append('\n');
-                    sb.append(line);
-                }
-            } catch (final IOException e) {
-                Log.e(USABILITY_TAG, "Can't read log file.");
-            } finally {
-                if (LatinImeLogger.sDBG) {
-                    Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
-                }
-                try {
-                    br.close();
-                } catch (final IOException e) {
-                    // ignore.
-                }
-            }
-            return sb.toString();
-        }
-
-        public void emailResearcherLogsAll() {
-            mLoggingHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    final Date date = new Date();
-                    date.setTime(System.currentTimeMillis());
-                    final String currentDateTimeString =
-                            new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
-                    if (mFile == null) {
-                        Log.w(USABILITY_TAG, "No internal log file found.");
-                        return;
-                    }
-                    if (mIms.checkCallingOrSelfPermission(
-                                android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
-                                        != PackageManager.PERMISSION_GRANTED) {
-                        Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
-                        return;
-                    }
-                    mWriter.flush();
-                    final String destPath = Environment.getExternalStorageDirectory()
-                            + "/research-" + currentDateTimeString + ".log";
-                    final File destFile = new File(destPath);
-                    try {
-                        final FileInputStream srcStream = new FileInputStream(mFile);
-                        final FileOutputStream destStream = new FileOutputStream(destFile);
-                        final FileChannel src = srcStream.getChannel();
-                        final FileChannel dest = destStream.getChannel();
-                        src.transferTo(0, src.size(), dest);
-                        src.close();
-                        srcStream.close();
-                        dest.close();
-                        destStream.close();
-                    } catch (final FileNotFoundException e1) {
-                        Log.w(USABILITY_TAG, e1);
-                        return;
-                    } catch (final IOException e2) {
-                        Log.w(USABILITY_TAG, e2);
-                        return;
-                    }
-                    if (destFile == null || !destFile.exists()) {
-                        Log.w(USABILITY_TAG, "Dest file doesn't exist.");
-                        return;
-                    }
-                    final Intent intent = new Intent(Intent.ACTION_SEND);
-                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                    if (LatinImeLogger.sDBG) {
-                        Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
-                    }
-                    intent.setType("text/plain");
-                    intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
-                    intent.putExtra(Intent.EXTRA_SUBJECT,
-                            "[Research Logs] " + currentDateTimeString);
-                    mIms.startActivity(intent);
-                }
-            });
-        }
-
-        public void printAll() {
-            mLoggingHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
-                }
-            });
-        }
-
-        public void clearAll() {
-            mLoggingHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    if (mFile != null && mFile.exists()) {
-                        if (LatinImeLogger.sDBG) {
-                            Log.d(USABILITY_TAG, "Delete log file.");
-                        }
-                        mFile.delete();
-                        mWriter.close();
-                    }
-                }
-            });
-        }
-
-        private BufferedReader getBufferedReader() {
-            createLogFileIfNotExist();
-            try {
-                return new BufferedReader(new FileReader(mFile));
-            } catch (final FileNotFoundException e) {
-                return null;
-            }
-        }
-
-        private PrintWriter getPrintWriter(final File dir, final String filename,
-                final boolean renew) throws IOException {
-            mFile = new File(dir, filename);
-            if (mFile.exists()) {
-                if (renew) {
-                    mFile.delete();
-                }
-            }
-            return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
-        }
-    }
-
-    // TODO: Make this an external class
-    public static final class Stats {
-        public static void onNonSeparator(final char code, final int x, final int y) {
-            UserLogRingCharBuffer.getInstance().push(code, x, y);
-            LatinImeLogger.logOnInputChar();
-        }
-
-        public static void onSeparator(final int code, final int x, final int y) {
-            // Helper method to log a single code point separator
-            // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils
-            onSeparator(new String(new int[]{code}, 0, 1), x, y);
-        }
-
-        public static void onSeparator(final String separator, final int x, final int y) {
-            final int length = separator.length();
-            for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
-                int codePoint = Character.codePointAt(separator, i);
-                // TODO: accept code points
-                UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y);
-            }
-            LatinImeLogger.logOnInputSeparator();
-        }
-
-        public static void onAutoCorrection(final String typedWord, final String correctedWord,
-                final String separatorString, final WordComposer wordComposer) {
-            final boolean isBatchMode = wordComposer.isBatchMode();
-            if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
-                return;
-            }
-            // TODO: this fails when the separator is more than 1 code point long, but
-            // the backend can't handle it yet. The only case when this happens is with
-            // smileys and other multi-character keys.
-            final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
-                    : separatorString.codePointAt(0);
-            if (!isBatchMode) {
-                LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
-            } else {
-                if (!TextUtils.isEmpty(correctedWord)) {
-                    // We must make sure that InputPointer contains only the relative timestamps,
-                    // not actual timestamps.
-                    LatinImeLogger.logOnAutoCorrectionForGeometric(
-                            "", correctedWord, codePoint, wordComposer.getInputPointers());
-                }
-            }
-        }
-
-        public static void onAutoCorrectionCancellation() {
-            LatinImeLogger.logOnAutoCorrectionCancelled();
-        }
-    }
-
-    public static String getDebugInfo(final SuggestedWords suggestions, final int pos) {
-        if (!LatinImeLogger.sDBG) {
-            return null;
-        }
-        final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
-        if (wordInfo == null) {
-            return null;
-        }
-        final String info = wordInfo.getDebugString();
-        if (TextUtils.isEmpty(info)) {
-            return null;
-        }
-        return info;
-    }
-
-    public static int getAcitivityTitleResId(final Context context,
-            final Class<? extends Activity> cls) {
-        final ComponentName cn = new ComponentName(context, cls);
-        try {
-            final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
-            if (ai != null) {
-                return ai.labelRes;
-            }
-        } catch (final NameNotFoundException e) {
-            Log.e(TAG, "Failed to get settings activity title res id.", e);
-        }
-        return 0;
-    }
-
-    /**
-     * A utility method to get the application's PackageInfo.versionName
-     * @return the application's PackageInfo.versionName
-     */
-    public static String getVersionName(final Context context) {
-        try {
-            if (context == null) {
-                return "";
-            }
-            final String packageName = context.getPackageName();
-            final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
-            return info.versionName;
-        } catch (final NameNotFoundException e) {
-            Log.e(TAG, "Could not find version info.", e);
-        }
-        return "";
-    }
-}