am 2192d08b: Remove unused scale parameter from getPointToKeyLength()

* commit '2192d08b9cca6a40d834d6a5001d19b5845ed8a0':
  Remove unused scale parameter from getPointToKeyLength()
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index 7df266e..c2aade6 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -57,7 +57,7 @@
         if (usabilityStudyPref instanceof CheckBoxPreference) {
             final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
             checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
-                    ResearchLogger.DEFAULT_USABILITY_STUDY_MODE));
+                    LatinImeLogger.getUsabilityStudyMode(prefs)));
             checkbox.setSummary(R.string.settings_warning_researcher_mode);
         }
         final Preference statisticsLoggingPref = findPreference(PREF_STATISTICS_LOGGING);
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index e934746..af494c4 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -428,7 +428,7 @@
         initSuggest();
 
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
+            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
         }
         mDisplayOrientation = getResources().getConfiguration().orientation;
 
@@ -563,6 +563,9 @@
         }
         mSettings.onDestroy();
         unregisterReceiver(mReceiver);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().onDestroy();
+        }
         // TODO: The experimental version is not supported by the Dictionary Pack Service yet.
         if (!ProductionFlag.IS_EXPERIMENTAL) {
             unregisterReceiver(mDictionaryPackInstallReceiver);
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index e4e8b94..3f2b0a3 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -37,6 +37,10 @@
     public static void commit() {
     }
 
+    public static boolean getUsabilityStudyMode(final SharedPreferences prefs) {
+        return false;
+    }
+
     public static void onDestroy() {
     }
 
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index acfcd53..7a604dc 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -28,6 +28,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 
 import java.io.BufferedReader;
@@ -77,6 +78,7 @@
         private RingCharBuffer() {
             // Intentional empty constructor for singleton.
         }
+        @UsedForTesting
         public static RingCharBuffer getInstance() {
             return sRingCharBuffer;
         }
@@ -93,6 +95,7 @@
             return ret < 0 ? ret + BUFSIZE : ret;
         }
         // TODO: accept code points
+        @UsedForTesting
         public void push(char c, int x, int y) {
             if (!mEnabled) return;
             mCharBuf[mEnd] = c;
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index e91976a..839e2b7 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -16,7 +16,6 @@
 
 package com.android.inputmethod.research;
 
-import android.content.SharedPreferences;
 import android.os.SystemClock;
 import android.text.TextUtils;
 import android.util.JsonWriter;
@@ -45,7 +44,7 @@
  * will not violate the user's privacy.  Checks for this may include whether other LogUnits have
  * been published recently, or whether the LogUnit contains numbers, etc.
  */
-/* package */ class LogUnit {
+public class LogUnit {
     private static final String TAG = LogUnit.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
 
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 45b83dd..9aa60f8 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -18,6 +18,7 @@
 
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.define.ProductionFlag;
@@ -64,7 +65,11 @@
     // The size of the n-grams logged.  E.g. N_GRAM_SIZE = 2 means to sample bigrams.
     public static final int N_GRAM_SIZE = 2;
 
-    private Suggest mSuggest;
+    // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate
+    // method.
+    private final Suggest mSuggest;
+    @UsedForTesting
+    private Dictionary mDictionaryForTesting;
     private boolean mIsStopping = false;
 
     /* package for test */ int mNumWordsBetweenNGrams;
@@ -73,17 +78,23 @@
     // after a sample is taken.
     /* package for test */ int mNumWordsUntilSafeToSample;
 
-    public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore) {
+    public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore,
+            final Suggest suggest) {
         super(N_GRAM_SIZE + wordsBetweenSamples);
         mNumWordsBetweenNGrams = wordsBetweenSamples;
         mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
-    }
-
-    public void setSuggest(final Suggest suggest) {
         mSuggest = suggest;
     }
 
+    @UsedForTesting
+    /* package for test */ void setDictionaryForTesting(final Dictionary dictionary) {
+        mDictionaryForTesting = dictionary;
+    }
+
     private Dictionary getDictionary() {
+        if (mDictionaryForTesting != null) {
+            return mDictionaryForTesting;
+        }
         if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
         return mSuggest.getMainDictionary();
     }
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 99d8493..9016e23 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -38,12 +38,19 @@
 /**
  * Logs the use of the LatinIME keyboard.
  *
- * This class logs operations on the IME keyboard, including what the user has typed.
- * Data is stored locally in a file in app-specific storage.
+ * This class logs operations on the IME keyboard, including what the user has typed.  Data is
+ * written to a {@link JsonWriter}, which will write to a local file.
+ *
+ * The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}.
+ *
+ * This class uses an executor to perform file-writing operations on a separate thread.  It also
+ * tries to avoid creating unnecessary files if there is nothing to write.  It also handles
+ * flushing, making sure it happens, but not too frequently.
  *
  * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
  */
 public class ResearchLog {
+    // TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it.
     private static final String TAG = ResearchLog.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
     private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
@@ -87,6 +94,12 @@
         mContext = context;
     }
 
+    /**
+     * Waits for any publication requests to finish and closes the {@link JsonWriter} used for
+     * output.
+     *
+     * See class comment for details about {@code JsonWriter} construction.
+     */
     public synchronized void close(final Runnable onClosed) {
         mExecutor.submit(new Callable<Object>() {
             @Override
@@ -94,20 +107,15 @@
                 try {
                     if (mHasWrittenData) {
                         mJsonWriter.endArray();
-                        mJsonWriter.flush();
-                        mJsonWriter.close();
-                        if (DEBUG) {
-                            Log.d(TAG, "wrote log to " + mFile);
-                        }
                         mHasWrittenData = false;
-                    } else {
-                        if (DEBUG) {
-                            Log.d(TAG, "close() called, but no data, not outputting");
-                        }
+                    }
+                    mJsonWriter.flush();
+                    mJsonWriter.close();
+                    if (DEBUG) {
+                        Log.d(TAG, "wrote log to " + mFile);
                     }
                 } catch (Exception e) {
-                    Log.d(TAG, "error when closing ResearchLog:");
-                    e.printStackTrace();
+                    Log.d(TAG, "error when closing ResearchLog:", e);
                 } finally {
                     if (mFile != null && mFile.exists()) {
                         mFile.setWritable(false, false);
@@ -125,6 +133,12 @@
 
     private boolean mIsAbortSuccessful;
 
+    /**
+     * Waits for publication requests to finish, closes the {@link JsonWriter}, but then deletes the
+     * backing file used for output.
+     *
+     * See class comment for details about {@code JsonWriter} construction.
+     */
     public synchronized void abort() {
         mExecutor.submit(new Callable<Object>() {
             @Override
@@ -184,6 +198,12 @@
         mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
     }
 
+    /**
+     * Queues up {@code logUnit} to be published in the background.
+     *
+     * @param logUnit the {@link LogUnit} to be published
+     * @param canIncludePrivateData whether private data in the LogUnit should be included
+     */
     public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) {
         try {
             mExecutor.submit(new Callable<Object>() {
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 25633d63..e705ddd 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -122,11 +122,9 @@
     // field holds a channel name, the developer does not have to re-enter it when using the
     // feedback mechanism to generate multiple tests.
     private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
-    public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
     /* package */ static boolean sIsLogging = false;
     private static final int OUTPUT_FORMAT_VERSION = 5;
     private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
-    private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
     /* 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";
@@ -154,7 +152,6 @@
     // constants related to specific log points
     private static final String WHITESPACE_SEPARATORS = " \t\n\r";
     private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
-    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
     private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
 
     private static final ResearchLogger sInstance = new ResearchLogger();
@@ -162,7 +159,6 @@
     private static String sAllowedAccountDomain = null;
     // to write to a different filename, e.g., for testing, set mFile before calling start()
     /* package */ File mFilesDir;
-    /* package */ String mUUIDString;
     /* 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
@@ -208,7 +204,7 @@
     private Intent mUploadIntent;
     private Intent mUploadNowIntent;
 
-    private LogUnit mCurrentLogUnit = new LogUnit();
+    /* package for test */ LogUnit mCurrentLogUnit = new LogUnit();
 
     // Gestured or tapped words may be committed after the gesture of the next word has started.
     // To ensure that the gesture data of the next word is not associated with the previous word,
@@ -237,50 +233,44 @@
         return sInstance;
     }
 
-    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
+    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher,
+            final Suggest suggest) {
         assert latinIME != null;
-        if (latinIME == null) {
-            Log.w(TAG, "IMS is null; logging is off");
-        } else {
-            mFilesDir = latinIME.getFilesDir();
-            if (mFilesDir == null || !mFilesDir.exists()) {
-                Log.w(TAG, "IME storage directory does not exist.");
-            }
+        mLatinIME = latinIME;
+        mFilesDir = latinIME.getFilesDir();
+        if (mFilesDir == null || !mFilesDir.exists()) {
+            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
+            return;
         }
-        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
-        if (prefs != null) {
-            mUUIDString = getUUID(prefs);
-            if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
-                Editor e = prefs.edit();
-                e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
-                e.apply();
-            }
-            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-            prefs.registerOnSharedPreferenceChangeListener(this);
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
+        mPrefs.registerOnSharedPreferenceChangeListener(this);
 
-            final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
-            final long now = System.currentTimeMillis();
-            if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
-                final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
-                cleanupLoggingDir(mFilesDir, timeHorizon);
-                Editor e = prefs.edit();
-                e.putLong(PREF_LAST_CLEANUP_TIME, now);
-                e.apply();
-            }
-        }
+        // Initialize fields from preferences
+        sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs);
+
+        // Initialize fields from resources
         final Resources res = latinIME.getResources();
         sAccountType = res.getString(R.string.research_account_type);
         sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);
-        mLatinIME = latinIME;
-        mPrefs = prefs;
+
+        // 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 external services
         mUploadIntent = new Intent(mLatinIME, UploaderService.class);
         mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
         mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
-        mReplayer.setKeyboardSwitcher(keyboardSwitcher);
-
         if (ProductionFlag.IS_EXPERIMENTAL) {
             scheduleUploadingService(mLatinIME);
         }
+        mReplayer.setKeyboardSwitcher(keyboardSwitcher);
     }
 
     /**
@@ -322,14 +312,16 @@
         mMainKeyboardView = null;
     }
 
-    private boolean hasSeenSplash() {
-        return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
+    public void onDestroy() {
+        if (mPrefs != null) {
+            mPrefs.unregisterOnSharedPreferenceChangeListener(this);
+        }
     }
 
     private Dialog mSplashDialog = null;
 
     private void maybeShowSplashScreen() {
-        if (hasSeenSplash()) {
+        if (ResearchSettings.readHasSeenSplash(mPrefs)) {
             return;
         }
         if (mSplashDialog != null && mSplashDialog.isShowing()) {
@@ -382,32 +374,23 @@
     }
 
     public void onUserLoggingConsent() {
-        setLoggingAllowed(true);
         if (mPrefs == null) {
-            return;
+            mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
+            if (mPrefs == null) return;
         }
-        final Editor e = mPrefs.edit();
-        e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
-        e.apply();
+        sIsLogging = true;
+        ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true);
+        ResearchSettings.writeHasSeenSplash(mPrefs, true);
         restart();
     }
 
-    private void setLoggingAllowed(boolean enableLogging) {
-        if (mPrefs == null) {
-            return;
-        }
-        Editor e = mPrefs.edit();
-        e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
-        e.apply();
-        sIsLogging = enableLogging;
-    }
-
     private static int sLogFileCounter = 0;
 
     private File createLogFile(final File filesDir) {
         final StringBuilder sb = new StringBuilder();
         sb.append(LOG_FILENAME_PREFIX).append('-');
-        sb.append(mUUIDString).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.
@@ -425,7 +408,8 @@
     private File createUserRecordingFile(final File filesDir) {
         final StringBuilder sb = new StringBuilder();
         sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
-        sb.append(mUUIDString).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());
@@ -467,14 +451,11 @@
             // Log.w(TAG, "not in usability mode; not logging");
             return;
         }
-        if (mFilesDir == null || !mFilesDir.exists()) {
-            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
-            return;
-        }
         if (mMainLogBuffer == null) {
             mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
             final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
-            mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore) {
+            mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
+                    mSuggest) {
                 @Override
                 protected void publish(final ArrayList<LogUnit> logUnits,
                         boolean canIncludePrivateData) {
@@ -497,7 +478,6 @@
                     }
                 }
             };
-            mMainLogBuffer.setSuggest(mSuggest);
         }
         if (mFeedbackLogBuffer == null) {
             resetFeedbackLogging();
@@ -574,7 +554,7 @@
     }
 
     @Override
-    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
         if (key == null || prefs == null) {
             return;
         }
@@ -596,7 +576,7 @@
         presentFeedbackDialog(latinIME);
     }
 
-    public void presentFeedbackDialog(LatinIME latinIME) {
+    public void presentFeedbackDialog(final LatinIME latinIME) {
         if (isMakingUserRecording()) {
             saveRecording();
         }
@@ -828,9 +808,7 @@
             if (mPrefs == null) {
                 return;
             }
-            final Editor e = mPrefs.edit();
-            e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName);
-            e.apply();
+            mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply();
         }
     }
 
@@ -845,10 +823,13 @@
         mInFeedbackDialog = false;
     }
 
-    public void initSuggest(Suggest suggest) {
+    public void initSuggest(final Suggest suggest) {
         mSuggest = suggest;
+        // MainLogBuffer has out-of-date Suggest object.  Need to close it down and create a new
+        // one.
         if (mMainLogBuffer != null) {
-            mMainLogBuffer.setSuggest(mSuggest);
+            stop();
+            start();
         }
     }
 
@@ -1137,18 +1118,6 @@
         }
     }
 
-    private static String getUUID(final SharedPreferences prefs) {
-        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
-        if (null == uuidString) {
-            UUID uuid = UUID.randomUUID();
-            uuidString = uuid.toString();
-            Editor editor = prefs.edit();
-            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
-            editor.apply();
-        }
-        return uuidString;
-    }
-
     private String scrubWord(String word) {
         final Dictionary dictionary = getDictionary();
         if (dictionary == null) {
@@ -1195,9 +1164,9 @@
                         0);
                 final Integer versionCode = packageInfo.versionCode;
                 final String versionName = packageInfo.versionName;
+                final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs);
                 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
-                        researchLogger.mUUIDString, editorInfo.packageName,
-                        Integer.toHexString(editorInfo.inputType),
+                        uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType),
                         Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
                         Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
                         OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
diff --git a/java/src/com/android/inputmethod/research/ResearchSettings.java b/java/src/com/android/inputmethod/research/ResearchSettings.java
new file mode 100644
index 0000000..11e9ac7
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchSettings.java
@@ -0,0 +1,61 @@
+/*
+ * 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.SharedPreferences;
+
+import java.util.UUID;
+
+public final class ResearchSettings {
+    public static final String PREF_RESEARCH_LOGGER_UUID = "pref_research_logger_uuid";
+    public static final String PREF_RESEARCH_LOGGER_ENABLED_FLAG =
+            "pref_research_logger_enabled_flag";
+    public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH =
+            "pref_research_logger_has_seen_splash";
+
+    private ResearchSettings() {
+        // Intentional empty constructor for singleton.
+    }
+
+    public static String readResearchLoggerUuid(final SharedPreferences prefs) {
+        if (prefs.contains(PREF_RESEARCH_LOGGER_UUID)) {
+            return prefs.getString(PREF_RESEARCH_LOGGER_UUID, null);
+        }
+        // Generate a random string as uuid if not yet set
+        final String newUuid = UUID.randomUUID().toString();
+        prefs.edit().putString(PREF_RESEARCH_LOGGER_UUID, newUuid).apply();
+        return newUuid;
+    }
+
+    public static boolean readResearchLoggerEnabledFlag(final SharedPreferences prefs) {
+        return prefs.getBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, false);
+    }
+
+    public static void writeResearchLoggerEnabledFlag(final SharedPreferences prefs,
+            final boolean isEnabled) {
+        prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, isEnabled).apply();
+    }
+
+    public static boolean readHasSeenSplash(final SharedPreferences prefs) {
+        return prefs.getBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, false);
+    }
+
+    public static void writeHasSeenSplash(final SharedPreferences prefs,
+            final boolean hasSeenSplash) {
+        prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply();
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 89c67fb..eb5bdef 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -56,7 +56,6 @@
     private static final int BUF_SIZE = 1024 * 8;
     protected static final int TIMEOUT_IN_MS = 1000 * 4;
 
-    private boolean mCanUpload;
     private File mFilesDir;
     private URL mUrl;
 
@@ -68,14 +67,10 @@
     public void onCreate() {
         super.onCreate();
 
-        mCanUpload = false;
         mFilesDir = null;
         mUrl = null;
 
-        final PackageManager packageManager = getPackageManager();
-        final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET,
-                getPackageName()) == PackageManager.PERMISSION_GRANTED;
-        if (!hasPermission) {
+        if (!hasUploadingPermission()) {
             return;
         }
 
@@ -86,23 +81,39 @@
             }
             mFilesDir = getFilesDir();
             mUrl = new URL(urlString);
-            mCanUpload = true;
         } catch (MalformedURLException e) {
             e.printStackTrace();
         }
     }
 
+    public boolean isPossibleToUpload() {
+        return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_AUTO_UPLOAD;
+    }
+
+    private boolean hasUploadingPermission() {
+        final PackageManager packageManager = getPackageManager();
+        return packageManager.checkPermission(Manifest.permission.INTERNET,
+                getPackageName()) == PackageManager.PERMISSION_GRANTED;
+    }
+
     @Override
     protected void onHandleIntent(Intent intent) {
-        if (!mCanUpload) {
-            return;
+        if (!isPossibleToUpload()) return;
+        if (isUploadingUnconditionally(intent.getExtras()) || isConvenientToUpload()) {
+            doUpload();
         }
-        boolean isUploadingUnconditionally = false;
-        Bundle bundle = intent.getExtras();
-        if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) {
-            isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY);
+    }
+
+    private boolean isUploadingUnconditionally(final Bundle bundle) {
+        if (bundle == null) return false;
+        if (bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) {
+            return bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY);
         }
-        doUpload(isUploadingUnconditionally);
+        return false;
+    }
+
+    private boolean isConvenientToUpload() {
+        return isExternallyPowered() && hasWifiConnection();
     }
 
     private boolean isExternallyPowered() {
@@ -120,11 +131,7 @@
         return wifiInfo.isConnected();
     }
 
-    private void doUpload(final boolean isUploadingUnconditionally) {
-        if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection()
-                || IS_INHIBITING_AUTO_UPLOAD)) {
-            return;
-        }
+    private void doUpload() {
         if (mFilesDir == null) {
             return;
         }