diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index f7c5fd50e802745aafd71d4ab33d010eaf0d5b42..080366e9bd92c19617db67a67fdc4592faae8350 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -118,6 +118,8 @@ public class ResearchLog {
                 } catch (Exception e) {
                     Log.d(TAG, "error when closing ResearchLog:", e);
                 } finally {
+                    // Marking the file as read-only signals that this log file is ready to be
+                    // uploaded.
                     if (mFile != null && mFile.exists()) {
                         mFile.setWritable(false, false);
                     }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogDirectory.java b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java
new file mode 100644
index 0000000000000000000000000000000000000000..291dea5d0a473b943713b0e1a779b109685c6e52
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchLogDirectory.java
@@ -0,0 +1,111 @@
+/*
+ * 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.research;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileFilter;
+
+/**
+ * Manages log files.
+ *
+ * This class handles all aspects where and how research log data is stored.  This includes
+ * generating log filenames in the correct place with the correct names, and cleaning up log files
+ * under this directory.
+ */
+public class ResearchLogDirectory {
+    public static final String TAG = ResearchLogDirectory.class.getSimpleName();
+    /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
+    private static final String FILENAME_SUFFIX = ".txt";
+    private static final String USER_RECORDING_FILENAME_PREFIX = "recording";
+
+    private static final ReadOnlyLogFileFilter sUploadableLogFileFilter =
+            new ReadOnlyLogFileFilter();
+
+    private final File mFilesDir;
+
+    static class ReadOnlyLogFileFilter implements FileFilter {
+        @Override
+        public boolean accept(final File pathname) {
+            return pathname.getName().startsWith(ResearchLogDirectory.LOG_FILENAME_PREFIX)
+                    && !pathname.canWrite();
+        }
+    }
+
+    /**
+     * Creates a new ResearchLogDirectory, creating the storage directory if it does not exist.
+     */
+    public ResearchLogDirectory(final Context context) {
+        mFilesDir = getLoggingDirectory(context);
+        if (mFilesDir == null) {
+            throw new NullPointerException("No files directory specified");
+        }
+        if (!mFilesDir.exists()) {
+            mFilesDir.mkdirs();
+        }
+    }
+
+    private File getLoggingDirectory(final Context context) {
+        // TODO: Switch to using a subdirectory of getFilesDir().
+        return context.getFilesDir();
+    }
+
+    /**
+     * Get an array of log files that are ready for uploading.
+     *
+     * A file is ready for uploading if it is marked as read-only.
+     *
+     * @return the array of uploadable files
+     */
+    public File[] getUploadableLogFiles() {
+        try {
+            return mFilesDir.listFiles(sUploadableLogFileFilter);
+        } catch (final SecurityException e) {
+            Log.e(TAG, "Could not cleanup log directory, permission denied", e);
+            return new File[0];
+        }
+    }
+
+    public void cleanupLogFilesOlderThan(final long time) {
+        try {
+            for (final File file : mFilesDir.listFiles()) {
+                final String filename = file.getName();
+                if ((filename.startsWith(LOG_FILENAME_PREFIX)
+                        || filename.startsWith(USER_RECORDING_FILENAME_PREFIX))
+                        && (file.lastModified() < time)) {
+                    file.delete();
+                }
+            }
+        } catch (final SecurityException e) {
+            Log.e(TAG, "Could not cleanup log directory, permission denied", e);
+        }
+    }
+
+    public File getLogFilePath(final long time) {
+        return new File(mFilesDir, getUniqueFilename(LOG_FILENAME_PREFIX, time));
+    }
+
+    public File getUserRecordingFilePath(final long time) {
+        return new File(mFilesDir, getUniqueFilename(USER_RECORDING_FILENAME_PREFIX, time));
+    }
+
+    private static String getUniqueFilename(final String prefix, final long time) {
+        return prefix + "-" + time + FILENAME_SUFFIX;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 11d1a5222231976c95550c79f95ebafd96ad32e2..e932a2d91f242eee2f4df4d028daae96af2aa0dd 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -125,12 +125,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
     /* package */ static boolean sIsLogging = false;
     private static final int OUTPUT_FORMAT_VERSION = 5;
     private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
-    /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
-    private static final String LOG_FILENAME_SUFFIX = ".txt";
-    /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording";
-    private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt";
-    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
-            new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
     // Whether all words should be recorded, leaving unsampled word between bigrams.  Useful for
     // testing.
     /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
@@ -156,12 +150,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
 
     private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000;
     private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000;
+    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
+    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
 
     private static final ResearchLogger sInstance = new ResearchLogger();
     private static String sAccountType = null;
     private static String sAllowedAccountDomain = null;
-    // to write to a different filename, e.g., for testing, set mFile before calling start()
-    /* package */ File mFilesDir;
     /* package */ ResearchLog mMainResearchLog;
     // mFeedbackLog records all events for the session, private or not (excepting
     // passwords).  It is written to permanent storage only if the user explicitly commands
@@ -187,9 +181,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
             Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
     // U+E001 is in the "private-use area"
     /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
-    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
-    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
-    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
     protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
     // set when LatinIME should ignore an onUpdateSelection() callback that
     // arises from operations in this class
@@ -203,6 +194,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
     private final Statistics mStatistics;
     private final MotionEventReader mMotionEventReader = new MotionEventReader();
     private final Replayer mReplayer = Replayer.getInstance();
+    private ResearchLogDirectory mResearchLogDirectory;
 
     private Intent mUploadIntent;
     private Intent mUploadNowIntent;
@@ -240,11 +232,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
             final Suggest suggest) {
         assert latinIME != null;
         mLatinIME = latinIME;
-        mFilesDir = latinIME.getFilesDir();
-        if (mFilesDir == null || !mFilesDir.exists()) {
-            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
-            return;
-        }
         mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
         mPrefs.registerOnSharedPreferenceChangeListener(this);
 
@@ -256,15 +243,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
         sAccountType = res.getString(R.string.research_account_type);
         sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);
 
-        // Cleanup logging directory
-        // TODO: Move this and other file-related components to separate file.
-        final long lastCleanupTime = mPrefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
-        final long now = System.currentTimeMillis();
-        if (now - lastCleanupTime > DURATION_BETWEEN_DIR_CLEANUP_IN_MS) {
-            final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
-            cleanupLoggingDir(mFilesDir, timeHorizon);
-            mPrefs.edit().putLong(PREF_LAST_CLEANUP_TIME, now).apply();
-        }
+        // Initialize directory manager
+        mResearchLogDirectory = new ResearchLogDirectory(mLatinIME);
+        cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis());
 
         // Initialize external services
         mUploadIntent = new Intent(mLatinIME, UploaderService.class);
@@ -276,6 +257,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
         mReplayer.setKeyboardSwitcher(keyboardSwitcher);
     }
 
+    private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory,
+            final long now) {
+        final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs);
+        if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return;
+        final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS;
+        mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime);
+        ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now);
+    }
+
     /**
      * Arrange for the UploaderService to be run on a regular basis.
      *
@@ -295,17 +285,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
                 UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
     }
 
-    private void cleanupLoggingDir(final File dir, final long time) {
-        for (File file : dir.listFiles()) {
-            final String filename = file.getName();
-            if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
-                    || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
-                    && file.lastModified() < time) {
-                file.delete();
-            }
-        }
-    }
-
     public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
         mMainKeyboardView = mainKeyboardView;
         maybeShowSplashScreen();
@@ -387,35 +366,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
         restart();
     }
 
-    private static int sLogFileCounter = 0;
-
-    private File createLogFile(final File filesDir) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(LOG_FILENAME_PREFIX).append('-');
-        final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs);
-        sb.append(uuid).append('-');
-        sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
-        // Sometimes logFiles are created within milliseconds of each other.  Append a counter to
-        // separate these.
-        if (sLogFileCounter < Integer.MAX_VALUE) {
-            sLogFileCounter++;
-        } else {
-            // Wrap the counter, in the unlikely event of overflow.
-            sLogFileCounter = 0;
-        }
-        sb.append(sLogFileCounter);
-        sb.append(LOG_FILENAME_SUFFIX);
-        return new File(filesDir, sb.toString());
-    }
-
-    private File createUserRecordingFile(final File filesDir) {
-        final StringBuilder sb = new StringBuilder();
-        sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
-        final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs);
-        sb.append(uuid).append('-');
-        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
-        sb.append(USER_RECORDING_FILENAME_SUFFIX);
-        return new File(filesDir, sb.toString());
+    private void setLoggingAllowed(final boolean enableLogging) {
+        if (mPrefs == null) return;
+        sIsLogging = enableLogging;
+        ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging);
     }
 
     private void checkForEmptyEditor() {
@@ -455,7 +409,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
             return;
         }
         if (mMainLogBuffer == null) {
-            mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
+            mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+                    System.currentTimeMillis()), mLatinIME);
             final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
             mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
                     mSuggest) {
@@ -488,7 +443,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
     }
 
     private void resetFeedbackLogging() {
-        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
+        mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+                System.currentTimeMillis()), mLatinIME);
         mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
     }
 
@@ -612,7 +568,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
         if (mUserRecordingLog != null) {
             mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
         }
-        mUserRecordingFile = createUserRecordingFile(mFilesDir);
+        mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath(
+                System.currentTimeMillis());
         mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
         mUserRecordingLogBuffer = new LogBuffer();
         resetRecordingTimer();
diff --git a/java/src/com/android/inputmethod/research/ResearchSettings.java b/java/src/com/android/inputmethod/research/ResearchSettings.java
index 11e9ac77a43e132380b60f6d446bcd21df7602ba..c0bc03fdec00a97cf7bd0977194dbb18f6037d13 100644
--- a/java/src/com/android/inputmethod/research/ResearchSettings.java
+++ b/java/src/com/android/inputmethod/research/ResearchSettings.java
@@ -26,6 +26,8 @@ public final class ResearchSettings {
             "pref_research_logger_enabled_flag";
     public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH =
             "pref_research_logger_has_seen_splash";
+    public static final String PREF_RESEARCH_LAST_DIR_CLEANUP_TIME =
+            "pref_research_last_dir_cleanup_time";
 
     private ResearchSettings() {
         // Intentional empty constructor for singleton.
@@ -58,4 +60,13 @@ public final class ResearchSettings {
             final boolean hasSeenSplash) {
         prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply();
     }
+
+    public static long readResearchLastDirCleanupTime(final SharedPreferences prefs) {
+        return prefs.getLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, 0L);
+    }
+
+    public static void writeResearchLastDirCleanupTime(final SharedPreferences prefs,
+            final long lastDirCleanupTime) {
+        prefs.edit().putLong(PREF_RESEARCH_LAST_DIR_CLEANUP_TIME, lastDirCleanupTime).apply();
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java
index df495a88dc6a22a79ac7a12aa85b02cd30daae79..152b94d30e204096afa3ad86e1b7d603fb276cb8 100644
--- a/java/src/com/android/inputmethod/research/Uploader.java
+++ b/java/src/com/android/inputmethod/research/Uploader.java
@@ -17,7 +17,6 @@
 package com.android.inputmethod.research;
 
 import android.Manifest;
-import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -33,7 +32,6 @@ import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileFilter;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -55,12 +53,12 @@ public final class Uploader {
     private static final int BUF_SIZE = 1024 * 8;
 
     private final Context mContext;
-    private final File mFilesDir;
+    private final ResearchLogDirectory mResearchLogDirectory;
     private final URL mUrl;
 
     public Uploader(final Context context) {
         mContext = context;
-        mFilesDir = context.getFilesDir();
+        mResearchLogDirectory = new ResearchLogDirectory(context);
 
         final String urlString = context.getString(R.string.research_logger_upload_url);
         if (TextUtils.isEmpty(urlString)) {
@@ -106,16 +104,8 @@ public final class Uploader {
     }
 
     public void doUpload() {
-        if (mFilesDir == null) {
-            return;
-        }
-        final File[] files = mFilesDir.listFiles(new FileFilter() {
-            @Override
-            public boolean accept(final File pathname) {
-                return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
-                        && !pathname.canWrite();
-            }
-        });
+        final File[] files = mResearchLogDirectory.getUploadableLogFiles();
+        if (files == null) return;
         for (final File file : files) {
             uploadFile(file);
         }