Merge "Consolidate SuggestedWords.getWordInfo and .getInfo"
diff --git a/java/res/values-fr/donottranslate.xml b/java/res/values-fr/donottranslate.xml
index e11e65c..2e916a7 100644
--- a/java/res/values-fr/donottranslate.xml
+++ b/java/res/values-fr/donottranslate.xml
@@ -18,15 +18,13 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Symbols that should be swapped with a magic space -->
-    <string name="weak_space_swapping_symbols">.,)]}</string>
-    <!-- Symbols that should strip a magic space -->
+    <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
+    <string name="symbols_preceded_by_space">([{*&amp;;:!?</string>
+    <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
+    <string name="symbols_followed_by_space">.,;:!?)]}*&amp;</string>
+    <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="weak_space_stripping_symbols">"&#x0009;&#x0020;\n"\'-/_\"</string>
-    <!-- Symbols that should promote magic spaces into real space -->
-    <string name="phantom_space_promoting_symbols">;:!?([*&amp;@{&lt;&gt;+=|</string>
-    <!-- Symbols that do NOT separate words -->
-    <!-- Note that this is identical to the default value, but since the above ones are different
-         and those variables only make sense together, this is kept here for readability. -->
-    <string name="symbols_excluded_from_word_separators">\'-</string>
+    <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <!-- Word connectors -->
+    <string name="symbols_word_connectors">\'-</string>
 </resources>
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index 193a019..70ace77 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -20,18 +20,15 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Symbols that are suggested between words -->
     <string name="suggested_punctuations">!,?,\\,,:,;,\",(,),\',-,/,@,_</string>
-    <!-- Symbols that should be swapped with a weak space -->
-    <string name="weak_space_swapping_symbols">.,;:!?)]}</string>
-    <!-- Symbols that should strip a weak space -->
+    <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
+    <string name="symbols_preceded_by_space">([{*&amp;</string>
+    <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
+    <string name="symbols_followed_by_space">.,;:!?)]}*&amp;</string>
+    <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="weak_space_stripping_symbols">"&#x0009;&#x0020;\n"/_\'-@\"</string>
-    <!-- Symbols that should convert weak spaces into real space -->
-    <string name="phantom_space_promoting_symbols">([*&amp;{&lt;&gt;+=|</string>
-    <!-- Symbols that do NOT separate words -->
-    <string name="symbols_excluded_from_word_separators">\'-</string>
-    <!-- Word separator list is the union of all symbols except those that are not separators:
-    weak_space_swapping_symbols | weak_space_stripping_symbols
-            \ symbols_excluded_from_word_separators -->
+    <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <!-- Word connectors -->
+    <string name="symbols_word_connectors">\'-</string>
     <!-- Symbol characters list that should switch back to the main layout -->
     <!-- U+2018: "‘" LEFT SINGLE QUOTATION MARK
          U+2019: "’" RIGHT SINGLE QUOTATION MARK
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 5c54427..f9d51ff 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -269,8 +269,7 @@
     <string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
     <!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
-    <!-- TODO: handle multilingual plurals -->
-    <string name="research_feedback_include_history_label" translatable="false">Include last <xliff:g id="word">%d</xliff:g> words entered</string>
+    <string name="research_feedback_include_history_label" translatable="false">Include session history</string>
     <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
@@ -376,4 +375,7 @@
     <string name="prefs_keypress_vibration_duration_settings">Keypress vibration duration settings</string>
     <!-- Title of the settings for keypress sound volume -->
     <string name="prefs_keypress_sound_volume_settings">Keypress sound volume settings</string>
+
+    <!-- Title of the button to revert to the default value of the device in the settings dialog [CHAR LIMIT=15] -->
+    <string name="button_default">Default</string>
 </resources>
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 6a01b01..ea86d98 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -209,7 +209,7 @@
     private String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
             final Key key) {
         final KeyboardId keyboardId = keyboard.mId;
-        final int actionId = keyboardId.imeActionId();
+        final int actionId = keyboardId.imeAction();
         final int resId;
 
         // Always use the label, if available.
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index f9ff7b0..02116ca 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -172,12 +172,7 @@
     }
 
     public int imeAction() {
-        return InputTypeUtils.getActionIdFromEditorInfo(mEditorInfo);
-    }
-
-    public int imeActionId() {
-        final int actionId = imeAction();
-        return actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL ? mEditorInfo.actionId : actionId;
+        return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
index a840725..4a8407c 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -138,6 +138,7 @@
     }
 
     private final RoundedLine mRoundedLine = new RoundedLine();
+    private final Rect mRoundedLineBounds = new Rect();
 
     /**
      * Draw gesture preview trail
@@ -149,6 +150,8 @@
      */
     public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
             final Rect outBoundsRect, final Params params) {
+        // Initialize bounds rectangle.
+        outBoundsRect.setEmpty();
         final int trailSize = mEventTimes.getLength();
         if (trailSize == 0) {
             return false;
@@ -171,39 +174,32 @@
         if (startIndex < trailSize) {
             paint.setColor(params.mTrailColor);
             paint.setStyle(Paint.Style.FILL);
-            final RoundedLine line = mRoundedLine;
+            final RoundedLine roundedLine = mRoundedLine;
             int p1x = getXCoordValue(xCoords[startIndex]);
             int p1y = yCoords[startIndex];
             final int lastTime = sinceDown - eventTimes[startIndex];
-            float maxWidth = getWidth(lastTime, params);
-            float r1 = maxWidth / 2.0f;
-            // Initialize bounds rectangle.
-            outBoundsRect.set(p1x, p1y, p1x, p1y);
+            float r1 = getWidth(lastTime, params) / 2.0f;
             for (int i = startIndex + 1; i < trailSize; i++) {
                 final int elapsedTime = sinceDown - eventTimes[i];
                 final int p2x = getXCoordValue(xCoords[i]);
                 final int p2y = yCoords[i];
-                final float width = getWidth(elapsedTime, params);
-                final float r2 = width / 2.0f;
+                final float r2 = getWidth(elapsedTime, params) / 2.0f;
                 // Draw trail line only when the current point isn't a down point.
                 if (!isDownEventXCoord(xCoords[i])) {
-                    final Path path = line.makePath(p1x, p1y, r1, p2x, p2y, r2);
+                    final Path path = roundedLine.makePath(p1x, p1y, r1, p2x, p2y, r2);
                     if (path != null) {
                         final int alpha = getAlpha(elapsedTime, params);
                         paint.setAlpha(alpha);
                         canvas.drawPath(path, paint);
                         // Take union for the bounds.
-                        outBoundsRect.union(p2x, p2y);
-                        maxWidth = Math.max(maxWidth, width);
+                        roundedLine.getBounds(mRoundedLineBounds);
+                        outBoundsRect.union(mRoundedLineBounds);
                     }
                 }
                 p1x = p2x;
                 p1y = p2y;
                 r1 = r2;
             }
-            // Take care of trail line width.
-            final int inset = -((int)maxWidth + 1);
-            outBoundsRect.inset(inset, inset);
         }
 
         final int newSize = trailSize - startIndex;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
index bfb7b1f..7c87467 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -52,7 +52,8 @@
     private int mOffscreenOffsetY;
     private Bitmap mOffscreenBuffer;
     private final Canvas mOffscreenCanvas = new Canvas();
-    private final Rect mOffscreenDirtyRect = new Rect();
+    private final Rect mOffscreenSrcRect = new Rect();
+    private final Rect mDirtyRect = new Rect();
     private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
     private final GestureFloatingPreviewText mGestureFloatingPreviewText;
     private boolean mShowSlidingKeyInputPreview;
@@ -193,6 +194,7 @@
         mOffscreenBuffer = Bitmap.createBitmap(
                 mOffscreenWidth, mOffscreenHeight, Bitmap.Config.ARGB_8888);
         mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+        mOffscreenCanvas.translate(0, mOffscreenOffsetY);
     }
 
     @Override
@@ -205,19 +207,18 @@
             mayAllocateOffscreenBuffer();
             // Draw gesture trails to offscreen buffer.
             final boolean needsUpdatingGesturePreviewTrail = drawGestureTrails(
-                    mOffscreenCanvas, mGesturePaint, mOffscreenDirtyRect);
-            // Transfer offscreen buffer to screen.
-            if (!mOffscreenDirtyRect.isEmpty()) {
-                canvas.translate(0, - mOffscreenOffsetY);
-                canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect,
-                        mGesturePaint);
-                canvas.translate(0, mOffscreenOffsetY);
-                // Note: Defer clearing the dirty rectangle here because we will get cleared
-                // rectangle on the canvas.
-            }
+                    mOffscreenCanvas, mGesturePaint, mDirtyRect);
             if (needsUpdatingGesturePreviewTrail) {
                 mDrawingHandler.postUpdateGestureTrailPreview();
             }
+            // Transfer offscreen buffer to screen.
+            if (!mDirtyRect.isEmpty()) {
+                mOffscreenSrcRect.set(mDirtyRect);
+                mOffscreenSrcRect.offset(0, mOffscreenOffsetY);
+                canvas.drawBitmap(mOffscreenBuffer, mOffscreenSrcRect, mDirtyRect, null);
+                // Note: Defer clearing the dirty rectangle here because we will get cleared
+                // rectangle on the canvas.
+            }
         }
         mGestureFloatingPreviewText.onDraw(canvas);
         if (mShowSlidingKeyInputPreview) {
@@ -235,10 +236,8 @@
             offscreenCanvas.drawRect(dirtyRect, paint);
         }
         dirtyRect.setEmpty();
-
-        // Draw gesture trails to offscreen buffer.
-        offscreenCanvas.translate(0, mOffscreenOffsetY);
         boolean needsUpdatingGesturePreviewTrail = false;
+        // Draw gesture trails to offscreen buffer.
         synchronized (mGesturePreviewTrails) {
             // Trails count == fingers count that have ever been active.
             final int trailsCount = mGesturePreviewTrails.size();
@@ -251,20 +250,9 @@
                 dirtyRect.union(mGesturePreviewTrailBoundsRect);
             }
         }
-        offscreenCanvas.translate(0, -mOffscreenOffsetY);
-
-        // Clip dirty rectangle with offscreen buffer width/height.
-        dirtyRect.offset(0, mOffscreenOffsetY);
-        clipRect(dirtyRect, 0, 0, mOffscreenWidth, mOffscreenHeight);
         return needsUpdatingGesturePreviewTrail;
     }
 
-    private static void clipRect(final Rect out, final int left, final int top, final int right,
-            final int bottom) {
-        out.set(Math.max(out.left, left), Math.max(out.top, top), Math.min(out.right, right),
-                Math.min(out.bottom, bottom));
-    }
-
     public void setGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
         if (!mGestureFloatingPreviewText.isPreviewEnabled()) return;
         mGestureFloatingPreviewText.setSuggetedWords(suggestedWords);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java b/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
index 1f52520..cd6efc4 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
@@ -15,6 +15,7 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.graphics.Path;
+import android.graphics.Rect;
 import android.graphics.RectF;
 
 public final class RoundedLine {
@@ -100,4 +101,10 @@
         mPath.close();
         return mPath;
     }
+
+    public void getBounds(final Rect outBounds) {
+        // Reuse mArc1 as working variable
+        mPath.computeBounds(mArc1, true /* unused */);
+        mArc1.roundOut(outBounds);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
index 55414b8..e2eacb3 100644
--- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
@@ -105,7 +105,7 @@
         return true;
     }
 
-    public static int getActionIdFromEditorInfo(final EditorInfo editorInfo) {
+    public static int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) {
         final int actionId = editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
         if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
             return EditorInfo.IME_ACTION_NONE;
@@ -115,4 +115,9 @@
             return actionId;
         }
     }
+
+    public static int getConcreteActionIdFromEditorInfo(final EditorInfo editorInfo) {
+        final int actionId = getImeOptionsActionIdFromEditorInfo(editorInfo);
+        return actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL ? editorInfo.actionId : actionId;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 488a6fc..a4019e9 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -45,19 +45,21 @@
     public final String mCommittedWord;
     public final String mSeparatorString;
     public final String mPrevWord;
+    public final int mCapitalizedMode;
     public final InputPointers mInputPointers =
             new InputPointers(Constants.Dictionary.MAX_WORD_LENGTH);
 
     private boolean mActive;
 
     public static final LastComposedWord NOT_A_COMPOSED_WORD =
-            new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null);
+            new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null,
+            WordComposer.CAPS_MODE_OFF);
 
     // Warning: this is using the passed objects as is and fully expects them to be
     // immutable. Do not fiddle with their contents after you passed them to this constructor.
     public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers,
-            final String typedWord, final String committedWord,
-            final String separatorString, final String prevWord) {
+            final String typedWord, final String committedWord, final String separatorString,
+            final String prevWord, final int capitalizedMode) {
         mPrimaryKeyCodes = primaryKeyCodes;
         if (inputPointers != null) {
             mInputPointers.copy(inputPointers);
@@ -67,6 +69,7 @@
         mSeparatorString = separatorString;
         mActive = true;
         mPrevWord = prevWord;
+        mCapitalizedMode = capitalizedMode;
     }
 
     public void deactivate() {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index ab21cff..d02c4df 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -166,6 +166,7 @@
     private boolean mExpectingUpdateSelection;
     private int mDeleteCount;
     private long mLastKeyTime;
+    private int mActionId;
 
     // Member variables for remembering the current device orientation.
     private int mDisplayOrientation;
@@ -754,6 +755,7 @@
 
         mLastSelectionStart = editorInfo.initialSelStart;
         mLastSelectionEnd = editorInfo.initialSelEnd;
+        mActionId = InputTypeUtils.getConcreteActionIdFromEditorInfo(editorInfo);
 
         mHandler.cancelUpdateSuggestionStrip();
         mHandler.cancelDoubleSpacePeriodTimer();
@@ -1223,7 +1225,8 @@
         }
         mPositionalInfoForUserDictPendingAddition =
                 new PositionalInfoForUserDictPendingAddition(
-                        word, mLastSelectionEnd, getCurrentInputEditorInfo());
+                        word, mLastSelectionEnd, getCurrentInputEditorInfo(),
+                        mLastComposedWord.mCapitalizedMode);
         mUserDictionary.addWordToUserDictionary(word, 128);
     }
 
@@ -1272,10 +1275,6 @@
         return mOptionsDialog != null && mOptionsDialog.isShowing();
     }
 
-    private static int getActionId(final Keyboard keyboard) {
-        return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
-    }
-
     private void performEditorAction(final int actionId) {
         mConnection.performEditorAction(actionId);
     }
@@ -1384,10 +1383,9 @@
             }
             break;
         case Constants.CODE_ACTION_ENTER:
-            final int actionId = getActionId(switcher.getKeyboard());
-            if (EditorInfo.IME_ACTION_NONE != actionId
-                && EditorInfo.IME_ACTION_UNSPECIFIED != actionId) {
-                performEditorAction(actionId);
+            if (EditorInfo.IME_ACTION_NONE != mActionId
+                && EditorInfo.IME_ACTION_UNSPECIFIED != mActionId) {
+                performEditorAction(mActionId);
                 break;
             }
             didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
@@ -1496,12 +1494,7 @@
             mSpaceState = SPACE_STATE_PHANTOM;
         } else {
             final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
-            // TODO: reverse this logic. We should have the means to determine whether a character
-            // should usually be followed by a space, and it should be more readable.
-            if (Constants.NOT_A_CODE != codePointBeforeCursor
-                    && !Character.isWhitespace(codePointBeforeCursor)
-                    && !mSettings.getCurrent().isPhantomSpacePromotingSymbol(codePointBeforeCursor)
-                    && !mSettings.getCurrent().isWeakSpaceStripper(codePointBeforeCursor)) {
+            if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
                 mSpaceState = SPACE_STATE_PHANTOM;
             }
         }
@@ -1778,25 +1771,22 @@
         }
     }
 
+    /*
+     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
+     */
     private boolean maybeStripSpace(final int code,
             final int spaceState, final boolean isFromSuggestionStrip) {
         if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
             mConnection.removeTrailingSpace();
             return false;
-        } else if ((SPACE_STATE_WEAK == spaceState
-                || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
-                && isFromSuggestionStrip) {
-            if (mSettings.getCurrent().isWeakSpaceSwapper(code)) {
-                return true;
-            } else {
-                if (mSettings.getCurrent().isWeakSpaceStripper(code)) {
-                    mConnection.removeTrailingSpace();
-                }
-                return false;
-            }
-        } else {
-            return false;
         }
+        if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
+                && isFromSuggestionStrip) {
+            if (mSettings.getCurrent().isUsuallyPrecededBySpace(code)) return false;
+            if (mSettings.getCurrent().isUsuallyFollowedBySpace(code)) return true;
+            mConnection.removeTrailingSpace();
+        }
+        return false;
     }
 
     private void handleCharacter(final int primaryCode, final int x,
@@ -1804,7 +1794,7 @@
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                !mSettings.getCurrent().isSymbolExcludedFromWordSeparators(primaryCode)) {
+                !mSettings.getCurrent().isWordConnector(primaryCode)) {
             if (isComposingWord) {
                 // Sanity check
                 throw new RuntimeException("Should not be composing here");
@@ -1816,7 +1806,7 @@
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
         if (!isComposingWord && (isAlphabet(primaryCode)
-                || mSettings.getCurrent().isSymbolExcludedFromWordSeparators(primaryCode))
+                || mSettings.getCurrent().isWordConnector(primaryCode))
                 && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) &&
                 !mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
             // Reset entirely the composing state anyway, then start composing a new word unless
@@ -1870,7 +1860,7 @@
     private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_handleSeparator();
+            ResearchLogger.recordTimeForLogUnitSplit();
         }
         boolean didAutoCorrect = false;
         // Handle separator
@@ -1888,7 +1878,7 @@
                 Constants.SUGGESTION_STRIP_COORDINATE == x);
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                mSettings.getCurrent().isPhantomSpacePromotingSymbol(primaryCode)) {
+                mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) {
             promotePhantomSpace();
         }
         sendKeyCodePoint(primaryCode);
@@ -1903,16 +1893,13 @@
             }
 
             mHandler.startDoubleSpacePeriodTimer();
-            if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
-                mHandler.postUpdateSuggestionStrip();
-            }
+            mHandler.postUpdateSuggestionStrip();
         } else {
             if (swapWeakSpace) {
                 swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
             } else if (SPACE_STATE_PHANTOM == spaceState
-                    && !mSettings.getCurrent().isWeakSpaceStripper(primaryCode)
-                    && !mSettings.getCurrent().isPhantomSpacePromotingSymbol(primaryCode)) {
+                    && mSettings.getCurrent().isUsuallyFollowedBySpace(primaryCode)) {
                 // If we are in phantom space state, and the user presses a separator, we want to
                 // stay in phantom space state so that the next keypress has a chance to add the
                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
@@ -2161,9 +2148,9 @@
                 // In the batch input mode, a manually picked suggested word should just replace
                 // the current batch input text and there is no need for a phantom space.
                 && !mWordComposer.isBatchMode()) {
-            int firstChar = Character.codePointAt(suggestion, 0);
-            if ((!mSettings.getCurrent().isWeakSpaceStripper(firstChar))
-                    && (!mSettings.getCurrent().isWeakSpaceSwapper(firstChar))) {
+            final int firstChar = Character.codePointAt(suggestion, 0);
+            if (!mSettings.getCurrent().isWordSeparator(firstChar)
+                    || mSettings.getCurrent().isUsuallyPrecededBySpace(firstChar)) {
                 promotePhantomSpace();
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java
index 1fd2563..a33cefc 100644
--- a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java
+++ b/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java
@@ -33,13 +33,15 @@
     final private String mOriginalWord;
     final private int mCursorPos; // Position of the cursor after the word
     final private EditorInfo mEditorInfo; // On what binding this has been added
+    final private int mCapitalizedMode;
     private String mActualWordBeingAdded;
 
     public PositionalInfoForUserDictPendingAddition(final String word, final int cursorPos,
-            final EditorInfo editorInfo) {
+            final EditorInfo editorInfo, final int capitalizedMode) {
         mOriginalWord = word;
         mCursorPos = cursorPos;
         mEditorInfo = editorInfo;
+        mCapitalizedMode = capitalizedMode;
     }
 
     public void setActualWordBeingAdded(final String actualWordBeingAdded) {
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index f7268fc..0e75533 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -577,11 +577,11 @@
         final CharSequence before = getTextBeforeCursor(1, 0);
         final CharSequence after = getTextAfterCursor(1, 0);
         if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
-                && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+                && !settingsValues.isWordConnector(before.charAt(0))) {
             return true;
         }
         if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
-                && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+                && !settingsValues.isWordConnector(after.charAt(0))) {
             return true;
         }
         return false;
@@ -633,12 +633,9 @@
         final char firstChar = word.charAt(0); // we just tested that word is not empty
         if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
 
-        // We only suggest on words that start with a letter or a symbol that is excluded from
-        // word separators (see #handleCharacterWhileInBatchEdit).
-        if (!(Character.isLetter(firstChar)
-                || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
-            return null;
-        }
+        // We don't restart suggestion if the first character is not a letter, because we don't
+        // start composing when the first character is not a letter.
+        if (!Character.isLetter(firstChar)) return null;
 
         return word;
     }
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialog.java b/java/src/com/android/inputmethod/latin/SeekBarDialog.java
index e576c09..c736d1b 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialog.java
+++ b/java/src/com/android/inputmethod/latin/SeekBarDialog.java
@@ -30,6 +30,8 @@
     public interface Listener {
         public void onPositiveButtonClick(final SeekBarDialog dialog);
         public void onNegativeButtonClick(final SeekBarDialog dialog);
+        public void onNeutralButtonClick(final SeekBarDialog dialog);
+        public void onDismiss(final SeekBarDialog dialog);
         public void onProgressChanged(final SeekBarDialog dialog);
         public void onStartTrackingTouch(final SeekBarDialog dialog);
         public void onStopTrackingTouch(final SeekBarDialog dialog);
@@ -39,7 +41,11 @@
         @Override
         public void onPositiveButtonClick(final SeekBarDialog dialog) {}
         @Override
-        public void onNegativeButtonClick(final SeekBarDialog dialog) { dialog.dismiss(); }
+        public void onNegativeButtonClick(final SeekBarDialog dialog) {}
+        @Override
+        public void onNeutralButtonClick(final SeekBarDialog dialog) {}
+        @Override
+        public void onDismiss(final SeekBarDialog dialog) {}
         @Override
         public void onProgressChanged(final SeekBarDialog dialog) {}
         @Override
@@ -63,6 +69,9 @@
         dialogBuilder.setView(builder.mView);
         dialogBuilder.setPositiveButton(android.R.string.ok, this);
         dialogBuilder.setNegativeButton(android.R.string.cancel, this);
+        if (builder.mNeutralButtonTextResId != 0) {
+            dialogBuilder.setNeutralButton(builder.mNeutralButtonTextResId, this);
+        }
         mDialog = dialogBuilder.create();
         mListener = (builder.mListener == null) ? EMPTY_ADAPTER : builder.mListener;
         mValueView = (TextView)builder.mView.findViewById(R.id.seek_bar_dialog_value);
@@ -101,15 +110,21 @@
     }
 
     @Override
-    public void onClick(final DialogInterface dialog, int which) {
-        if (which == DialogInterface.BUTTON_POSITIVE) {
+    public void onClick(final DialogInterface dialog, final int which) {
+        switch (which) {
+        case DialogInterface.BUTTON_POSITIVE:
             mListener.onPositiveButtonClick(this);
-            return;
-        }
-        if (which == DialogInterface.BUTTON_NEGATIVE) {
+            break;
+        case DialogInterface.BUTTON_NEGATIVE:
             mListener.onNegativeButtonClick(this);
+            break;
+        case DialogInterface.BUTTON_NEUTRAL:
+            mListener.onNeutralButtonClick(this);
+            break;
+        default:
             return;
         }
+        mListener.onDismiss(this);
     }
 
     @Override
@@ -135,6 +150,7 @@
         final AlertDialog.Builder mDialogBuilder;
         final View mView;
 
+        int mNeutralButtonTextResId;
         int mMaxValue;
         int mValueFormatResId;
         int mValue;
@@ -150,8 +166,14 @@
             return this;
         }
 
+        public Builder setNeutralButtonText(final int resId) {
+            mNeutralButtonTextResId = resId;
+            return this;
+        }
+
         public Builder setMaxValue(final int max) {
             mMaxValue = max;
+            mValue = Math.min(mValue, max);
             return this;
         }
 
@@ -161,7 +183,7 @@
         }
 
         public Builder setValue(final int value) {
-            mValue = value;
+            mValue = Math.min(value, mMaxValue);
             return this;
         }
 
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index c5930a9..866bef0 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -167,19 +167,21 @@
     public static float readKeypressSoundVolume(final SharedPreferences prefs,
             final Resources res) {
         final float volume = prefs.getFloat(PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
-        if (volume >= 0) {
-            return volume;
-        }
+        return (volume >= 0) ? volume : readDefaultKeypressSoundVolume(res);
+    }
+
+    public static float readDefaultKeypressSoundVolume(final Resources res) {
         return Float.parseFloat(
                 ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_volumes));
     }
 
-    public static int readVibrationDuration(final SharedPreferences prefs,
+    public static int readKeypressVibrationDuration(final SharedPreferences prefs,
             final Resources res) {
         final int ms = prefs.getInt(PREF_VIBRATION_DURATION_SETTINGS, -1);
-        if (ms >= 0) {
-            return ms;
-        }
+        return (ms >= 0) ? ms : readDefaultKeypressVibrationDuration(res);
+    }
+
+    public static int readDefaultKeypressVibrationDuration(final Resources res) {
         return Integer.parseInt(
                 ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations));
     }
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 507a37b..6a43718 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -180,7 +180,7 @@
                     });
             mKeypressVibrationDurationSettingsPref.setSummary(
                     res.getString(R.string.settings_keypress_vibration_duration,
-                            Settings.readVibrationDuration(prefs, res)));
+                            Settings.readKeypressVibrationDuration(prefs, res)));
         }
 
         mKeypressSoundVolumeSettingsPref =
@@ -308,10 +308,29 @@
         final Context context = getActivity();
         final PreferenceScreen settingsPref = mKeypressVibrationDurationSettingsPref;
         final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
+            private void writePreference(final SharedPreferences sp, final int value) {
+                sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, value).apply();
+            }
+
+            private void feedbackSettingsValue(final int value) {
+                AudioAndHapticFeedbackManager.getInstance().vibrate(value);
+            }
+
             @Override
             public void onPositiveButtonClick(final SeekBarDialog dialog) {
-                final int ms = dialog.getValue();
-                sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, ms).apply();
+                writePreference(sp, dialog.getValue());
+            }
+
+            @Override
+            public void onNeutralButtonClick(final SeekBarDialog dialog) {
+                final int defaultValue =
+                        Settings.readDefaultKeypressVibrationDuration(context.getResources());
+                dialog.setValue(defaultValue, false /* fromUser */);
+                writePreference(sp, defaultValue);
+            }
+
+            @Override
+            public void onDismiss(final SeekBarDialog dialog) {
                 if (settingsPref != null) {
                     settingsPref.setSummary(dialog.getValueText());
                 }
@@ -319,13 +338,13 @@
 
             @Override
             public void onStopTrackingTouch(final SeekBarDialog dialog) {
-                final int ms = dialog.getValue();
-                AudioAndHapticFeedbackManager.getInstance().vibrate(ms);
+                feedbackSettingsValue(dialog.getValue());
             }
         };
-        final int currentMs = Settings.readVibrationDuration(sp, getResources());
+        final int currentMs = Settings.readKeypressVibrationDuration(sp, getResources());
         final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
         builder.setTitle(R.string.prefs_keypress_vibration_duration_settings)
+                .setNeutralButtonText(R.string.button_default)
                 .setListener(listener)
                 .setMaxValue(AudioAndHapticFeedbackManager.MAX_KEYPRESS_VIBRATION_DURATION)
                 .setValueFromat(R.string.settings_keypress_vibration_duration)
@@ -348,10 +367,29 @@
         final AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         final PreferenceScreen settingsPref = mKeypressSoundVolumeSettingsPref;
         final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
+            private void writePreference(final SharedPreferences sp, final float value) {
+                sp.edit().putFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, value).apply();
+            }
+
+            private void feedbackSettingsValue(final float value) {
+                am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, value);
+            }
+
             @Override
             public void onPositiveButtonClick(final SeekBarDialog dialog) {
-                final float volume = dialog.getValue() / PERCENT_FLOAT;
-                sp.edit().putFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, volume).apply();
+                writePreference(sp, dialog.getValue() / PERCENT_FLOAT);
+            }
+
+            @Override
+            public void onNeutralButtonClick(final SeekBarDialog dialog) {
+                final float defaultValue =
+                        Settings.readDefaultKeypressSoundVolume(context.getResources());
+                dialog.setValue((int)(defaultValue * PERCENT_INT), false /* fromUser */);
+                writePreference(sp, defaultValue);
+            }
+
+            @Override
+            public void onDismiss(final SeekBarDialog dialog) {
                 if (settingsPref != null) {
                     settingsPref.setSummary(dialog.getValueText());
                 }
@@ -359,13 +397,13 @@
 
             @Override
             public void onStopTrackingTouch(final SeekBarDialog dialog) {
-                final float volume = dialog.getValue() / PERCENT_FLOAT;
-                am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, volume);
+                feedbackSettingsValue(dialog.getValue() / PERCENT_FLOAT);
             }
         };
         final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
         final int currentVolumeInt = getCurrentKeyPressSoundVolumePercent(sp, getResources());
         builder.setTitle(R.string.prefs_keypress_sound_volume_settings)
+                .setNeutralButtonText(R.string.button_default)
                 .setListener(listener)
                 .setMaxValue(PERCENT_INT)
                 .setValue(currentVolumeInt)
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 9a20246..1e3bdf0 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -37,11 +37,10 @@
 
     // From resources:
     public final int mDelayUpdateOldSuggestions;
-    public final String mWeakSpaceStrippers;
-    public final String mWeakSpaceSwappers;
-    private final String mPhantomSpacePromotingSymbols;
+    public final int[] mSymbolsPrecededBySpace;
+    public final int[] mSymbolsFollowedBySpace;
+    public final int[] mWordConnectors;
     public final SuggestedWords mSuggestPuncList;
-    private final String mSymbolsExcludedFromWordSeparators;
     public final String mWordSeparators;
     public final CharSequence mHintToSaveText;
 
@@ -79,25 +78,19 @@
             final InputAttributes inputAttributes) {
         // Get the resources
         mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions);
-        mWeakSpaceStrippers = res.getString(R.string.weak_space_stripping_symbols);
-        mWeakSpaceSwappers = res.getString(R.string.weak_space_swapping_symbols);
-        mPhantomSpacePromotingSymbols = res.getString(R.string.phantom_space_promoting_symbols);
-        if (LatinImeLogger.sDBG) {
-            final int length = mWeakSpaceStrippers.length();
-            for (int i = 0; i < length; i = mWeakSpaceStrippers.offsetByCodePoints(i, 1)) {
-                if (isWeakSpaceSwapper(mWeakSpaceStrippers.codePointAt(i))) {
-                    throw new RuntimeException("Char code " + mWeakSpaceStrippers.codePointAt(i)
-                            + " is both a weak space swapper and stripper.");
-                }
-            }
-        }
+        mSymbolsPrecededBySpace =
+                StringUtils.toCodePointArray(res.getString(R.string.symbols_preceded_by_space));
+        Arrays.sort(mSymbolsPrecededBySpace);
+        mSymbolsFollowedBySpace =
+                StringUtils.toCodePointArray(res.getString(R.string.symbols_followed_by_space));
+        Arrays.sort(mSymbolsFollowedBySpace);
+        mWordConnectors =
+                StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors));
+        Arrays.sort(mWordConnectors);
         final String[] suggestPuncsSpec = KeySpecParser.parseCsvString(
                 res.getString(R.string.suggested_punctuations), null);
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
-        mSymbolsExcludedFromWordSeparators =
-                res.getString(R.string.symbols_excluded_from_word_separators);
-        mWordSeparators = createWordSeparators(mWeakSpaceStrippers, mWeakSpaceSwappers,
-                mSymbolsExcludedFromWordSeparators, res);
+        mWordSeparators = res.getString(R.string.symbols_word_separators);
         mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
 
         // Store the input attributes
@@ -128,7 +121,7 @@
         mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
 
         // Compute other readable settings
-        mKeypressVibrationDuration = Settings.readVibrationDuration(prefs, res);
+        mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res);
         mKeypressSoundVolume = Settings.readKeypressSoundVolume(prefs, res);
         mKeyPreviewPopupDismissDelay = Settings.readKeyPreviewPopupDismissDelay(prefs, res);
         mAutoCorrectionThreshold = readAutoCorrectionThreshold(res,
@@ -169,25 +162,16 @@
         return mWordSeparators.contains(String.valueOf((char)code));
     }
 
-    public boolean isSymbolExcludedFromWordSeparators(final int code) {
-        return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code));
+    public boolean isWordConnector(final int code) {
+        return Arrays.binarySearch(mWordConnectors, code) >= 0;
     }
 
-    // TODO: use "Phantom" instead of "Weak" in this method name
-    public boolean isWeakSpaceStripper(final int code) {
-        // TODO: this does not work if the code does not fit in a char
-        return mWeakSpaceStrippers.contains(String.valueOf((char)code));
+    public boolean isUsuallyPrecededBySpace(final int code) {
+        return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0;
     }
 
-    // TODO: use "Phantom" instead of "Weak" in this method name
-    public boolean isWeakSpaceSwapper(final int code) {
-        // TODO: this does not work if the code does not fit in a char
-        return mWeakSpaceSwappers.contains(String.valueOf((char)code));
-    }
-
-    public boolean isPhantomSpacePromotingSymbol(final int code) {
-        // TODO: this does not work if the code does not fit in a char
-        return mPhantomSpacePromotingSymbols.contains(String.valueOf((char)code));
+    public boolean isUsuallyFollowedBySpace(final int code) {
+        return Arrays.binarySearch(mSymbolsFollowedBySpace, code) >= 0;
     }
 
     public boolean shouldInsertSpacesAutomatically() {
@@ -239,18 +223,6 @@
                 false /* isPrediction */);
     }
 
-    private static String createWordSeparators(final String weakSpaceStrippers,
-            final String weakSpaceSwappers, final String symbolsExcludedFromWordSeparators,
-            final Resources res) {
-        String wordSeparators = weakSpaceStrippers + weakSpaceSwappers
-                + res.getString(R.string.phantom_space_promoting_symbols);
-        for (int i = symbolsExcludedFromWordSeparators.length() - 1; i >= 0; --i) {
-            wordSeparators = wordSeparators.replace(
-                    symbolsExcludedFromWordSeparators.substring(i, i + 1), "");
-        }
-        return wordSeparators;
-    }
-
     private static final int SUGGESTION_VISIBILITY_SHOW_VALUE =
             R.string.prefs_suggestion_visibility_show_value;
     private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE =
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 4f17590..b9ec497 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -350,7 +350,7 @@
         mPrimaryKeyCodes = new int[MAX_WORD_LENGTH];
         final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
                 mInputPointers, mTypedWord.toString(), committedWord, separatorString,
-                prevWord);
+                prevWord, mCapitalizedMode);
         mInputPointers.reset();
         if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
                 && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
@@ -374,6 +374,7 @@
         mTypedWord.setLength(0);
         mTypedWord.append(lastComposedWord.mTypedWord);
         refreshSize();
+        mCapitalizedMode = lastComposedWord.mCapitalizedMode;
         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
         mIsResumed = true;
     }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 8b6bff4..a2bcf44 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -106,7 +106,8 @@
     // Change the default indicator to something very visible.  Currently two red vertical bars on
     // either side of they keyboard.
     private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || IS_LOGGING_EVERYTHING;
-    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
+    // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself.
+    public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1;
 
     // constants related to specific log points
     private static final String WHITESPACE_SEPARATORS = " \t\n\r";
@@ -391,9 +392,7 @@
         }
         if (mFeedbackLogBuffer == null) {
             mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
-            // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
-            // the feedback LogUnit itself.
-            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
+            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
         }
     }
 
@@ -522,8 +521,25 @@
     */
 
     private boolean mInFeedbackDialog = false;
+
+    // The feedback dialog causes stop() to be called for the keyboard connected to the original
+    // window.  This is because the feedback dialog must present its own EditText box that displays
+    // a keyboard.  stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
+    // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
+    // wants to upload, to be closed.  This is good because we don't need to log information about
+    // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
+    // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
+    private LogBuffer mSavedFeedbackLogBuffer;
+    private ResearchLog mSavedFeedbackLog;
+
     public void presentFeedbackDialog(LatinIME latinIME) {
         mInFeedbackDialog = true;
+        mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
+        mSavedFeedbackLog = mFeedbackLog;
+        // Set the non-saved versions to null so that the stop() caused by switching to the
+        // Feedback dialog will not close them.
+        mFeedbackLogBuffer = null;
+        mFeedbackLog = null;
         latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
     }
 
@@ -589,28 +605,25 @@
     }
 
     private static final LogStatement LOGSTATEMENT_FEEDBACK =
-            new LogStatement("UserTimestamp", false, false, "contents");
+            new LogStatement("UserFeedback", false, false, "contents");
     public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
-        if (mFeedbackLogBuffer == null) {
+        if (mSavedFeedbackLogBuffer == null) {
             return;
         }
-        if (includeHistory) {
-            commitCurrentLogUnit();
-        } else {
-            mFeedbackLogBuffer.clear();
+        if (!includeHistory) {
+            mSavedFeedbackLogBuffer.clear();
         }
         final LogUnit feedbackLogUnit = new LogUnit();
         feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
                 feedbackContents);
         mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
-        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
-        mFeedbackLog.close(new Runnable() {
+        publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
+        mSavedFeedbackLog.close(new Runnable() {
             @Override
             public void run() {
                 uploadNow();
             }
         });
-        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
     }
 
     public void uploadNow() {
@@ -643,13 +656,6 @@
     }
 
     private boolean isAllowedToLog() {
-        if (DEBUG) {
-            Log.d(TAG, "iatl: " +
-                "mipw=" + mIsPasswordView +
-                ", mils=" + mIsLoggingSuspended +
-                ", sil=" + sIsLogging +
-                ", mInFeedbackDialog=" + mInFeedbackDialog);
-        }
         return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
     }
 
@@ -852,6 +858,11 @@
         mCurrentLogUnit = newLogUnit;
     }
 
+    /**
+     * Record the time of a MotionEvent.ACTION_DOWN.
+     *
+     * Warning: Not thread safe.  Only call from the main thread.
+     */
     private void setSavedDownEventTime(final long time) {
         mSavedDownEventTime = time;
     }
@@ -1475,20 +1486,20 @@
 
     private boolean isExpectingCommitText = false;
     /**
-     * Log a call to RichInputConnection.commitPartialText
+     * Log a call to (UnknownClass).commitPartialText
      *
      * SystemResponse: The IME is committing part of a word.  This happens if a space is
      * automatically inserted to split a single typed string into two or more words.
      */
     // TODO: This method is currently unused.  Find where it should be called from in the IME and
     // add invocations.
-    private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT =
-            new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition");
-    public static void latinIME_commitPartialText(final String committedWord,
+    private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT =
+            new LogStatement("CommitPartialText", true, false, "newCursorPosition");
+    public static void commitPartialText(final String committedWord,
             final long lastTimestampOfWordData, final boolean isBatchMode) {
         final ResearchLogger researchLogger = getInstance();
         final String scrubbedWord = scrubDigitsFromString(committedWord);
-        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT);
+        researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
         researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
                 isBatchMode);
     }
@@ -1705,12 +1716,16 @@
         researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval);
     }
 
-    public static void latinIME_handleSeparator() {
-        // Reset the saved down event time.  For tapping, motion events, etc. before the separator
-        // are assigned to the previous LogUnit, and events after the separator are assigned to the
-        // next LogUnit.  In the case of multitap, this might capture down events corresponding to
-        // the next word, however it should not be more than a character or two.
-        getInstance().setSavedDownEventTime(SystemClock.uptimeMillis());
+    /**
+     * Record the current time in case the LogUnit is later split.
+     *
+     * If the current logUnitis split, then tapping, motion events, etc. before this time should
+     * be assigned to one LogUnit, and events after this time should go into the following LogUnit.
+     */
+    public static void recordTimeForLogUnitSplit() {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis());
+        researchLogger.mSavedDownEventTime = Long.MAX_VALUE;
     }
 
     /**
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index c89a870..4d5a2b2 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -316,7 +316,7 @@
 #define TWO_WORDS_CAPITALIZED_DEMOTION_RATE 50
 #define TWO_WORDS_CORRECTION_DEMOTION_BASE 80
 #define TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER 1
-#define ZERO_DISTANCE_PROMOTION_RATE 110
+#define ZERO_DISTANCE_PROMOTION_RATE 110.0f
 #define NEUTRAL_SCORE_SQUARED_RADIUS 8.0f
 #define HALF_SCORE_SQUARED_RADIUS 32.0f
 #define MAX_FREQ 255
diff --git a/native/jni/src/suggest_utils.h b/native/jni/src/suggest_utils.h
index a3b3c12..42cc5de 100644
--- a/native/jni/src/suggest_utils.h
+++ b/native/jni/src/suggest_utils.h
@@ -18,11 +18,12 @@
 #define LATINIME_SUGGEST_UTILS_H
 
 #include "defines.h"
+#include "proximity_info_state.h"
 
 namespace latinime {
 class SuggestUtils {
  public:
-    static float getDistanceScalingFactor(float normalizedSquaredDistance) {
+    static float getDistanceScalingFactor(const float normalizedSquaredDistance) {
         if (normalizedSquaredDistance < 0.0f) {
             return -1.0f;
         }
@@ -33,8 +34,8 @@
         static const float MIN = 0.3f;
         static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS;
         static const float R2 = HALF_SCORE_SQUARED_RADIUS;
-        const float x = static_cast<float>(normalizedSquaredDistance)
-                / ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
+        const float x = normalizedSquaredDistance / static_cast<float>(
+                ProximityInfoState::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR);
         const float factor = max((x < R1)
                 ? (A * (R1 - x) + B * x) / R1
                 : (B * (R2 - x) + C * (x - R1)) / (R2 - R1), MIN);
@@ -48,6 +49,9 @@
         // 0   R1 R2             .
         return factor;
     }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(SuggestUtils);
 };
 } // namespace latinime
 #endif // LATINIME_SUGGEST_UTILS_H