Merge "Add a kind to the suggestion for bookkeeping (A1)"
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 99ceca2..c6800a9 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -24,8 +24,7 @@
     <string name="aosp_android_keyboard_ime_name" msgid="7877134937939182296">"Android-tastatur (AOSP)"</string>
     <string name="english_ime_settings" msgid="6661589557206947774">"Android-tastatur-indstillinger"</string>
     <string name="english_ime_input_options" msgid="3909945612939668554">"Indstillinger for input"</string>
-    <!-- no translation found for english_ime_research_log (8492602295696577851) -->
-    <skip />
+    <string name="english_ime_research_log" msgid="8492602295696577851">"Forskningslogkommandoer"</string>
     <string name="spell_checker_service_name" msgid="7338064335159755926">"Android-stavekontrol"</string>
     <string name="aosp_spell_checker_service_name" msgid="6985142605330377819">"Android-stavekontrol (AOSP)"</string>
     <string name="android_spell_checker_settings" msgid="5822324635435443689">"Indstillinger for stavekontrol"</string>
@@ -113,18 +112,12 @@
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Stemmeinput deaktiveret"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Konfigurer inputmetoder"</string>
     <string name="language_selection_title" msgid="1651299598555326750">"Inputsprog"</string>
-    <!-- no translation found for note_timestamp_for_researchlog (1889446857977976026) -->
-    <skip />
-    <!-- no translation found for notify_recorded_timestamp (8036429032449612051) -->
-    <skip />
-    <!-- no translation found for do_not_log_this_session (413762473641146336) -->
-    <skip />
-    <!-- no translation found for notify_session_log_deleting (3299507647764414623) -->
-    <skip />
-    <!-- no translation found for notify_session_log_deleted (8687927130100934686) -->
-    <skip />
-    <!-- no translation found for notify_session_log_not_deleted (2592908998810755970) -->
-    <skip />
+    <string name="note_timestamp_for_researchlog" msgid="1889446857977976026">"Notér tidsstempel i log"</string>
+    <string name="notify_recorded_timestamp" msgid="8036429032449612051">"Noteret tidsstempel"</string>
+    <string name="do_not_log_this_session" msgid="413762473641146336">"Logfør ikke denne session"</string>
+    <string name="notify_session_log_deleting" msgid="3299507647764414623">"Sletter sessionslogfil"</string>
+    <string name="notify_session_log_deleted" msgid="8687927130100934686">"Sessionslogfil slettet"</string>
+    <string name="notify_session_log_not_deleted" msgid="2592908998810755970">"Sessionslog IKKE slettet"</string>
     <string name="select_language" msgid="3693815588777926848">"Inputsprog"</string>
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Tryk igen for at gemme"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Ordbog er tilgængelig"</string>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index f8f3c33..16411b8 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -41,15 +41,15 @@
     <string name="include_other_imes_in_language_switch_list" msgid="4533689960308565519">"Diğer giriş yöntemine geç"</string>
     <string name="include_other_imes_in_language_switch_list_summary" msgid="840637129103317635">"Dil geçiş tuşu diğer giriş yöntemlerini de kapsar"</string>
     <string name="suppress_language_switch_key" msgid="8003788410354806368">"Dil geçiş tuşunu gösterme"</string>
-    <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tuş popup içn kaptm ertlm"</string>
+    <string name="key_preview_popup_dismiss_delay" msgid="6213164897443068248">"Tuş popup\'ının kapatılmasını geciktirme"</string>
     <string name="key_preview_popup_dismiss_no_delay" msgid="2096123151571458064">"Gecikme yok"</string>
     <string name="key_preview_popup_dismiss_default_delay" msgid="2166964333903906734">"Varsayılan"</string>
     <string name="use_contacts_dict" msgid="4435317977804180815">"Kişi Adları öner"</string>
     <string name="use_contacts_dict_summary" msgid="6599983334507879959">"Öneri ve düzeltmeler için Kişiler\'deki adları kullan"</string>
     <string name="enable_span_insert" msgid="7204653105667167620">"Düzeltmeleri etkinleştir"</string>
     <string name="enable_span_insert_summary" msgid="2947317657871394467">"Yeniden düzeltmeler için önerileri ayarla"</string>
-    <string name="auto_cap" msgid="1719746674854628252">"Otomatik olarak büyük harf yap"</string>
-    <string name="configure_dictionaries_title" msgid="4238652338556902049">"Ekli sözlükler"</string>
+    <string name="auto_cap" msgid="1719746674854628252">"Otomatik olarak büyük fark yap"</string>
+    <string name="configure_dictionaries_title" msgid="4238652338556902049">"Ek sözlükler"</string>
     <string name="main_dictionary" msgid="4798763781818361168">"Ana sözlük"</string>
     <string name="prefs_show_suggestions" msgid="8026799663445531637">"Düzeltme önerilerini göster"</string>
     <string name="prefs_show_suggestions_summary" msgid="1583132279498502825">"Yazarken, önerilen kelimeleri görüntüle"</string>
@@ -57,7 +57,7 @@
     <string name="prefs_suggestion_visibility_show_only_portrait_name" msgid="3551821800439659812">"Dikey modda göster"</string>
     <string name="prefs_suggestion_visibility_hide_name" msgid="6309143926422234673">"Her zaman gizle"</string>
     <string name="auto_correction" msgid="4979925752001319458">"Otomatik düzeltme"</string>
-    <string name="auto_correction_summary" msgid="5625751551134658006">"Boşluk çbğ ve nokt işr yanlış yazılan kelimeleri oto düzeltir"</string>
+    <string name="auto_correction_summary" msgid="5625751551134658006">"Boşluk tuşu ve noktalama işaretleri yanlış yazılan kelimeleri otomatikman düzeltir"</string>
     <string name="auto_correction_threshold_mode_off" msgid="8470882665417944026">"Kapalı"</string>
     <string name="auto_correction_threshold_mode_modest" msgid="8788366690620799097">"Ölçülü"</string>
     <string name="auto_correction_threshold_mode_aggeressive" msgid="3524029103734923819">"Agresif"</string>
@@ -107,7 +107,7 @@
     <string name="voice_input_modes_main_keyboard" msgid="3360660341121083174">"Ana klavyede"</string>
     <string name="voice_input_modes_symbols_keyboard" msgid="7203213240786084067">"Simge klavyesinde"</string>
     <string name="voice_input_modes_off" msgid="3745699748218082014">"Kapalı"</string>
-    <string name="voice_input_modes_summary_main_keyboard" msgid="6586544292900314339">"Ana klavyede mikrfn"</string>
+    <string name="voice_input_modes_summary_main_keyboard" msgid="6586544292900314339">"Ana klavyedeki mikrofon"</string>
     <string name="voice_input_modes_summary_symbols_keyboard" msgid="5233725927281932391">"Simge klavysnd mikrf"</string>
     <string name="voice_input_modes_summary_off" msgid="63875609591897607">"Sesle grş devre dışı"</string>
     <string name="configure_input_method" msgid="373356270290742459">"Giriş yöntemlerini yapılandır"</string>
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 7cd9bc2..9c3d46e 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -115,4 +115,12 @@
     public void close() {
         // empty base implementation
     }
+
+    /**
+     * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+     */
+
+    public boolean isInitialized() {
+        return true;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 1a05fcd..26c2e63 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -82,8 +82,9 @@
         return maxFreq;
     }
 
-    public boolean isEmpty() {
-        return mDictionaries.isEmpty();
+    @Override
+    public boolean isInitialized() {
+        return !mDictionaries.isEmpty();
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 77a0ccf..559f5ec 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -456,6 +456,9 @@
         }
 
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.getInstance().initSuggest(mSuggest);
+        }
 
         mUserDictionary = new UserBinaryDictionary(this, localeStr);
         mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
@@ -872,33 +875,32 @@
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
         }
-        if (null != mInputAttributes && mInputAttributes.mApplicationSpecifiedCompletionOn) {
-            mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
-            if (applicationSpecifiedCompletions == null) {
-                clearSuggestions();
-                return;
-            }
-
-            final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
-                    SuggestedWords.getFromApplicationSpecifiedCompletions(
-                            applicationSpecifiedCompletions);
-            final SuggestedWords suggestedWords = new SuggestedWords(
-                    applicationSuggestedWords,
-                    false /* typedWordValid */,
-                    false /* hasAutoCorrectionCandidate */,
-                    false /* allowsToBeAutoCorrected */,
-                    false /* isPunctuationSuggestions */,
-                    false /* isObsoleteSuggestions */,
-                    false /* isPrediction */);
-            // When in fullscreen mode, show completions generated by the application
-            final boolean isAutoCorrection = false;
-            setSuggestions(suggestedWords, isAutoCorrection);
-            setAutoCorrectionIndicator(isAutoCorrection);
-            // TODO: is this the right thing to do? What should we auto-correct to in
-            // this case? This says to keep whatever the user typed.
-            mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
-            setSuggestionStripShown(true);
+        if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+        if (applicationSpecifiedCompletions == null) {
+            clearSuggestions();
+            return;
         }
+
+        final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+                SuggestedWords.getFromApplicationSpecifiedCompletions(
+                        applicationSpecifiedCompletions);
+        final SuggestedWords suggestedWords = new SuggestedWords(
+                applicationSuggestedWords,
+                false /* typedWordValid */,
+                false /* hasAutoCorrectionCandidate */,
+                false /* allowsToBeAutoCorrected */,
+                false /* isPunctuationSuggestions */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction */);
+        // When in fullscreen mode, show completions generated by the application
+        final boolean isAutoCorrection = false;
+        setSuggestions(suggestedWords, isAutoCorrection);
+        setAutoCorrectionIndicator(isAutoCorrection);
+        // TODO: is this the right thing to do? What should we auto-correct to in
+        // this case? This says to keep whatever the user typed.
+        mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+        setSuggestionStripShown(true);
     }
 
     private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
@@ -1277,8 +1279,7 @@
             }
             break;
         default:
-            if (primaryCode == Keyboard.CODE_TAB
-                    && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
+            if (primaryCode == Keyboard.CODE_TAB && mCurrentSettings.isEditorActionNext()) {
                 performEditorAction(EditorInfo.IME_ACTION_NEXT);
                 break;
             }
@@ -1628,8 +1629,9 @@
 
     public boolean isSuggestionsRequested() {
         // TODO: move this method to mCurrentSettings
-        return (null != mInputAttributes && mInputAttributes.mIsSettingsSuggestionStripOn)
-                && (mCurrentSettings.isCorrectionOn() || isShowingSuggestionsStrip());
+        return mCurrentSettings.isSuggestionStripRequestedByTextField()
+                && (mCurrentSettings.isCorrectionOn()
+                || mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation));
     }
 
     public boolean isShowingPunctuationList() {
@@ -1637,18 +1639,14 @@
         return mCurrentSettings.mSuggestPuncList == mSuggestionsView.getSuggestions();
     }
 
-    public boolean isShowingSuggestionsStrip() {
-        return mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation);
-    }
-
     public boolean isSuggestionsStripVisible() {
         if (mSuggestionsView == null)
             return false;
         if (mSuggestionsView.isShowingAddToDictionaryHint())
             return true;
-        if (!isShowingSuggestionsStrip())
+        if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
             return false;
-        if (null != mInputAttributes && mInputAttributes.mApplicationSpecifiedCompletionOn)
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
             return true;
         return isSuggestionsRequested();
     }
@@ -1830,7 +1828,7 @@
             }
         }
 
-        if ((null != mInputAttributes && mInputAttributes.mApplicationSpecifiedCompletionOn)
+        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
                 && mApplicationSpecifiedCompletions != null
                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
             if (mSuggestionsView != null) {
@@ -2227,13 +2225,14 @@
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
         p.println("  Keyboard mode = " + keyboardMode);
-        p.println("  mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
+        p.println("  mIsSuggestionsStripRequestedByTextField = "
+                + mCurrentSettings.isSuggestionStripRequestedByTextField());
         p.println("  mCorrectionMode=" + mCurrentSettings.mCorrectionMode);
         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
         p.println("  isCorrectionOn=" + mCurrentSettings.isCorrectionOn());
         p.println("  mSoundOn=" + mCurrentSettings.mSoundOn);
         p.println("  mVibrateOn=" + mCurrentSettings.mVibrateOn);
         p.println("  mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
-        p.println("  mInputAttributes=" + mInputAttributes.toString());
+        p.println("  inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
index 5779d99..ee59692 100644
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -51,7 +51,9 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
@@ -67,6 +69,7 @@
 public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String TAG = ResearchLogger.class.getSimpleName();
     private static final boolean DEBUG = false;
+    private static final boolean OUTPUT_ENTIRE_BUFFER = false;  // true may disclose private info
     /* package */ static boolean sIsLogging = false;
     private static final int OUTPUT_FORMAT_VERSION = 1;
     private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
@@ -96,10 +99,19 @@
     private static final int LOGGING_STATE_STOPPING = 2;
     private boolean mIsPasswordView = false;
 
+    // digits entered by the user are replaced with this codepoint.
+    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
+            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";
     // set when LatinIME should ignore an onUpdateSelection() callback that
     // arises from operations in this class
     private static boolean sLatinIMEExpectingUpdateSelection = false;
 
+    // used to check whether words are not unique
+    private Suggest mSuggest;
+    private Dictionary mDictionary;
+
     private static class NullOutputStream extends OutputStream {
         /** {@inheritDoc} */
         @Override
@@ -314,6 +326,10 @@
         latinIME.showOptionDialog(builder.create());
     }
 
+    public void initSuggest(Suggest suggest) {
+        mSuggest = suggest;
+    }
+
     private void setIsPasswordView(boolean isPasswordView) {
         mIsPasswordView = isPasswordView;
     }
@@ -327,125 +343,241 @@
     private static final String EVENT_TYPE_KEY = "_ty";
     private static final Object[] EVENTKEYS_NULLVALUES = {};
 
+    private LogUnit mCurrentLogUnit = new LogUnit();
+
     /**
-     * Write a description of the event out to the ResearchLog.
+     * Buffer a research log event, flagging it as privacy-sensitive.
      *
-     * Runs in the background to avoid blocking the UI thread.
+     * This event contains potentially private information.  If the word that this event is a part
+     * of is determined to be privacy-sensitive, then this event should not be included in the
+     * output log.  The system waits to output until the containing word is known.
      *
      * @param keys an array containing a descriptive name for the event, followed by the keys
      * @param values an array of values, either a String or Number.  length should be one
      * less than the keys array
      */
-    private synchronized void writeEvent(final String[] keys, final Object[] values) {
+    private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
+            final Object[] values) {
         assert values.length + 1 == keys.length;
+        mCurrentLogUnit.addLogAtom(keys, values, true);
+    }
+
+    /**
+     * Buffer a research log event, flaggint it as not privacy-sensitive.
+     *
+     * This event contains no potentially private information.  Even if the word that this event
+     * is privacy-sensitive, this event can still safely be sent to the output log.  The system
+     * waits until the containing word is known so that this event can be written in the proper
+     * temporal order with other events that may be privacy sensitive.
+     *
+     * @param keys an array containing a descriptive name for the event, followed by the keys
+     * @param values an array of values, either a String or Number.  length should be one
+     * less than the keys array
+     */
+    private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
+        assert values.length + 1 == keys.length;
+        mCurrentLogUnit.addLogAtom(keys, values, false);
+    }
+
+    private boolean isInDictionary(CharSequence word) {
+        return (mDictionary != null) && (mDictionary.isValidWord(word));
+    }
+
+    /**
+     * Write out enqueued LogEvents to the log, filtered for privacy.
+     *
+     * If word is in the dictionary, then it is not privacy-sensitive and all LogEvents related to
+     * it can be written to the log.  If the word is not in the dictionary, then it may correspond
+     * to a proper name, which might reveal private information, so neither the word nor any
+     * information related to the word (e.g. the down/motion/up coordinates) should be revealed.
+     * These LogEvents have been marked as privacy-sensitive; non privacy-sensitive events are still
+     * written out.
+     *
+     * @param word the word to be checked for inclusion in the dictionary
+     */
+    /* package for test */ synchronized void flushQueue(CharSequence word) {
         if (isAllowedToLog()) {
-            mLoggingHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        mJsonWriter.beginObject();
-                        mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
-                        mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
-                        mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
-                        final int length = values.length;
-                        for (int i = 0; i < length; i++) {
-                            mJsonWriter.name(keys[i + 1]);
-                            Object value = values[i];
-                            if (value instanceof String) {
-                                mJsonWriter.value((String) value);
-                            } else if (value instanceof Number) {
-                                mJsonWriter.value((Number) value);
-                            } else if (value instanceof Boolean) {
-                                mJsonWriter.value((Boolean) value);
-                            } else if (value instanceof CompletionInfo[]) {
-                                CompletionInfo[] ci = (CompletionInfo[]) value;
-                                mJsonWriter.beginArray();
-                                for (int j = 0; j < ci.length; j++) {
-                                    mJsonWriter.value(ci[j].toString());
-                                }
-                                mJsonWriter.endArray();
-                            } else if (value instanceof SharedPreferences) {
-                                SharedPreferences prefs = (SharedPreferences) value;
-                                mJsonWriter.beginObject();
-                                for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
-                                    mJsonWriter.name(entry.getKey());
-                                    final Object innerValue = entry.getValue();
-                                    if (innerValue == null) {
-                                        mJsonWriter.nullValue();
-                                    } else if (innerValue instanceof Boolean) {
-                                        mJsonWriter.value((Boolean) innerValue);
-                                    } else if (innerValue instanceof Number) {
-                                        mJsonWriter.value((Number) innerValue);
-                                    } else {
-                                        mJsonWriter.value(innerValue.toString());
-                                    }
-                                }
-                                mJsonWriter.endObject();
-                            } else if (value instanceof Key[]) {
-                                Key[] keys = (Key[]) value;
-                                mJsonWriter.beginArray();
-                                for (Key key : keys) {
-                                    mJsonWriter.beginObject();
-                                    mJsonWriter.name("code").value(key.mCode);
-                                    mJsonWriter.name("altCode").value(key.mAltCode);
-                                    mJsonWriter.name("x").value(key.mX);
-                                    mJsonWriter.name("y").value(key.mY);
-                                    mJsonWriter.name("w").value(key.mWidth);
-                                    mJsonWriter.name("h").value(key.mHeight);
-                                    mJsonWriter.endObject();
-                                }
-                                mJsonWriter.endArray();
-                            } else if (value instanceof SuggestedWords) {
-                                SuggestedWords words = (SuggestedWords) value;
-                                mJsonWriter.beginObject();
-                                mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
-                                mJsonWriter.name("hasAutoCorrectionCandidate")
-                                    .value(words.mHasAutoCorrectionCandidate);
-                                mJsonWriter.name("isPunctuationSuggestions")
-                                    .value(words.mIsPunctuationSuggestions);
-                                mJsonWriter.name("allowsToBeAutoCorrected")
-                                    .value(words.mAllowsToBeAutoCorrected);
-                                mJsonWriter.name("isObsoleteSuggestions")
-                                    .value(words.mIsObsoleteSuggestions);
-                                mJsonWriter.name("isPrediction")
-                                    .value(words.mIsPrediction);
-                                mJsonWriter.name("words");
-                                mJsonWriter.beginArray();
-                                final int size = words.size();
-                                for (int j = 0; j < size; j++) {
-                                    SuggestedWordInfo wordInfo = words.getWordInfo(j);
-                                    mJsonWriter.value(wordInfo.toString());
-                                }
-                                mJsonWriter.endArray();
-                                mJsonWriter.endObject();
-                            } else if (value == null) {
-                                mJsonWriter.nullValue();
-                            } else {
-                                Log.w(TAG, "Unrecognized type to be logged: " +
-                                        (value == null ? "<null>" : value.getClass().getName()));
-                                mJsonWriter.nullValue();
-                            }
-                        }
-                        mJsonWriter.endObject();
-                    } catch (IOException e) {
-                        e.printStackTrace();
-                        Log.w(TAG, "Error in JsonWriter; disabling logging");
-                        try {
-                            mJsonWriter.close();
-                        } catch (IllegalStateException e1) {
-                            // assume that this is just the json not being terminated properly.
-                            // ignore
-                        } catch (IOException e1) {
-                            e1.printStackTrace();
-                        } finally {
-                            mJsonWriter = NULL_JSON_WRITER;
+            // check for dictionary
+            if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) {
+                mDictionary = mSuggest.getMainDictionary();
+            }
+            mCurrentLogUnit.setIsPrivacySafe(word != null && isInDictionary(word));
+            mLoggingHandler.post(mCurrentLogUnit);
+            mCurrentLogUnit = new LogUnit();
+        }
+    }
+
+    private synchronized void outputEvent(final String[] keys, final Object[] values) {
+        try {
+            mJsonWriter.beginObject();
+            mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+            mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
+            mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
+            final int length = values.length;
+            for (int i = 0; i < length; i++) {
+                mJsonWriter.name(keys[i + 1]);
+                Object value = values[i];
+                if (value instanceof String) {
+                    mJsonWriter.value((String) value);
+                } else if (value instanceof Number) {
+                    mJsonWriter.value((Number) value);
+                } else if (value instanceof Boolean) {
+                    mJsonWriter.value((Boolean) value);
+                } else if (value instanceof CompletionInfo[]) {
+                    CompletionInfo[] ci = (CompletionInfo[]) value;
+                    mJsonWriter.beginArray();
+                    for (int j = 0; j < ci.length; j++) {
+                        mJsonWriter.value(ci[j].toString());
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SharedPreferences) {
+                    SharedPreferences prefs = (SharedPreferences) value;
+                    mJsonWriter.beginObject();
+                    for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+                        mJsonWriter.name(entry.getKey());
+                        final Object innerValue = entry.getValue();
+                        if (innerValue == null) {
+                            mJsonWriter.nullValue();
+                        } else if (innerValue instanceof Boolean) {
+                            mJsonWriter.value((Boolean) innerValue);
+                        } else if (innerValue instanceof Number) {
+                            mJsonWriter.value((Number) innerValue);
+                        } else {
+                            mJsonWriter.value(innerValue.toString());
                         }
                     }
+                    mJsonWriter.endObject();
+                } else if (value instanceof Key[]) {
+                    Key[] keyboardKeys = (Key[]) value;
+                    mJsonWriter.beginArray();
+                    for (Key keyboardKey : keyboardKeys) {
+                        mJsonWriter.beginObject();
+                        mJsonWriter.name("code").value(keyboardKey.mCode);
+                        mJsonWriter.name("altCode").value(keyboardKey.mAltCode);
+                        mJsonWriter.name("x").value(keyboardKey.mX);
+                        mJsonWriter.name("y").value(keyboardKey.mY);
+                        mJsonWriter.name("w").value(keyboardKey.mWidth);
+                        mJsonWriter.name("h").value(keyboardKey.mHeight);
+                        mJsonWriter.endObject();
+                    }
+                    mJsonWriter.endArray();
+                } else if (value instanceof SuggestedWords) {
+                    SuggestedWords words = (SuggestedWords) value;
+                    mJsonWriter.beginObject();
+                    mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+                    mJsonWriter.name("hasAutoCorrectionCandidate")
+                        .value(words.mHasAutoCorrectionCandidate);
+                    mJsonWriter.name("isPunctuationSuggestions")
+                        .value(words.mIsPunctuationSuggestions);
+                    mJsonWriter.name("allowsToBeAutoCorrected")
+                        .value(words.mAllowsToBeAutoCorrected);
+                    mJsonWriter.name("isObsoleteSuggestions")
+                        .value(words.mIsObsoleteSuggestions);
+                    mJsonWriter.name("isPrediction")
+                        .value(words.mIsPrediction);
+                    mJsonWriter.name("words");
+                    mJsonWriter.beginArray();
+                    final int size = words.size();
+                    for (int j = 0; j < size; j++) {
+                        SuggestedWordInfo wordInfo = words.getWordInfo(j);
+                        mJsonWriter.value(wordInfo.toString());
+                    }
+                    mJsonWriter.endArray();
+                    mJsonWriter.endObject();
+                } else if (value == null) {
+                    mJsonWriter.nullValue();
+                } else {
+                    Log.w(TAG, "Unrecognized type to be logged: " +
+                            (value == null ? "<null>" : value.getClass().getName()));
+                    mJsonWriter.nullValue();
                 }
-            });
+            }
+            mJsonWriter.endObject();
+        } catch (IOException e) {
+            e.printStackTrace();
+            Log.w(TAG, "Error in JsonWriter; disabling logging");
+            try {
+                mJsonWriter.close();
+            } catch (IllegalStateException e1) {
+                // assume that this is just the json not being terminated properly.
+                // ignore
+            } catch (IOException e1) {
+                e1.printStackTrace();
+            } finally {
+                mJsonWriter = NULL_JSON_WRITER;
+            }
         }
     }
 
+    private static class LogUnit implements Runnable {
+        private final List<String[]> mKeysList = new ArrayList<String[]>();
+        private final List<Object[]> mValuesList = new ArrayList<Object[]>();
+        private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
+        private boolean mIsPrivacySafe = false;
+
+        private void addLogAtom(final String[] keys, final Object[] values,
+                final Boolean isPotentiallyPrivate) {
+            mKeysList.add(keys);
+            mValuesList.add(values);
+            mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+        }
+
+        void setIsPrivacySafe(boolean isPrivacySafe) {
+            mIsPrivacySafe = isPrivacySafe;
+        }
+
+        @Override
+        public void run() {
+            final int numAtoms = mKeysList.size();
+            for (int atomIndex = 0; atomIndex < numAtoms; atomIndex++) {
+                if (!mIsPrivacySafe && mIsPotentiallyPrivate.get(atomIndex)) {
+                    continue;
+                }
+                final String[] keys = mKeysList.get(atomIndex);
+                final Object[] values = mValuesList.get(atomIndex);
+                ResearchLogger.getInstance().outputEvent(keys, values);
+            }
+        }
+    }
+
+    private static int scrubDigitFromCodePoint(int codePoint) {
+        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
+    }
+
+    /* package for test */ static String scrubDigitsFromString(String s) {
+        StringBuilder sb = null;
+        final int length = s.length();
+        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
+            int codePoint = Character.codePointAt(s, i);
+            if (Character.isDigit(codePoint)) {
+                if (sb == null) {
+                    sb = new StringBuilder(length);
+                    sb.append(s.substring(0, i));
+                }
+                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
+            } else {
+                if (sb != null) {
+                    sb.appendCodePoint(codePoint);
+                }
+            }
+        }
+        if (sb == null) {
+            return s;
+        } else {
+            return sb.toString();
+        }
+    }
+
+    private String scrubWord(String word) {
+        if (mDictionary == null) {
+            return WORD_REPLACEMENT_STRING;
+        }
+        if (mDictionary.isValidWord(word)) {
+            return word;
+        }
+        return WORD_REPLACEMENT_STRING;
+    }
+
     private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
         "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
         "pressure"
@@ -469,7 +601,8 @@
             final Object[] values = {
                 actionString, eventTime, id, x, y, size, pressure
             };
-            getInstance().writeEvent(EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
         }
     }
 
@@ -478,9 +611,9 @@
     };
     public static void latinIME_onCodeInput(final int code, final int x, final int y) {
         final Object[] values = {
-            Keyboard.printableCode(code), x, y
+            Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
     }
 
     private static final String[] EVENTKEYS_CORRECTION = {
@@ -489,9 +622,9 @@
     public static void logCorrection(final String subgroup, final String before, final String after,
             final int position) {
         final Object[] values = {
-            subgroup, before, after, position
+            subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position
         };
-        getInstance().writeEvent(EVENTKEYS_CORRECTION, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = {
@@ -500,9 +633,12 @@
     public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
             final String autoCorrection) {
         final Object[] values = {
-            typedWord, autoCorrection
+            scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values);
+        researchLogger.flushQueue(autoCorrection);
     }
 
     private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = {
@@ -510,9 +646,11 @@
     };
     public static void latinIME_commitText(final CharSequence typedWord) {
         final Object[] values = {
-            typedWord.toString()
+            scrubDigitsFromString(typedWord.toString())
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_COMMITTEXT, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values);
+        researchLogger.flushQueue(typedWord);
     }
 
     private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = {
@@ -522,14 +660,14 @@
         final Object[] values = {
             length
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = {
         "LatinIMEDoubleSpaceAutoPeriod"
     };
     public static void latinIME_doubleSpaceAutoPeriod() {
-        getInstance().writeEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES);
     }
 
     private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
@@ -540,7 +678,8 @@
         final Object[] values = {
             applicationSpecifiedCompletions
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
+                values);
     }
 
     /* package */ static boolean getAndClearLatinIMEExpectingUpdateSelection() {
@@ -561,27 +700,35 @@
             ic.setSelection(savedSelectionStart, savedSelectionEnd);
             ic.endBatchEdit();
             sLatinIMEExpectingUpdateSelection = true;
-            Object[] values = new Object[2];
-            if (TextUtils.isEmpty(charSequence)) {
-                values[0] = false;
-                values[1] = "";
-            } else {
-                if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
-                    int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
-                    // do not cut in the middle of a supplementary character
-                    final char c = charSequence.charAt(length - 1);
-                    if (Character.isHighSurrogate(c)) {
-                        length--;
-                    }
-                    final CharSequence truncatedCharSequence = charSequence.subSequence(0, length);
-                    values[0] = true;
-                    values[1] = truncatedCharSequence.toString();
-                } else {
+            final Object[] values = new Object[2];
+            if (OUTPUT_ENTIRE_BUFFER) {
+                if (TextUtils.isEmpty(charSequence)) {
                     values[0] = false;
-                    values[1] = charSequence.toString();
+                    values[1] = "";
+                } else {
+                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+                        // do not cut in the middle of a supplementary character
+                        final char c = charSequence.charAt(length - 1);
+                        if (Character.isHighSurrogate(c)) {
+                            length--;
+                        }
+                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+                                length);
+                        values[0] = true;
+                        values[1] = truncatedCharSequence.toString();
+                    } else {
+                        values[0] = false;
+                        values[1] = charSequence.toString();
+                    }
                 }
+            } else {
+                values[0] = true;
+                values[1] = "";
             }
-            getInstance().writeEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+            final ResearchLogger researchLogger = getInstance();
+            researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+            researchLogger.flushQueue(null);
         }
     }
 
@@ -597,7 +744,7 @@
                 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY,
                 Build.MODEL, prefs, OUTPUT_FORMAT_VERSION
             };
-            getInstance().writeEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
+            getInstance().enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
         }
     }
 
@@ -631,12 +778,14 @@
                 word = range.mWord;
             }
         }
+        final ResearchLogger researchLogger = getInstance();
+        final String scrubbedWord = researchLogger.scrubWord(word);
         final Object[] values = {
             lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
             newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
-            expectingUpdateSelectionFromLogger, word
+            expectingUpdateSelectionFromLogger, scrubbedWord
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = {
@@ -646,7 +795,7 @@
         final Object[] values = {
             imeActionNext
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = {
@@ -657,7 +806,10 @@
         final Object[] values = {
             index, cs, x, y
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(
+                EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
+        researchLogger.flushQueue(cs.toString());
     }
 
     private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
@@ -666,9 +818,13 @@
     public static void latinIME_pickSuggestionManually(final String replacedWord,
             final int index, CharSequence suggestion, int x, int y) {
         final Object[] values = {
-            replacedWord, index, suggestion, x, y
+            scrubDigitsFromString(replacedWord), index, suggestion == null ? null :
+                    scrubDigitsFromString(suggestion.toString()), x, y
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY, values);
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
+                values);
+        researchLogger.flushQueue(suggestion.toString());
     }
 
     private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
@@ -679,14 +835,14 @@
         final Object[] values = {
             index, suggestion, x, y
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = {
         "LatinIMERevertDoubleSpaceWhileInBatchEdit"
     };
     public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
-        getInstance().writeEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT,
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT,
                 EVENTKEYS_NULLVALUES);
     }
 
@@ -694,7 +850,7 @@
         "LatinIMERevertSwapPunctuation"
     };
     public static void latinIME_revertSwapPunctuation() {
-        getInstance().writeEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES);
     }
 
     private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
@@ -702,16 +858,16 @@
     };
     public static void latinIME_sendKeyCodePoint(final int code) {
         final Object[] values = {
-            code
+            Keyboard.printableCode(scrubDigitFromCodePoint(code))
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
     }
 
     private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = {
         "LatinIMESwapSwapperAndSpaceWhileInBatchEdit"
     };
     public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
-        getInstance().writeEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT,
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT,
                 EVENTKEYS_NULLVALUES);
     }
 
@@ -719,14 +875,14 @@
         "LatinIMESwitchToKeyboardView"
     };
     public static void latinIME_switchToKeyboardView() {
-        getInstance().writeEvent(EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW, EVENTKEYS_NULLVALUES);
+        getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW, EVENTKEYS_NULLVALUES);
     }
 
     private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS = {
         "LatinKeyboardViewOnLongPress"
     };
     public static void latinKeyboardView_onLongPress() {
-        getInstance().writeEvent(EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
+        getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
     }
 
     private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD = {
@@ -738,7 +894,7 @@
     public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
         if (keyboard != null) {
             final KeyboardId kid = keyboard.mId;
-            boolean isPasswordView = kid.passwordInput();
+            final boolean isPasswordView = kid.passwordInput();
             final Object[] values = {
                     KeyboardId.elementIdToName(kid.mElementId),
                     kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
@@ -758,7 +914,7 @@
                     keyboard.mOccupiedHeight,
                     keyboard.mKeys
                 };
-            getInstance().writeEvent(EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD, values);
+            getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD, values);
             getInstance().setIsPasswordView(isPasswordView);
         }
     }
@@ -770,14 +926,14 @@
         final Object[] values = {
             originallyTypedWord
         };
-        getInstance().writeEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
     }
 
     private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
         "PointerTrackerCallListenerOnCancelInput"
     };
     public static void pointerTracker_callListenerOnCancelInput() {
-        getInstance().writeEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
+        getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
                 EVENTKEYS_NULLVALUES);
     }
 
@@ -791,10 +947,12 @@
         if (key != null) {
             CharSequence outputText = key.mOutputText;
             final Object[] values = {
-                Keyboard.printableCode(code), outputText, x, y, ignoreModifierKey, altersCode,
-                key.isEnabled()
+                Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
+                        : scrubDigitsFromString(outputText.toString()),
+                x, y, ignoreModifierKey, altersCode, key.isEnabled()
             };
-            getInstance().writeEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
         }
     }
 
@@ -806,10 +964,11 @@
             final boolean withSliding, final boolean ignoreModifierKey) {
         if (key != null) {
             final Object[] values = {
-                Keyboard.printableCode(primaryCode), withSliding, ignoreModifierKey,
-                key.isEnabled()
+                Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+                ignoreModifierKey, key.isEnabled()
             };
-            getInstance().writeEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
         }
     }
 
@@ -820,7 +979,7 @@
         final Object[] values = {
             deltaT, distanceSquared
         };
-        getInstance().writeEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
     }
 
     private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
@@ -831,7 +990,7 @@
         final Object[] values = {
             x, y, lastX, lastY
         };
-        getInstance().writeEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
+        getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
     }
 
     private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
@@ -842,8 +1001,8 @@
             final Object[] values = {
                 me.toString()
             };
-            getInstance().writeEvent(EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
-                    values);
+            getInstance().enqueuePotentiallyPrivateEvent(
+                    EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
         }
     }
 
@@ -855,7 +1014,8 @@
             final Object[] values = {
                 suggestedWords
             };
-            getInstance().writeEvent(EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS, values);
+            getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS,
+                    values);
         }
     }
 
@@ -863,6 +1023,6 @@
         "UserTimestamp"
     };
     public void userTimestamp() {
-        getInstance().writeEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+        getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index da4acea..d6d6c6c 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -76,9 +76,7 @@
     @SuppressWarnings("unused") // TODO: Use this
     private final String mKeyPreviewPopupDismissDelayRawValue;
     public final boolean mUseContactsDict;
-    // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
-    public final boolean mBigramSuggestionEnabled;
-    // Prediction: use bigrams to predict the next word when there is no input for it yet
+    // Use bigrams to predict the next word when there is no input for it yet
     public final boolean mBigramPredictionEnabled;
     public final boolean mEnableSuggestionSpanInsertion;
     @SuppressWarnings("unused") // TODO: Use this
@@ -87,6 +85,9 @@
     private final float mKeypressSoundVolumeRawValue;
     private final InputMethodSubtype[] mAdditionalSubtypes;
 
+    // From the input box
+    private final InputAttributes mInputAttributes;
+
     // Deduced settings
     public final int mKeypressVibrationDuration;
     public final float mFxVolume;
@@ -125,6 +126,13 @@
                 mSymbolsExcludedFromWordSeparators, res);
         mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
 
+        // Store the input attributes
+        if (null == inputAttributes) {
+            mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+        } else {
+            mInputAttributes = inputAttributes;
+        }
+
         // Get the settings preferences
         mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
         mVibrateOn = isVibrateOn(context, prefs, res);
@@ -147,10 +155,7 @@
                 Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
         mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
         mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
-        mBigramSuggestionEnabled = mAutoCorrectEnabled
-                && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
-        mBigramPredictionEnabled = mBigramSuggestionEnabled
-                && isBigramPredictionEnabled(prefs, res);
+        mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
         // TODO: remove mEnableSuggestionSpanInsertion. It's always true.
         mEnableSuggestionSpanInsertion = true;
         mVibrationDurationSettingsRawValue =
@@ -167,7 +172,7 @@
         mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
         mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
                 getPrefAdditionalSubtypes(prefs, res));
-        mCorrectionMode = createCorrectionMode(inputAttributes);
+        mCorrectionMode = createCorrectionMode();
         mSuggestionVisibility = createSuggestionVisibility(res);
     }
 
@@ -201,10 +206,10 @@
         return wordSeparators;
     }
 
-    private int createCorrectionMode(final InputAttributes inputAttributes) {
+    private int createCorrectionMode() {
         final boolean shouldAutoCorrect = mAutoCorrectEnabled
-                && (null == inputAttributes || !inputAttributes.mInputTypeNoAutoCorrect);
-        if (mBigramSuggestionEnabled && shouldAutoCorrect) return Suggest.CORRECTION_FULL_BIGRAM;
+                && !mInputAttributes.mInputTypeNoAutoCorrect;
+        if (shouldAutoCorrect) return Suggest.CORRECTION_FULL_BIGRAM;
         return shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
     }
 
@@ -225,6 +230,18 @@
                 res.getBoolean(R.bool.config_default_vibration_enabled));
     }
 
+    public boolean isApplicationSpecifiedCompletionsOn() {
+        return mInputAttributes.mApplicationSpecifiedCompletionOn;
+    }
+
+    public boolean isEditorActionNext() {
+        return mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT;
+    }
+
+    public boolean isSuggestionStripRequestedByTextField() {
+        return mInputAttributes.mIsSettingsSuggestionStripOn;
+    }
+
     public boolean isCorrectionOn() {
         return mCorrectionMode == Suggest.CORRECTION_FULL
                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM;
@@ -285,12 +302,6 @@
                         R.integer.config_key_preview_linger_timeout))));
     }
 
-    private static boolean isBigramSuggestionEnabled(final SharedPreferences sp,
-            final Resources resources, final boolean autoCorrectEnabled) {
-        // TODO: remove this method. Bigram suggestion is always true.
-        return true;
-    }
-
     private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
             final Resources resources) {
         return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
@@ -412,4 +423,9 @@
         final String newStr = Utils.localeAndTimeHashMapToStr(map);
         prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
     }
+
+    // For debug.
+    public String getInputAttributesDebugString() {
+        return mInputAttributes.toString();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 5b9d4a9..26e4591 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -65,7 +65,7 @@
 
     private static final boolean DBG = LatinImeLogger.sDBG;
 
-    private boolean mHasMainDictionary;
+    private Dictionary mMainDictionary;
     private ContactsBinaryDictionary mContactsDict;
     private WhitelistDictionary mWhiteListDictionary;
     private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
@@ -98,7 +98,7 @@
             final long startOffset, final long length, final Locale locale) {
         final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
                 startOffset, length /* useFullEditDistance */, false, locale);
-        mHasMainDictionary = null != mainDict;
+        mMainDictionary = mainDict;
         addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
         addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
         initWhitelistAndAutocorrectAndPool(context, locale);
@@ -129,15 +129,15 @@
     }
 
     public void resetMainDict(final Context context, final Locale locale) {
-        mHasMainDictionary = false;
+        mMainDictionary = null;
         new Thread("InitializeBinaryDictionary") {
             @Override
             public void run() {
                 final DictionaryCollection newMainDict =
                         DictionaryFactory.createMainDictionaryFromManager(context, locale);
-                mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
                 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
                 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+                mMainDictionary = newMainDict;
             }
         }.start();
     }
@@ -145,7 +145,11 @@
     // The main dictionary could have been loaded asynchronously.  Don't cache the return value
     // of this method.
     public boolean hasMainDictionary() {
-        return mHasMainDictionary;
+        return null != mMainDictionary && mMainDictionary.isInitialized();
+    }
+
+    public Dictionary getMainDictionary() {
+        return mMainDictionary;
     }
 
     public ContactsBinaryDictionary getContactsDictionary() {
@@ -366,7 +370,7 @@
         // language, and it will unexpectedly auto-correct. For example, if the user types in
         // English with no dictionary and has a "Will" in their contact list, "will" would
         // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
-                && mHasMainDictionary;
+                && hasMainDictionary();
 
         boolean autoCorrectionAvailable = hasAutoCorrection;
         if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
@@ -513,7 +517,7 @@
         for (final Dictionary dictionary : dictionaries) {
             dictionary.close();
         }
-        mHasMainDictionary = false;
+        mMainDictionary = null;
     }
 
     // TODO: Resolve the inconsistencies between the native auto correction algorithms and