Merge "Import translations. DO NOT MERGE"
diff --git a/dictionaries/fr_wordlist.combined.gz b/dictionaries/fr_wordlist.combined.gz
index b7a4e35..ccc31c8 100644
--- a/dictionaries/fr_wordlist.combined.gz
+++ b/dictionaries/fr_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pl_wordlist.combined.gz b/dictionaries/pl_wordlist.combined.gz
index 12d5238..bf02298 100644
--- a/dictionaries/pl_wordlist.combined.gz
+++ b/dictionaries/pl_wordlist.combined.gz
Binary files differ
diff --git a/java/res/raw/main_fr.dict b/java/res/raw/main_fr.dict
index 5aed479..c2941b7 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 5151782..1088fda 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -66,7 +66,8 @@
         new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
     };
 
-    private AudioAndHapticFeedbackManager mFeedbackManager;
+    private final AudioAndHapticFeedbackManager mFeedbackManager =
+            AudioAndHapticFeedbackManager.getInstance();
     private SubtypeSwitcher mSubtypeSwitcher;
     private SharedPreferences mPrefs;
 
@@ -104,7 +105,6 @@
     private void initInternal(final LatinIME latinIme, final SharedPreferences prefs) {
         mLatinIME = latinIme;
         mResources = latinIme.getResources();
-        mFeedbackManager = new AudioAndHapticFeedbackManager(latinIme);
         mPrefs = prefs;
         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
         mState = new KeyboardState(this);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 61d3874..b7bee34 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -50,6 +50,7 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.research.ResearchLogger;
 
@@ -870,9 +871,9 @@
         mPreviewPlacerView.dismissSlidingKeyInputPreview();
     }
 
-    public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
+    public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
         locatePreviewPlacerView();
-        mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText);
+        mPreviewPlacerView.setGestureFloatingPreviewText(suggestedWords);
     }
 
     public void dismissGestureFloatingPreviewText() {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java
new file mode 100644
index 0000000..8a3f064
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.graphics.Canvas;
+
+import com.android.inputmethod.keyboard.PointerTracker;
+
+/**
+ * Abstract base class for previews that are drawn on PreviewPlacerView, e.g.,
+ * GestureFloatingPrevewText, GestureTrail, and SlidingKeyInputPreview.
+ */
+public abstract class AbstractDrawingPreview {
+    private boolean mPreviewEnabled;
+
+    public void setPreviewEnabled(final boolean enabled) {
+        mPreviewEnabled = enabled;
+    }
+
+    public boolean isPreviewEnabled() {
+        return mPreviewEnabled;
+    }
+
+    /**
+     * Draws the preview
+     * @param canvas The canvas where the preview is drawn.
+     */
+    public abstract void onDraw(final Canvas canvas);
+
+    /**
+     * Set the position of the preview.
+     * @param pt The new location of the preview is based on the points in PointerTracker pt.
+     */
+    public abstract void setPreviewPosition(final PointerTracker pt);
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java
new file mode 100644
index 0000000..84cfb38
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.CoordinateUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResizableIntArray;
+import com.android.inputmethod.latin.SuggestedWords;
+
+/**
+ * The class for single gesture preview text. The class for multiple gesture preview text will be
+ * derived from it.
+ */
+public class GestureFloatingPreviewText extends AbstractDrawingPreview {
+    private static final class GesturePreviewTextParams {
+        public final int mGesturePreviewTextSize;
+        public final int mGesturePreviewTextColor;
+        public final int mGesturePreviewTextDimmedColor;
+        public final int mGesturePreviewTextOffset;
+        public final int mGesturePreviewTextHeight;
+        public final int mGesturePreviewColor;
+        public final float mGesturePreviewHorizontalPadding;
+        public final float mGesturePreviewVerticalPadding;
+        public final float mGesturePreviewRoundRadius;
+        public final Paint mTextPaint;
+
+        private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' };
+
+        public GesturePreviewTextParams(final TypedArray keyboardViewAttr) {
+            mGesturePreviewTextSize = keyboardViewAttr.getDimensionPixelSize(
+                    R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0);
+            mGesturePreviewTextColor = keyboardViewAttr.getColor(
+                    R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0);
+            mGesturePreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset(
+                    R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0);
+            mGesturePreviewColor = keyboardViewAttr.getColor(
+                    R.styleable.KeyboardView_gestureFloatingPreviewColor, 0);
+            mGesturePreviewHorizontalPadding = keyboardViewAttr.getDimension(
+                    R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
+            mGesturePreviewVerticalPadding = keyboardViewAttr.getDimension(
+                    R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f);
+            mGesturePreviewRoundRadius = keyboardViewAttr.getDimension(
+                    R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f);
+            mGesturePreviewTextDimmedColor = Color.GRAY;
+
+            final Paint textPaint = new Paint();
+            textPaint.setAntiAlias(true);
+            textPaint.setTextAlign(Align.CENTER);
+            textPaint.setTextSize(mGesturePreviewTextSize);
+            mTextPaint = textPaint;
+            final Rect textRect = new Rect();
+            textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect);
+            mGesturePreviewTextHeight = textRect.height();
+        }
+    }
+
+    protected final GesturePreviewTextParams mParams;
+    protected int mPreviewWordNum;
+    protected final RectF mGesturePreviewRectangle = new RectF();
+    protected int mHighlightedWordIndex;
+
+    private static final int PREVIEW_TEXT_ARRAY_CAPACITY = 10;
+    // These variables store the positions of preview words. In multi-preview mode, the gesture
+    // floating preview at most shows PREVIEW_TEXT_ARRAY_CAPACITY words.
+    protected final ResizableIntArray mPreviewTextXArray = new ResizableIntArray(
+            PREVIEW_TEXT_ARRAY_CAPACITY);
+    protected final ResizableIntArray mPreviewTextYArray = new ResizableIntArray(
+            PREVIEW_TEXT_ARRAY_CAPACITY);
+
+    protected SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+    protected final Context mContext;
+    public final int[] mLastPointerCoords = CoordinateUtils.newInstance();
+
+    public GestureFloatingPreviewText(final TypedArray typedArray, final Context context) {
+        mParams = new GesturePreviewTextParams(typedArray);
+        mHighlightedWordIndex = 0;
+        mContext = context;
+    }
+
+    public void setSuggetedWords(final SuggestedWords suggestedWords) {
+        if (suggestedWords == null) {
+            mSuggestedWords = SuggestedWords.EMPTY;
+        } else {
+            mSuggestedWords = suggestedWords;
+        }
+        updatePreviewPosition();
+    }
+
+    protected void drawText(final Canvas canvas, final String text, final float textX,
+            final float textY, final int color) {
+        final Paint paint = mParams.mTextPaint;
+        paint.setColor(color);
+        canvas.drawText(text, textX, textY, paint);
+    }
+
+    @Override
+    public void setPreviewPosition(final PointerTracker pt) {
+        pt.getLastCoordinates(mLastPointerCoords);
+        updatePreviewPosition();
+    }
+
+    /**
+     * Draws gesture preview text
+     * @param canvas The canvas where preview text is drawn.
+     */
+    @Override
+    public void onDraw(final Canvas canvas) {
+        if (!isPreviewEnabled() || mSuggestedWords.isEmpty()
+                || TextUtils.isEmpty(mSuggestedWords.getWord(0))) {
+            return;
+        }
+        final Paint paint = mParams.mTextPaint;
+        paint.setColor(mParams.mGesturePreviewColor);
+        final float round = mParams.mGesturePreviewRoundRadius;
+        canvas.drawRoundRect(mGesturePreviewRectangle, round, round, paint);
+        final String text = mSuggestedWords.getWord(0);
+        final int textX = mPreviewTextXArray.get(0);
+        final int textY = mPreviewTextYArray.get(0);
+        drawText(canvas, text, textX, textY, mParams.mGesturePreviewTextColor);
+    }
+
+    /**
+     * Updates gesture preview text position based on mLastPointerCoords.
+     */
+    protected void updatePreviewPosition() {
+        if (mSuggestedWords.isEmpty() || TextUtils.isEmpty(mSuggestedWords.getWord(0))) {
+            return;
+        }
+        final String text = mSuggestedWords.getWord(0);
+
+        final Paint paint = mParams.mTextPaint;
+        final RectF rectangle = mGesturePreviewRectangle;
+
+        final int textHeight = mParams.mGesturePreviewTextHeight;
+        final float textWidth = paint.measureText(text);
+        final float hPad = mParams.mGesturePreviewHorizontalPadding;
+        final float vPad = mParams.mGesturePreviewVerticalPadding;
+        final float rectWidth = textWidth + hPad * 2.0f;
+        final float rectHeight = textHeight + vPad * 2.0f;
+
+        final int displayWidth = mContext.getResources().getDisplayMetrics().widthPixels;
+        final float rectX = Math.min(
+                Math.max(CoordinateUtils.x(mLastPointerCoords) - rectWidth / 2.0f, 0.0f),
+                displayWidth - rectWidth);
+        final float rectY = CoordinateUtils.y(mLastPointerCoords)
+                - mParams.mGesturePreviewTextOffset - rectHeight;
+        rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
+
+        final int textX = (int)(rectX + hPad + textWidth / 2.0f);
+        final int textY = (int)(rectY + vPad) + textHeight;
+        mPreviewTextXArray.add(0, textX);
+        mPreviewTextYArray.add(0, textY);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
index bc734b0..a005dc9 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -22,13 +22,10 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
-import android.graphics.Paint.Align;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.os.Message;
-import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.widget.RelativeLayout;
@@ -39,15 +36,9 @@
 import com.android.inputmethod.latin.CoordinateUtils;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.SuggestedWords;
 
 public final class PreviewPlacerView extends RelativeLayout {
-    private final int mGestureFloatingPreviewTextColor;
-    private final int mGestureFloatingPreviewTextOffset;
-    private final int mGestureFloatingPreviewColor;
-    private final float mGestureFloatingPreviewHorizontalPadding;
-    private final float mGestureFloatingPreviewVerticalPadding;
-    private final float mGestureFloatingPreviewRoundRadius;
-
     private final int[] mKeyboardViewOrigin = CoordinateUtils.newInstance();
 
     private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
@@ -62,16 +53,7 @@
     private final Canvas mOffscreenCanvas = new Canvas();
     private final Rect mOffscreenDirtyRect = new Rect();
     private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
-
-    private final Paint mTextPaint;
-    private String mGestureFloatingPreviewText;
-    private final int mGestureFloatingPreviewTextHeight;
-    // {@link RectF} is needed for {@link Canvas#drawRoundRect(RectF, float, float, Paint)}.
-    private final RectF mGestureFloatingPreviewRectangle = new RectF();
-    private final int[] mLastPointerCoords = CoordinateUtils.newInstance();
-    private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' };
-    private boolean mDrawsGestureFloatingPreviewText;
-
+    private final GestureFloatingPreviewText mGestureFloatingPreviewText;
     private boolean mShowSlidingKeyInputPreview;
     private final int[] mRubberBandFrom = CoordinateUtils.newInstance();
     private final int[] mRubberBandTo = CoordinateUtils.newInstance();
@@ -130,22 +112,11 @@
 
         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(
                 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
-        final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize(
-                R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0);
-        mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor(
-                R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0);
-        mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset(
-                R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0);
-        mGestureFloatingPreviewColor = keyboardViewAttr.getColor(
-                R.styleable.KeyboardView_gestureFloatingPreviewColor, 0);
-        mGestureFloatingPreviewHorizontalPadding = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
-        mGestureFloatingPreviewVerticalPadding = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f);
-        mGestureFloatingPreviewRoundRadius = keyboardViewAttr.getDimension(
-                R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f);
         final int gestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt(
                 R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 0);
+        // TODO: mGestureFloatingPreviewText could be an instance of GestureFloatingPreviewText or
+        // MultiGesturePreviewText, depending on the user's choice in the settings.
+        mGestureFloatingPreviewText = new GestureFloatingPreviewText(keyboardViewAttr, context);
         mGesturePreviewTrailParams = new Params(keyboardViewAttr);
         keyboardViewAttr.recycle();
 
@@ -157,15 +128,6 @@
         gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
         mGesturePaint = gesturePaint;
 
-        final Paint textPaint = new Paint();
-        textPaint.setAntiAlias(true);
-        textPaint.setTextAlign(Align.CENTER);
-        textPaint.setTextSize(gestureFloatingPreviewTextSize);
-        mTextPaint = textPaint;
-        final Rect textRect = new Rect();
-        textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect);
-        mGestureFloatingPreviewTextHeight = textRect.height();
-
         final Paint layerPaint = new Paint();
         layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
         setLayerType(LAYER_TYPE_HARDWARE, layerPaint);
@@ -181,14 +143,14 @@
     public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
             final boolean drawsGestureFloatingPreviewText) {
         mDrawsGesturePreviewTrail = drawsGesturePreviewTrail;
-        mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText;
+        mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
     }
 
     public void invalidatePointer(final PointerTracker tracker, final boolean isOldestTracker) {
         final boolean needsToUpdateLastPointer =
-                isOldestTracker && mDrawsGestureFloatingPreviewText;
+                isOldestTracker && mGestureFloatingPreviewText.isPreviewEnabled();
         if (needsToUpdateLastPointer) {
-            tracker.getLastCoordinates(mLastPointerCoords);
+            mGestureFloatingPreviewText.setPreviewPosition(tracker);
         }
 
         if (mDrawsGesturePreviewTrail) {
@@ -252,6 +214,7 @@
         super.onDraw(canvas);
         final int originX = CoordinateUtils.x(mKeyboardViewOrigin);
         final int originY = CoordinateUtils.y(mKeyboardViewOrigin);
+        canvas.translate(originX, originY);
         if (mDrawsGesturePreviewTrail) {
             mayAllocateOffscreenBuffer();
             // Draw gesture trails to offscreen buffer.
@@ -259,11 +222,10 @@
                     mOffscreenCanvas, mGesturePaint, mOffscreenDirtyRect);
             // Transfer offscreen buffer to screen.
             if (!mOffscreenDirtyRect.isEmpty()) {
-                final int offsetY = originY - mOffscreenOffsetY;
-                canvas.translate(originX, offsetY);
+                canvas.translate(0, - mOffscreenOffsetY);
                 canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect,
                         mGesturePaint);
-                canvas.translate(-originX, -offsetY);
+                canvas.translate(0, mOffscreenOffsetY);
                 // Note: Defer clearing the dirty rectangle here because we will get cleared
                 // rectangle on the canvas.
             }
@@ -271,16 +233,11 @@
                 mDrawingHandler.postUpdateGestureTrailPreview();
             }
         }
-        if (mDrawsGestureFloatingPreviewText) {
-            canvas.translate(originX, originY);
-            drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText);
-            canvas.translate(-originX, -originY);
-        }
+        mGestureFloatingPreviewText.onDraw(canvas);
         if (mShowSlidingKeyInputPreview) {
-            canvas.translate(originX, originY);
             drawSlidingKeyInputPreview(canvas);
-            canvas.translate(-originX, -originY);
         }
+        canvas.translate(-originX, -originY);
     }
 
     private boolean drawGestureTrails(final Canvas offscreenCanvas, final Paint paint,
@@ -322,9 +279,9 @@
                 Math.min(out.bottom, bottom));
     }
 
-    public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
-        if (!mDrawsGestureFloatingPreviewText) return;
-        mGestureFloatingPreviewText = gestureFloatingPreviewText;
+    public void setGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
+        if (!mGestureFloatingPreviewText.isPreviewEnabled()) return;
+        mGestureFloatingPreviewText.setSuggetedWords(suggestedWords);
         invalidate();
     }
 
@@ -332,39 +289,6 @@
         mDrawingHandler.dismissGestureFloatingPreviewText();
     }
 
-    private void drawGestureFloatingPreviewText(final Canvas canvas,
-            final String gestureFloatingPreviewText) {
-        if (TextUtils.isEmpty(gestureFloatingPreviewText)) {
-            return;
-        }
-
-        final Paint paint = mTextPaint;
-        final RectF rectangle = mGestureFloatingPreviewRectangle;
-
-        // Paint the round rectangle background.
-        final int textHeight = mGestureFloatingPreviewTextHeight;
-        final float textWidth = paint.measureText(gestureFloatingPreviewText);
-        final float hPad = mGestureFloatingPreviewHorizontalPadding;
-        final float vPad = mGestureFloatingPreviewVerticalPadding;
-        final float rectWidth = textWidth + hPad * 2.0f;
-        final float rectHeight = textHeight + vPad * 2.0f;
-        final int canvasWidth = canvas.getWidth();
-        final float rectX = Math.min(
-                Math.max(CoordinateUtils.x(mLastPointerCoords) - rectWidth / 2.0f, 0.0f),
-                canvasWidth - rectWidth);
-        final float rectY = CoordinateUtils.y(mLastPointerCoords)
-                - mGestureFloatingPreviewTextOffset - rectHeight;
-        rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
-        final float round = mGestureFloatingPreviewRoundRadius;
-        paint.setColor(mGestureFloatingPreviewColor);
-        canvas.drawRoundRect(rectangle, round, round, paint);
-        // Paint the text preview
-        paint.setColor(mGestureFloatingPreviewTextColor);
-        final float textX = rectX + hPad + textWidth / 2.0f;
-        final float textY = rectY + vPad + textHeight;
-        canvas.drawText(gestureFloatingPreviewText, textX, textY, paint);
-    }
-
     private void drawSlidingKeyInputPreview(final Canvas canvas) {
         // TODO: Implement rubber band preview
     }
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index 0e7f891..6367156 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -18,11 +18,10 @@
 
 import android.content.Context;
 import android.media.AudioManager;
+import android.os.Vibrator;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
 
-import com.android.inputmethod.latin.VibratorUtils;
-
 /**
  * This class gathers audio feedback and haptic feedback functions.
  *
@@ -32,34 +31,61 @@
 public final class AudioAndHapticFeedbackManager {
     public static final int MAX_KEYPRESS_VIBRATION_DURATION = 250; // millisecond
 
-    private final AudioManager mAudioManager;
-    private final VibratorUtils mVibratorUtils;
+    private AudioManager mAudioManager;
+    private Vibrator mVibrator;
 
     private SettingsValues mSettingsValues;
     private boolean mSoundOn;
 
-    public AudioAndHapticFeedbackManager(final LatinIME latinIme) {
-        mVibratorUtils = VibratorUtils.getInstance(latinIme);
-        mAudioManager = (AudioManager) latinIme.getSystemService(Context.AUDIO_SERVICE);
+    private static final AudioAndHapticFeedbackManager sInstance =
+            new AudioAndHapticFeedbackManager();
+
+    public static AudioAndHapticFeedbackManager getInstance() {
+        return sInstance;
+    }
+
+    private AudioAndHapticFeedbackManager() {
+        // Intentional empty constructor for singleton.
+    }
+
+    public static void init(final Context context) {
+        sInstance.initInternal(context);
+    }
+
+    private void initInternal(final Context context) {
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
     }
 
     public void hapticAndAudioFeedback(final int primaryCode,
             final View viewToPerformHapticFeedbackOn) {
-        vibrate(viewToPerformHapticFeedbackOn);
+        vibrateInternal(viewToPerformHapticFeedbackOn);
         playKeyClick(primaryCode);
     }
 
+    public boolean hasVibrator() {
+        return mVibrator != null && mVibrator.hasVibrator();
+    }
+
+    public void vibrate(final long milliseconds) {
+        if (mVibrator == null) {
+            return;
+        }
+        mVibrator.vibrate(milliseconds);
+    }
+
     private boolean reevaluateIfSoundIsOn() {
         if (mSettingsValues == null || !mSettingsValues.mSoundOn || mAudioManager == null) {
             return false;
-        } else {
-            return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
         }
+        return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
     }
 
-    private void playKeyClick(int primaryCode) {
+    private void playKeyClick(final int primaryCode) {
         // if mAudioManager is null, we can't play a sound anyway, so return
-        if (mAudioManager == null) return;
+        if (mAudioManager == null) {
+            return;
+        }
         if (mSoundOn) {
             final int sound;
             switch (primaryCode) {
@@ -80,7 +106,7 @@
         }
     }
 
-    private void vibrate(final View viewToPerformHapticFeedbackOn) {
+    private void vibrateInternal(final View viewToPerformHapticFeedbackOn) {
         if (!mSettingsValues.mVibrateOn) {
             return;
         }
@@ -91,9 +117,9 @@
                         HapticFeedbackConstants.KEYBOARD_TAP,
                         HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
             }
-        } else if (mVibratorUtils != null) {
-            mVibratorUtils.vibrate(mSettingsValues.mKeypressVibrationDuration);
+            return;
         }
+        vibrate(mSettingsValues.mKeypressVibrationDuration);
     }
 
     public void onSettingsChanged(final SettingsValues settingsValues) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 9d16eb7..6eeee9c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -73,7 +73,6 @@
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.KeyboardView;
 import com.android.inputmethod.keyboard.MainKeyboardView;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
 import com.android.inputmethod.latin.Utils.Stats;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
@@ -128,7 +127,7 @@
     // Current space state of the input method. This can be any of the above constants.
     private int mSpaceState;
 
-    private SettingsValues mCurrentSettings;
+    private final Settings mSettings;
 
     private View mExtractArea;
     private View mKeyPreviewBackingView;
@@ -139,8 +138,6 @@
     private ApplicationInfo mTargetApplicationInfo;
 
     private RichInputMethodManager mRichImm;
-    private Resources mResources;
-    private SharedPreferences mPrefs;
     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
     private final SubtypeSwitcher mSubtypeSwitcher;
     private final SubtypeState mSubtypeState = new SubtypeState();
@@ -401,6 +398,7 @@
 
     public LatinIME() {
         super();
+        mSettings = Settings.getInstance();
         mSubtypeSwitcher = SubtypeSwitcher.getInstance();
         mKeyboardSwitcher = KeyboardSwitcher.getInstance();
         mIsHardwareAcceleratedDrawingEnabled =
@@ -410,9 +408,7 @@
 
     @Override
     public void onCreate() {
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
-        mResources = getResources();
-
+        Settings.init(this);
         LatinImeLogger.init(this);
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.getInstance().init(this);
@@ -421,6 +417,7 @@
         mRichImm = RichInputMethodManager.getInstance();
         SubtypeSwitcher.init(this);
         KeyboardSwitcher.init(this);
+        AudioAndHapticFeedbackManager.init(this);
         AccessibilityUtils.init(this);
 
         super.onCreate();
@@ -431,7 +428,7 @@
         loadSettings();
         initSuggest();
 
-        mDisplayOrientation = mResources.getConfiguration().orientation;
+        mDisplayOrientation = getResources().getConfiguration().orientation;
 
         // Register to receive ringer mode change and network state change.
         // Also receive installation and removal of a dictionary pack.
@@ -458,18 +455,10 @@
     // Has to be package-visible for unit tests
     @UsedForTesting
     void loadSettings() {
-        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
-        // is not guaranteed. It may even be called at the same time on a different thread.
-        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
         final InputAttributes inputAttributes =
                 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
-        final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
-            @Override
-            protected SettingsValues job(Resources res) {
-                return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
-            }
-        };
-        mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+        mSettings.loadSettings(locale, inputAttributes);
         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
     }
 
@@ -496,8 +485,8 @@
         }
         mSuggest = new Suggest(this /* Context */, subtypeLocale,
                 this /* SuggestInitializationListener */);
-        if (mCurrentSettings.mCorrectionEnabled) {
-            mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
+        if (mSettings.getCurrent().mCorrectionEnabled) {
+            mSuggest.setAutoCorrectionThreshold(mSettings.getCurrent().mAutoCorrectionThreshold);
         }
 
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
@@ -511,10 +500,8 @@
 
         resetContactsDictionary(oldContactsDictionary);
 
-        // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
-        // is not guaranteed. It may even be called at the same time on a different thread.
-        if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
-        mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, mPrefs);
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, prefs);
         mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
     }
 
@@ -527,7 +514,8 @@
      * @param oldContactsDictionary an optional dictionary to use, or null
      */
     private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
-        final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
+        final boolean shouldSetDictionary =
+                (null != mSuggest && mSettings.getCurrent().mUseContactsDict);
 
         final ContactsBinaryDictionary dictionaryToUse;
         if (!shouldSetDictionary) {
@@ -571,6 +559,7 @@
             mSuggest.close();
             mSuggest = null;
         }
+        mSettings.onDestroy();
         unregisterReceiver(mReceiver);
         // TODO: The experimental version is not supported by the Dictionary Pack Service yet.
         if (!ProductionFlag.IS_EXPERIMENTAL) {
@@ -681,7 +670,8 @@
                     + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs);
+            final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+            ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs);
         }
         if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
             Log.w(TAG, "Deprecated private IME option specified: "
@@ -713,7 +703,7 @@
             accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
         }
 
-        final boolean inputTypeChanged = !mCurrentSettings.isSameInputType(editorInfo);
+        final boolean inputTypeChanged = !mSettings.getCurrent().isSameInputType(editorInfo);
         final boolean isDifferentTextField = !restarting || inputTypeChanged;
         if (isDifferentTextField) {
             mSubtypeSwitcher.updateParametersOnStartInputView();
@@ -743,11 +733,12 @@
             mainKeyboardView.closing();
             loadSettings();
 
-            if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
-                mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
+            if (mSuggest != null && mSettings.getCurrent().mCorrectionEnabled) {
+                mSuggest.setAutoCorrectionThreshold(
+                        mSettings.getCurrent().mAutoCorrectionThreshold);
             }
 
-            switcher.loadKeyboard(editorInfo, mCurrentSettings);
+            switcher.loadKeyboard(editorInfo, mSettings.getCurrent());
         } else if (restarting) {
             // TODO: Come up with a more comprehensive way to reset the keyboard layout when
             // a keyboard layout set doesn't get reloaded in this method.
@@ -768,11 +759,12 @@
         mHandler.cancelDoubleSpacePeriodTimer();
 
         mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
-        mainKeyboardView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
-                mCurrentSettings.mKeyPreviewPopupDismissDelay);
-        mainKeyboardView.setGestureHandlingEnabledByUser(mCurrentSettings.mGestureInputEnabled);
-        mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled,
-                mCurrentSettings.mGestureFloatingPreviewTextEnabled);
+        mainKeyboardView.setKeyPreviewPopupEnabled(mSettings.getCurrent().mKeyPreviewPopupOn,
+                mSettings.getCurrent().mKeyPreviewPopupDismissDelay);
+        mainKeyboardView.setGestureHandlingEnabledByUser(
+                mSettings.getCurrent().mGestureInputEnabled);
+        mainKeyboardView.setGesturePreviewMode(mSettings.getCurrent().mGesturePreviewTrailEnabled,
+                mSettings.getCurrent().mGestureFloatingPreviewTextEnabled);
 
         // If we have a user dictionary addition in progress, we should check now if we should
         // replace the previously committed string with the word that has actually been added
@@ -930,7 +922,7 @@
      */
     @Override
     public void onExtractedTextClicked() {
-        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
+        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedTextClicked();
     }
@@ -946,7 +938,7 @@
      */
     @Override
     public void onExtractedCursorMovement(final int dx, final int dy) {
-        if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
+        if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return;
 
         super.onExtractedCursorMovement(dx, dy);
     }
@@ -974,7 +966,7 @@
                 }
             }
         }
-        if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+        if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
         mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
         if (applicationSpecifiedCompletions == null) {
             clearSuggestionStrip();
@@ -1039,7 +1031,7 @@
         }
         final int keyboardHeight = mainKeyboardView.getHeight();
         final int suggestionsHeight = mSuggestionsContainer.getHeight();
-        final int displayHeight = mResources.getDisplayMetrics().heightPixels;
+        final int displayHeight = getResources().getDisplayMetrics().heightPixels;
         final Rect rect = new Rect();
         mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
         final int notificationBarHeight = rect.top;
@@ -1118,10 +1110,10 @@
     // the composing word, reset the last composed word, tell the inputconnection about it.
     private void resetEntireInputState(final int newCursorPosition) {
         resetComposingState(true /* alsoResetLastComposedWord */);
-        if (mCurrentSettings.mBigramPredictionEnabled) {
+        if (mSettings.getCurrent().mBigramPredictionEnabled) {
             clearSuggestionStrip();
         } else {
-            setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
+            setSuggestionStrip(mSettings.getCurrent().mSuggestPuncList, false);
         }
         mConnection.resetCachesUponCursorMove(newCursorPosition);
     }
@@ -1139,7 +1131,7 @@
             commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
                     separatorString);
             if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.getInstance().onWordComplete(typedWord, Long.MAX_VALUE);
+                ResearchLogger.getInstance().onWordFinished(typedWord);
             }
         }
     }
@@ -1147,7 +1139,7 @@
     // Called from the KeyboardSwitcher which needs to know auto caps state to display
     // the right layout.
     public int getCurrentAutoCapsState() {
-        if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+        if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
 
         final EditorInfo ei = getCurrentInputEditorInfo();
         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
@@ -1179,16 +1171,15 @@
             final String text = lastTwo.charAt(1) + " ";
             mConnection.commitText(text, 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.getInstance().onWordComplete(text, Long.MAX_VALUE);
-                ResearchLogger.latinIME_swapSwapperAndSpace();
+                ResearchLogger.latinIME_swapSwapperAndSpace(text);
             }
             mKeyboardSwitcher.updateShiftState();
         }
     }
 
     private boolean maybeDoubleSpacePeriod() {
-        if (!mCurrentSettings.mCorrectionEnabled) return false;
-        if (!mCurrentSettings.mUseDoubleSpacePeriod) return false;
+        if (!mSettings.getCurrent().mCorrectionEnabled) return false;
+        if (!mSettings.getCurrent().mUseDoubleSpacePeriod) return false;
         if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
         final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
         if (lastThree != null && lastThree.length() == 3
@@ -1200,7 +1191,7 @@
             final String textToInsert = ". ";
             mConnection.commitText(textToInsert, 1);
             if (ProductionFlag.IS_EXPERIMENTAL) {
-                ResearchLogger.getInstance().onWordComplete(textToInsert, Long.MAX_VALUE);
+                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert);
             }
             mKeyboardSwitcher.updateShiftState();
             return true;
@@ -1291,7 +1282,7 @@
     // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
     private void handleLanguageSwitchKey() {
         final IBinder token = getWindow().getWindow().getAttributes().token;
-        if (mCurrentSettings.mIncludesOtherImesInLanguageSwitchList) {
+        if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
             mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
             return;
         }
@@ -1393,7 +1384,7 @@
             break;
         default:
             mSpaceState = SPACE_STATE_NONE;
-            if (mCurrentSettings.isWordSeparator(primaryCode)) {
+            if (mSettings.getCurrent().isWordSeparator(primaryCode)) {
                 didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
             } else {
                 if (SPACE_STATE_PHANTOM == spaceState) {
@@ -1449,7 +1440,7 @@
         }
         mConnection.commitText(text, 1);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().onWordComplete(text, Long.MAX_VALUE);
+            ResearchLogger.latinIME_onTextInput(text);
         }
         mConnection.endBatchEdit();
         // Space state must be updated before calling updateShiftState
@@ -1491,8 +1482,8 @@
             // should usually be followed by a space, and it should be more readable.
             if (Constants.NOT_A_CODE != codePointBeforeCursor
                     && !Character.isWhitespace(codePointBeforeCursor)
-                    && !mCurrentSettings.isPhantomSpacePromotingSymbol(codePointBeforeCursor)
-                    && !mCurrentSettings.isWeakSpaceStripper(codePointBeforeCursor)) {
+                    && !mSettings.getCurrent().isPhantomSpacePromotingSymbol(codePointBeforeCursor)
+                    && !mSettings.getCurrent().isWeakSpaceStripper(codePointBeforeCursor)) {
                 mSpaceState = SPACE_STATE_PHANTOM;
             }
         }
@@ -1599,9 +1590,7 @@
         if (dismissGestureFloatingPreviewText) {
             mainKeyboardView.dismissGestureFloatingPreviewText();
         } else {
-            final String batchInputText = suggestedWords.isEmpty()
-                    ? null : suggestedWords.getWord(0);
-            mainKeyboardView.showGestureFloatingPreviewText(batchInputText);
+            mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
         }
     }
 
@@ -1710,7 +1699,7 @@
             }
             if (SPACE_STATE_DOUBLE == spaceState) {
                 mHandler.cancelDoubleSpacePeriodTimer();
-                if (mConnection.revertDoubleSpace()) {
+                if (mConnection.revertDoubleSpacePeriod()) {
                     // No need to reset mSpaceState, it has already be done (that's why we
                     // receive it as a parameter)
                     return;
@@ -1749,7 +1738,7 @@
                     mConnection.deleteSurroundingText(1, 0);
                 }
             }
-            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+            if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
             }
         }
@@ -1763,10 +1752,10 @@
         } else if ((SPACE_STATE_WEAK == spaceState
                 || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
                 && isFromSuggestionStrip) {
-            if (mCurrentSettings.isWeakSpaceSwapper(code)) {
+            if (mSettings.getCurrent().isWeakSpaceSwapper(code)) {
                 return true;
             } else {
-                if (mCurrentSettings.isWeakSpaceStripper(code)) {
+                if (mSettings.getCurrent().isWeakSpaceStripper(code)) {
                     mConnection.removeTrailingSpace();
                 }
                 return false;
@@ -1781,7 +1770,7 @@
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
+                !mSettings.getCurrent().isSymbolExcludedFromWordSeparators(primaryCode)) {
             if (isComposingWord) {
                 // Sanity check
                 throw new RuntimeException("Should not be composing here");
@@ -1793,9 +1782,9 @@
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
         if (!isComposingWord && (isAlphabet(primaryCode)
-                || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
-                && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
-                !mConnection.isCursorTouchingWord(mCurrentSettings)) {
+                || mSettings.getCurrent().isSymbolExcludedFromWordSeparators(primaryCode))
+                && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) &&
+                !mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
             // Reset entirely the composing state anyway, then start composing a new word unless
             // the character is a single quote. The idea here is, single quote is not a
             // separator and it should be treated as a normal character, except in the first
@@ -1849,7 +1838,7 @@
         boolean didAutoCorrect = false;
         // Handle separator
         if (mWordComposer.isComposingWord()) {
-            if (mCurrentSettings.mCorrectionEnabled) {
+            if (mSettings.getCurrent().mCorrectionEnabled) {
                 // TODO: maybe cache Strings in an <String> sparse array or something
                 commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
                 didAutoCorrect = true;
@@ -1862,13 +1851,13 @@
                 Constants.SUGGESTION_STRIP_COORDINATE == x);
 
         if (SPACE_STATE_PHANTOM == spaceState &&
-                mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
+                mSettings.getCurrent().isPhantomSpacePromotingSymbol(primaryCode)) {
             promotePhantomSpace();
         }
         sendKeyCodePoint(primaryCode);
 
         if (Constants.CODE_SPACE == primaryCode) {
-            if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+            if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
                 if (maybeDoubleSpacePeriod()) {
                     mSpaceState = SPACE_STATE_DOUBLE;
                 } else if (!isShowingPunctuationList()) {
@@ -1877,7 +1866,7 @@
             }
 
             mHandler.startDoubleSpacePeriodTimer();
-            if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
+            if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
                 mHandler.postUpdateSuggestionStrip();
             }
         } else {
@@ -1885,8 +1874,8 @@
                 swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
             } else if (SPACE_STATE_PHANTOM == spaceState
-                    && !mCurrentSettings.isWeakSpaceStripper(primaryCode)
-                    && !mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
+                    && !mSettings.getCurrent().isWeakSpaceStripper(primaryCode)
+                    && !mSettings.getCurrent().isPhantomSpacePromotingSymbol(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
@@ -1933,7 +1922,7 @@
     @UsedForTesting
     boolean isShowingPunctuationList() {
         if (mSuggestionStripView == null) return false;
-        return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions();
+        return mSettings.getCurrent().mSuggestPuncList == mSuggestionStripView.getSuggestions();
     }
 
     private boolean isSuggestionsStripVisible() {
@@ -1941,11 +1930,11 @@
             return false;
         if (mSuggestionStripView.isShowingAddToDictionaryHint())
             return true;
-        if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
+        if (!mSettings.getCurrent().isSuggestionStripVisibleInOrientation(mDisplayOrientation))
             return false;
-        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
+        if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn())
             return true;
-        return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
+        return mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation);
     }
 
     private void clearSuggestionStrip() {
@@ -1979,7 +1968,8 @@
         mHandler.cancelUpdateSuggestionStrip();
 
         // Check if we have a suggestion engine attached.
-        if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+        if (mSuggest == null
+                || !mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
             if (mWordComposer.isComposingWord()) {
                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
                         + "requested!");
@@ -1987,7 +1977,7 @@
             return;
         }
 
-        if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
+        if (!mWordComposer.isComposingWord() && !mSettings.getCurrent().mBigramPredictionEnabled) {
             setPunctuationSuggestions();
             return;
         }
@@ -2008,10 +1998,10 @@
         // should just skip whitespace if any, so 1.
         // TODO: this is slow (2-way IPC) - we should probably cache this instead.
         final String prevWord =
-                mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators,
+                mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators,
                 mWordComposer.isComposingWord() ? 2 : 1);
         final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
-                prevWord, keyboard.getProximityInfo(), mCurrentSettings.mCorrectionEnabled,
+                prevWord, keyboard.getProximityInfo(), mSettings.getCurrent().mCorrectionEnabled,
                 sessionId);
         return maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
     }
@@ -2036,7 +2026,7 @@
 
     private SuggestedWords getOlderSuggestions(final String typedWord) {
         SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions();
-        if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
+        if (previousSuggestions == mSettings.getCurrent().mSuggestPuncList) {
             previousSuggestions = SuggestedWords.EMPTY;
         }
         if (typedWord == null) {
@@ -2087,12 +2077,11 @@
             if (ProductionFlag.IS_INTERNAL) {
                 Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer);
             }
-            mExpectingUpdateSelection = true;
             if (ProductionFlag.IS_EXPERIMENTAL) {
                 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
                         separatorString);
             }
-
+            mExpectingUpdateSelection = true;
             commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
                     separatorString);
             if (!typedWord.equals(autoCorrection)) {
@@ -2135,13 +2124,13 @@
                 // the current batch input text and there is no need for a phantom space.
                 && !mWordComposer.isBatchMode()) {
             int firstChar = Character.codePointAt(suggestion, 0);
-            if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
-                    && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
+            if ((!mSettings.getCurrent().isWeakSpaceStripper(firstChar))
+                    && (!mSettings.getCurrent().isWeakSpaceSwapper(firstChar))) {
                 promotePhantomSpace();
             }
         }
 
-        if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
+        if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()
                 && mApplicationSpecifiedCompletions != null
                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
             if (mSuggestionStripView != null) {
@@ -2186,7 +2175,7 @@
         }
         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
             mSuggestionStripView.showAddToDictionaryHint(
-                    suggestion, mCurrentSettings.mHintToSaveText);
+                    suggestion, mSettings.getCurrent().mHintToSaveText);
         } else {
             // If we're not showing the "Touch again to save", then update the suggestion strip.
             mHandler.postUpdateSuggestionStrip();
@@ -2212,10 +2201,10 @@
     }
 
     private void setPunctuationSuggestions() {
-        if (mCurrentSettings.mBigramPredictionEnabled) {
+        if (mSettings.getCurrent().mBigramPredictionEnabled) {
             clearSuggestionStrip();
         } else {
-            setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
+            setSuggestionStrip(mSettings.getCurrent().mSuggestPuncList, false);
         }
         setAutoCorrectionIndicator(false);
         setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2228,12 +2217,12 @@
         // If correction is not enabled, we don't add words to the user history dictionary.
         // That's to avoid unintended additions in some sensitive fields, or fields that
         // expect to receive non-words.
-        if (!mCurrentSettings.mCorrectionEnabled) return null;
+        if (!mSettings.getCurrent().mCorrectionEnabled) return null;
 
         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
         if (userHistoryDictionary != null) {
             final String prevWord
-                    = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
+                    = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2);
             final String secondWord;
             if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
                 secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
@@ -2256,7 +2245,8 @@
      * word, else do nothing.
      */
     private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
-        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+        final CharSequence word =
+                mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
         if (null != word) {
             restartSuggestionsOnWordBeforeCursor(word);
         }
@@ -2303,7 +2293,6 @@
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
             ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord);
-            ResearchLogger.getInstance().onWordComplete(originallyTypedWord, Long.MAX_VALUE);
         }
         // Don't restart suggestion yet. We'll restart if the user deletes the
         // separator.
@@ -2314,14 +2303,14 @@
 
     // This essentially inserts a space, and that's it.
     public void promotePhantomSpace() {
-        if (mCurrentSettings.shouldInsertSpacesAutomatically()) {
+        if (mSettings.getCurrent().shouldInsertSpacesAutomatically()) {
             sendKeyCodePoint(Constants.CODE_SPACE);
         }
     }
 
     // Used by the RingCharBuffer
     public boolean isWordSeparator(final int code) {
-        return mCurrentSettings.isWordSeparator(code);
+        return mSettings.getCurrent().isWordSeparator(code);
     }
 
     // TODO: Make this private
@@ -2334,7 +2323,7 @@
         loadSettings();
         if (mKeyboardSwitcher.getMainKeyboardView() != null) {
             // Reload keyboard because the current language has been changed.
-            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
+            mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
         }
         // Since we just changed languages, we should re-evaluate suggestions with whatever word
         // we are currently composing. If we are not composing anything, we may want to display
@@ -2495,7 +2484,7 @@
                 .append("\nPackage : ").append(mTargetApplicationInfo.packageName)
                 .append("\nTarget app sdk version : ")
                 .append(mTargetApplicationInfo.targetSdkVersion)
-                .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString())
+                .append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
                 .append("\nContext : ").append(context);
         throw new RuntimeException(s.toString());
     }
@@ -2509,13 +2498,14 @@
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
         p.println("  Keyboard mode = " + keyboardMode);
+        final SettingsValues settingsValues = mSettings.getCurrent();
         p.println("  mIsSuggestionsSuggestionsRequested = "
-                + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
-        p.println("  mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
+                + settingsValues.isSuggestionsRequested(mDisplayOrientation));
+        p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
         p.println("  isComposingWord=" + mWordComposer.isComposingWord());
-        p.println("  mSoundOn=" + mCurrentSettings.mSoundOn);
-        p.println("  mVibrateOn=" + mCurrentSettings.mVibrateOn);
-        p.println("  mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
-        p.println("  inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
+        p.println("  mSoundOn=" + settingsValues.mSoundOn);
+        p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
+        p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
+        p.println("  inputAttributes=" + settingsValues.mInputAttributes);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 9cb24b5..0d3ebac 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -643,7 +643,7 @@
         return word;
     }
 
-    public boolean revertDoubleSpace() {
+    public boolean revertDoubleSpacePeriod() {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         // Here we test whether we indeed have a period and a space before us. This should not
         // be needed, but it's there just in case something went wrong.
@@ -660,7 +660,7 @@
         final String doubleSpace = "  ";
         commitText(doubleSpace, 1);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().onWordComplete(doubleSpace, Long.MAX_VALUE);
+            ResearchLogger.richInputConnection_revertDoubleSpacePeriod(doubleSpace);
         }
         return true;
     }
@@ -685,7 +685,7 @@
         final String text = " " + textBeforeCursor.subSequence(0, 1);
         commitText(text, 1);
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().onWordComplete(text, Long.MAX_VALUE);
+            ResearchLogger.richInputConnection_revertSwapPunctuation(text);
         }
         return true;
     }
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 222adcb..1d9d85b 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2013 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
@@ -16,27 +16,16 @@
 
 package com.android.inputmethod.latin;
 
-import android.app.backup.BackupManager;
 import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.preference.CheckBoxPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.Preference.OnPreferenceClickListener;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceScreen;
-import android.view.inputmethod.InputMethodSubtype;
+import android.preference.PreferenceManager;
 
-import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethodcommon.InputMethodSettingsFragment;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
 
-public final class Settings extends InputMethodSettingsFragment
-        implements SharedPreferences.OnSharedPreferenceChangeListener {
+import java.util.Locale;
 
+public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
     // In the same order as xml/prefs.xml
     public static final String PREF_GENERAL_SETTINGS = "general_settings";
     public static final String PREF_AUTO_CAP = "auto_cap";
@@ -77,338 +66,54 @@
     public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
     public static final String PREF_DEBUG_SETTINGS = "debug_settings";
 
-    private PreferenceScreen mKeypressVibrationDurationSettingsPref;
-    private PreferenceScreen mKeypressSoundVolumeSettingsPref;
-    private ListPreference mVoicePreference;
-    private ListPreference mShowCorrectionSuggestionsPreference;
-    private ListPreference mAutoCorrectionThresholdPreference;
-    private ListPreference mKeyPreviewPopupDismissDelay;
-    // Use bigrams to predict the next word when there is no input for it yet
-    private CheckBoxPreference mBigramPrediction;
-    private Preference mDebugSettingsPreference;
+    private Resources mRes;
+    private SharedPreferences mPrefs;
+    private Locale mCurrentLocale;
+    private SettingsValues mSettingsValues;
 
-    private static void setPreferenceEnabled(final Preference preference, final boolean enabled) {
-        if (preference != null) {
-            preference.setEnabled(enabled);
-        }
+    private static final Settings sInstance = new Settings();
+
+    public static Settings getInstance() {
+        return sInstance;
     }
 
-    private void ensureConsistencyOfAutoCorrectionSettings() {
-        final String autoCorrectionOff = getResources().getString(
-                R.string.auto_correction_threshold_mode_index_off);
-        final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
-        setPreferenceEnabled(mBigramPrediction, !currentSetting.equals(autoCorrectionOff));
+    public static void init(final Context context) {
+        sInstance.onCreate(context);
     }
 
-    @Override
-    public void onCreate(final Bundle icicle) {
-        super.onCreate(icicle);
-        setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
-        setSubtypeEnablerTitle(R.string.select_language);
-        addPreferencesFromResource(R.xml.prefs);
-
-        final Resources res = getResources();
-        final Context context = getActivity();
-
-        // When we are called from the Settings application but we are not already running, the
-        // {@link SubtypeLocale} class may not have been initialized. It is safe to call
-        // {@link SubtypeLocale#init(Context)} multiple times.
-        SubtypeLocale.init(context);
-        mVoicePreference = (ListPreference) findPreference(PREF_VOICE_MODE);
-        mShowCorrectionSuggestionsPreference =
-                (ListPreference) findPreference(PREF_SHOW_SUGGESTIONS_SETTING);
-        final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
-        prefs.registerOnSharedPreferenceChangeListener(this);
-
-        mAutoCorrectionThresholdPreference =
-                (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
-        mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
-        ensureConsistencyOfAutoCorrectionSettings();
-
-        final PreferenceGroup generalSettings =
-                (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS);
-        final PreferenceGroup textCorrectionGroup =
-                (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS);
-        final PreferenceGroup gestureTypingSettings =
-                (PreferenceGroup) findPreference(PREF_GESTURE_SETTINGS);
-        final PreferenceGroup miscSettings =
-                (PreferenceGroup) findPreference(PREF_MISC_SETTINGS);
-
-        mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
-        if (mDebugSettingsPreference != null) {
-            if (ProductionFlag.IS_INTERNAL) {
-                final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
-                debugSettingsIntent.setClassName(
-                        context.getPackageName(), DebugSettingsActivity.class.getName());
-                mDebugSettingsPreference.setIntent(debugSettingsIntent);
-            } else {
-                miscSettings.removePreference(mDebugSettingsPreference);
-            }
-        }
-
-        final boolean showVoiceKeyOption = res.getBoolean(
-                R.bool.config_enable_show_voice_key_option);
-        if (!showVoiceKeyOption) {
-            generalSettings.removePreference(mVoicePreference);
-        }
-
-        final PreferenceGroup advancedSettings =
-                (PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
-        if (!VibratorUtils.getInstance(context).hasVibrator()) {
-            generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
-            if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
-                advancedSettings.removePreference(findPreference(PREF_VIBRATION_DURATION_SETTINGS));
-            }
-        }
-
-        final boolean showKeyPreviewPopupOption = res.getBoolean(
-                R.bool.config_enable_show_popup_on_keypress_option);
-        mKeyPreviewPopupDismissDelay =
-                (ListPreference) findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
-        if (!showKeyPreviewPopupOption) {
-            generalSettings.removePreference(findPreference(PREF_POPUP_ON));
-            if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
-                advancedSettings.removePreference(mKeyPreviewPopupDismissDelay);
-            }
-        } else {
-            final String[] entries = new String[] {
-                    res.getString(R.string.key_preview_popup_dismiss_no_delay),
-                    res.getString(R.string.key_preview_popup_dismiss_default_delay),
-            };
-            final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
-                    R.integer.config_key_preview_linger_timeout));
-            mKeyPreviewPopupDismissDelay.setEntries(entries);
-            mKeyPreviewPopupDismissDelay.setEntryValues(
-                    new String[] { "0", popupDismissDelayDefaultValue });
-            if (null == mKeyPreviewPopupDismissDelay.getValue()) {
-                mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
-            }
-            setPreferenceEnabled(mKeyPreviewPopupDismissDelay,
-                    SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
-        }
-
-        setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
-                SettingsValues.showsLanguageSwitchKey(prefs));
-
-        final PreferenceScreen dictionaryLink =
-                (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
-        final Intent intent = dictionaryLink.getIntent();
-
-        final int number = context.getPackageManager().queryIntentActivities(intent, 0).size();
-        // TODO: The experimental version is not supported by the Dictionary Pack Service yet
-        if (ProductionFlag.IS_EXPERIMENTAL || 0 >= number) {
-            textCorrectionGroup.removePreference(dictionaryLink);
-        }
-
-        final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
-                R.bool.config_gesture_input_enabled_by_build_config);
-        if (!gestureInputEnabledByBuildConfig) {
-            getPreferenceScreen().removePreference(gestureTypingSettings);
-        }
-
-        mKeypressVibrationDurationSettingsPref =
-                (PreferenceScreen) findPreference(PREF_VIBRATION_DURATION_SETTINGS);
-        if (mKeypressVibrationDurationSettingsPref != null) {
-            mKeypressVibrationDurationSettingsPref.setOnPreferenceClickListener(
-                    new OnPreferenceClickListener() {
-                        @Override
-                        public boolean onPreferenceClick(Preference arg0) {
-                            showKeypressVibrationDurationSettingsDialog();
-                            return true;
-                        }
-                    });
-            mKeypressVibrationDurationSettingsPref.setSummary(
-                    res.getString(R.string.settings_keypress_vibration_duration,
-                            SettingsValues.getCurrentVibrationDuration(prefs, res)));
-        }
-
-        mKeypressSoundVolumeSettingsPref =
-                (PreferenceScreen) findPreference(PREF_KEYPRESS_SOUND_VOLUME);
-        if (mKeypressSoundVolumeSettingsPref != null) {
-            mKeypressSoundVolumeSettingsPref.setOnPreferenceClickListener(
-                    new OnPreferenceClickListener() {
-                        @Override
-                        public boolean onPreferenceClick(Preference arg0) {
-                            showKeypressSoundVolumeSettingDialog();
-                            return true;
-                        }
-                    });
-            mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf(
-                    getCurrentKeyPressSoundVolumePercent(prefs, res)));
-        }
-        refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, res);
+    private Settings() {
+        // Intentional empty constructor for singleton.
     }
 
-    @Override
-    public void onResume() {
-        super.onResume();
-        final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
-        if (isShortcutImeEnabled) {
-            updateVoiceModeSummary();
-        } else {
-            getPreferenceScreen().removePreference(mVoicePreference);
-        }
-        updateShowCorrectionSuggestionsSummary();
-        updateKeyPreviewPopupDelaySummary();
-        updateCustomInputStylesSummary();
+    private void onCreate(final Context context) {
+        mRes = context.getResources();
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+        mPrefs.registerOnSharedPreferenceChangeListener(this);
     }
 
-    @Override
     public void onDestroy() {
-        getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
-                this);
-        super.onDestroy();
+        mPrefs.unregisterOnSharedPreferenceChangeListener(this);
     }
 
     @Override
     public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
-        (new BackupManager(getActivity())).dataChanged();
-        if (key.equals(PREF_POPUP_ON)) {
-            setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY),
-                    prefs.getBoolean(PREF_POPUP_ON, true));
-        } else if (key.equals(PREF_SHOW_LANGUAGE_SWITCH_KEY)) {
-            setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
-                    SettingsValues.showsLanguageSwitchKey(prefs));
-        } else if (key.equals(PREF_GESTURE_INPUT)) {
-            final boolean gestureInputEnabledByConfig = getResources().getBoolean(
-                    R.bool.config_gesture_input_enabled_by_build_config);
-            if (gestureInputEnabledByConfig) {
-                final boolean gestureInputEnabledByUser = prefs.getBoolean(
-                        PREF_GESTURE_INPUT, true);
-                setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL),
-                        gestureInputEnabledByUser);
-                setPreferenceEnabled(findPreference(PREF_GESTURE_FLOATING_PREVIEW_TEXT),
-                        gestureInputEnabledByUser);
-            }
-        }
-        ensureConsistencyOfAutoCorrectionSettings();
-        updateVoiceModeSummary();
-        updateShowCorrectionSuggestionsSummary();
-        updateKeyPreviewPopupDelaySummary();
-        refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources());
+        loadSettings(mCurrentLocale, mSettingsValues.mInputAttributes);
     }
 
-    private void updateShowCorrectionSuggestionsSummary() {
-        mShowCorrectionSuggestionsPreference.setSummary(
-                getResources().getStringArray(R.array.prefs_suggestion_visibilities)
-                [mShowCorrectionSuggestionsPreference.findIndexOfValue(
-                        mShowCorrectionSuggestionsPreference.getValue())]);
-    }
-
-    private void updateCustomInputStylesSummary() {
-        final PreferenceScreen customInputStyles =
-                (PreferenceScreen)findPreference(PREF_CUSTOM_INPUT_STYLES);
-        final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
-        final Resources res = getResources();
-        final String prefSubtype = SettingsValues.getPrefAdditionalSubtypes(prefs, res);
-        final InputMethodSubtype[] subtypes =
-                AdditionalSubtype.createAdditionalSubtypesArray(prefSubtype);
-        final StringBuilder styles = new StringBuilder();
-        for (final InputMethodSubtype subtype : subtypes) {
-            if (styles.length() > 0) styles.append(", ");
-            styles.append(SubtypeLocale.getSubtypeDisplayName(subtype, res));
-        }
-        customInputStyles.setSummary(styles);
-    }
-
-    private void updateKeyPreviewPopupDelaySummary() {
-        final ListPreference lp = mKeyPreviewPopupDismissDelay;
-        final CharSequence[] entries = lp.getEntries();
-        if (entries == null || entries.length <= 0) return;
-        lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
-    }
-
-    private void updateVoiceModeSummary() {
-        mVoicePreference.setSummary(
-                getResources().getStringArray(R.array.voice_input_modes_summary)
-                        [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
-    }
-
-    private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
-            final SharedPreferences sp, final Resources res) {
-        if (mKeypressVibrationDurationSettingsPref != null) {
-            final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity())
-                    .hasVibrator();
-            final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON,
-                    res.getBoolean(R.bool.config_default_vibration_enabled));
-            setPreferenceEnabled(mKeypressVibrationDurationSettingsPref,
-                    hasVibratorHardware && vibrateOnByUser);
-        }
-
-        if (mKeypressSoundVolumeSettingsPref != null) {
-            final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
-                    res.getBoolean(R.bool.config_default_sound_enabled));
-            setPreferenceEnabled(mKeypressSoundVolumeSettingsPref, soundOn);
-        }
-    }
-
-    private void showKeypressVibrationDurationSettingsDialog() {
-        final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
-        final Context context = getActivity();
-        final PreferenceScreen settingsPref = mKeypressVibrationDurationSettingsPref;
-        final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
+    public void loadSettings(final Locale locale, final InputAttributes inputAttributes) {
+        mCurrentLocale = locale;
+        final SharedPreferences prefs = mPrefs;
+        final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
             @Override
-            public void onPositiveButtonClick(final SeekBarDialog dialog) {
-                final int ms = dialog.getValue();
-                sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, ms).apply();
-                if (settingsPref != null) {
-                    settingsPref.setSummary(dialog.getValueText());
-                }
-            }
-
-            @Override
-            public void onStopTrackingTouch(final SeekBarDialog dialog) {
-                final int ms = dialog.getValue();
-                VibratorUtils.getInstance(context).vibrate(ms);
+            protected SettingsValues job(final Resources res) {
+                return new SettingsValues(prefs, res, inputAttributes);
             }
         };
-        final int currentMs = SettingsValues.getCurrentVibrationDuration(sp, getResources());
-        final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
-        builder.setTitle(R.string.prefs_keypress_vibration_duration_settings)
-            .setListener(listener)
-            .setMaxValue(AudioAndHapticFeedbackManager.MAX_KEYPRESS_VIBRATION_DURATION)
-            .setValueFromat(R.string.settings_keypress_vibration_duration)
-            .setValue(currentMs)
-            .create()
-            .show();
+        mSettingsValues = job.runInLocale(mRes, locale);
     }
 
-    private static final int PERCENT_INT = 100;
-    private static final float PERCENT_FLOAT = 100.0f;
-
-    private static int getCurrentKeyPressSoundVolumePercent(final SharedPreferences sp,
-            final Resources res) {
-        return (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * PERCENT_FLOAT);
-    }
-
-    private void showKeypressSoundVolumeSettingDialog() {
-        final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
-        final Context context = getActivity();
-        final AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-        final PreferenceScreen settingsPref = mKeypressSoundVolumeSettingsPref;
-        final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
-            @Override
-            public void onPositiveButtonClick(final SeekBarDialog dialog) {
-                final float volume = dialog.getValue() / PERCENT_FLOAT;
-                sp.edit().putFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, volume).apply();
-                if (settingsPref != null) {
-                    settingsPref.setSummary(dialog.getValueText());
-                }
-            }
-
-            @Override
-            public void onStopTrackingTouch(final SeekBarDialog dialog) {
-                final float volume = dialog.getValue() / PERCENT_FLOAT;
-                am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, volume);
-            }
-        };
-        final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
-        final int currentVolumeInt = getCurrentKeyPressSoundVolumePercent(sp, getResources());
-        builder.setTitle(R.string.prefs_keypress_sound_volume_settings)
-            .setListener(listener)
-            .setMaxValue(PERCENT_INT)
-            .setValue(currentVolumeInt)
-            .create()
-            .show();
+    // TODO: Remove this method and add proxy method to SettingsValues.
+    public SettingsValues getCurrent() {
+        return mSettingsValues;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java
index 0d3c8eb..3aeb101 100644
--- a/java/src/com/android/inputmethod/latin/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java
@@ -20,7 +20,7 @@
 import android.preference.PreferenceActivity;
 
 public final class SettingsActivity extends PreferenceActivity {
-    private static final String DEFAULT_FRAGMENT = Settings.class.getName();
+    private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
 
     @Override
     public Intent getIntent() {
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
new file mode 100644
index 0000000..a2980bf
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethodcommon.InputMethodSettingsFragment;
+
+public final class SettingsFragment extends InputMethodSettingsFragment
+        implements SharedPreferences.OnSharedPreferenceChangeListener {
+    private PreferenceScreen mKeypressVibrationDurationSettingsPref;
+    private PreferenceScreen mKeypressSoundVolumeSettingsPref;
+    private ListPreference mVoicePreference;
+    private ListPreference mShowCorrectionSuggestionsPreference;
+    private ListPreference mAutoCorrectionThresholdPreference;
+    private ListPreference mKeyPreviewPopupDismissDelay;
+    // Use bigrams to predict the next word when there is no input for it yet
+    private CheckBoxPreference mBigramPrediction;
+    private Preference mDebugSettingsPreference;
+
+    private void setPreferenceEnabled(final String preferenceKey, final boolean enabled) {
+        final Preference preference = findPreference(preferenceKey);
+        if (preference != null) {
+            preference.setEnabled(enabled);
+        }
+    }
+
+    private void ensureConsistencyOfAutoCorrectionSettings() {
+        final String autoCorrectionOff = getResources().getString(
+                R.string.auto_correction_threshold_mode_index_off);
+        final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
+        mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
+    }
+
+    @Override
+    public void onCreate(final Bundle icicle) {
+        super.onCreate(icicle);
+        setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
+        setSubtypeEnablerTitle(R.string.select_language);
+        addPreferencesFromResource(R.xml.prefs);
+
+        final Resources res = getResources();
+        final Context context = getActivity();
+
+        // When we are called from the Settings application but we are not already running, the
+        // {@link SubtypeLocale} class may not have been initialized. It is safe to call
+        // {@link SubtypeLocale#init(Context)} multiple times.
+        SubtypeLocale.init(context);
+        mVoicePreference = (ListPreference) findPreference(Settings.PREF_VOICE_MODE);
+        mShowCorrectionSuggestionsPreference =
+                (ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
+        final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+        prefs.registerOnSharedPreferenceChangeListener(this);
+
+        mAutoCorrectionThresholdPreference =
+                (ListPreference) findPreference(Settings.PREF_AUTO_CORRECTION_THRESHOLD);
+        mBigramPrediction = (CheckBoxPreference) findPreference(Settings.PREF_BIGRAM_PREDICTIONS);
+        ensureConsistencyOfAutoCorrectionSettings();
+
+        final PreferenceGroup generalSettings =
+                (PreferenceGroup) findPreference(Settings.PREF_GENERAL_SETTINGS);
+        final PreferenceGroup textCorrectionGroup =
+                (PreferenceGroup) findPreference(Settings.PREF_CORRECTION_SETTINGS);
+        final PreferenceGroup gestureTypingSettings =
+                (PreferenceGroup) findPreference(Settings.PREF_GESTURE_SETTINGS);
+        final PreferenceGroup miscSettings =
+                (PreferenceGroup) findPreference(Settings.PREF_MISC_SETTINGS);
+
+        mDebugSettingsPreference = findPreference(Settings.PREF_DEBUG_SETTINGS);
+        if (mDebugSettingsPreference != null) {
+            if (ProductionFlag.IS_INTERNAL) {
+                final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
+                debugSettingsIntent.setClassName(
+                        context.getPackageName(), DebugSettingsActivity.class.getName());
+                mDebugSettingsPreference.setIntent(debugSettingsIntent);
+            } else {
+                miscSettings.removePreference(mDebugSettingsPreference);
+            }
+        }
+
+        final boolean showVoiceKeyOption = res.getBoolean(
+                R.bool.config_enable_show_voice_key_option);
+        if (!showVoiceKeyOption) {
+            generalSettings.removePreference(mVoicePreference);
+        }
+
+        final PreferenceGroup advancedSettings =
+                (PreferenceGroup) findPreference(Settings.PREF_ADVANCED_SETTINGS);
+        if (!AudioAndHapticFeedbackManager.getInstance().hasVibrator()) {
+            generalSettings.removePreference(findPreference(Settings.PREF_VIBRATE_ON));
+            if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+                advancedSettings.removePreference(
+                        findPreference(Settings.PREF_VIBRATION_DURATION_SETTINGS));
+            }
+        }
+
+        final boolean showKeyPreviewPopupOption = res.getBoolean(
+                R.bool.config_enable_show_popup_on_keypress_option);
+        mKeyPreviewPopupDismissDelay =
+                (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+        if (!showKeyPreviewPopupOption) {
+            generalSettings.removePreference(findPreference(Settings.PREF_POPUP_ON));
+            if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+                advancedSettings.removePreference(mKeyPreviewPopupDismissDelay);
+            }
+        } else {
+            final String[] entries = new String[] {
+                    res.getString(R.string.key_preview_popup_dismiss_no_delay),
+                    res.getString(R.string.key_preview_popup_dismiss_default_delay),
+            };
+            final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+                    R.integer.config_key_preview_linger_timeout));
+            mKeyPreviewPopupDismissDelay.setEntries(entries);
+            mKeyPreviewPopupDismissDelay.setEntryValues(
+                    new String[] { "0", popupDismissDelayDefaultValue });
+            if (null == mKeyPreviewPopupDismissDelay.getValue()) {
+                mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
+            }
+            mKeyPreviewPopupDismissDelay.setEnabled(
+                    SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
+        }
+
+        setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST,
+                SettingsValues.showsLanguageSwitchKey(prefs));
+
+        final PreferenceScreen dictionaryLink =
+                (PreferenceScreen) findPreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY);
+        final Intent intent = dictionaryLink.getIntent();
+
+        final int number = context.getPackageManager().queryIntentActivities(intent, 0).size();
+        // TODO: The experimental version is not supported by the Dictionary Pack Service yet
+        if (ProductionFlag.IS_EXPERIMENTAL || 0 >= number) {
+            textCorrectionGroup.removePreference(dictionaryLink);
+        }
+
+        final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+                R.bool.config_gesture_input_enabled_by_build_config);
+        if (!gestureInputEnabledByBuildConfig) {
+            getPreferenceScreen().removePreference(gestureTypingSettings);
+        }
+
+        mKeypressVibrationDurationSettingsPref =
+                (PreferenceScreen) findPreference(Settings.PREF_VIBRATION_DURATION_SETTINGS);
+        if (mKeypressVibrationDurationSettingsPref != null) {
+            mKeypressVibrationDurationSettingsPref.setOnPreferenceClickListener(
+                    new OnPreferenceClickListener() {
+                        @Override
+                        public boolean onPreferenceClick(Preference arg0) {
+                            showKeypressVibrationDurationSettingsDialog();
+                            return true;
+                        }
+                    });
+            mKeypressVibrationDurationSettingsPref.setSummary(
+                    res.getString(R.string.settings_keypress_vibration_duration,
+                            SettingsValues.getCurrentVibrationDuration(prefs, res)));
+        }
+
+        mKeypressSoundVolumeSettingsPref =
+                (PreferenceScreen) findPreference(Settings.PREF_KEYPRESS_SOUND_VOLUME);
+        if (mKeypressSoundVolumeSettingsPref != null) {
+            mKeypressSoundVolumeSettingsPref.setOnPreferenceClickListener(
+                    new OnPreferenceClickListener() {
+                        @Override
+                        public boolean onPreferenceClick(Preference arg0) {
+                            showKeypressSoundVolumeSettingDialog();
+                            return true;
+                        }
+                    });
+            mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf(
+                    getCurrentKeyPressSoundVolumePercent(prefs, res)));
+        }
+        refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, res);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
+        if (isShortcutImeEnabled) {
+            updateVoiceModeSummary();
+        } else {
+            getPreferenceScreen().removePreference(mVoicePreference);
+        }
+        updateShowCorrectionSuggestionsSummary();
+        updateKeyPreviewPopupDelaySummary();
+        updateCustomInputStylesSummary();
+    }
+
+    @Override
+    public void onDestroy() {
+        getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
+                this);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+        (new BackupManager(getActivity())).dataChanged();
+        if (key.equals(Settings.PREF_POPUP_ON)) {
+            setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+                    prefs.getBoolean(Settings.PREF_POPUP_ON, true));
+        } else if (key.equals(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY)) {
+            setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST,
+                    SettingsValues.showsLanguageSwitchKey(prefs));
+        } else if (key.equals(Settings.PREF_GESTURE_INPUT)) {
+            final boolean gestureInputEnabledByConfig = getResources().getBoolean(
+                    R.bool.config_gesture_input_enabled_by_build_config);
+            if (gestureInputEnabledByConfig) {
+                final boolean gestureInputEnabledByUser = prefs.getBoolean(
+                        Settings.PREF_GESTURE_INPUT, true);
+                setPreferenceEnabled(Settings.PREF_GESTURE_PREVIEW_TRAIL,
+                        gestureInputEnabledByUser);
+                setPreferenceEnabled(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT,
+                        gestureInputEnabledByUser);
+            }
+        }
+        ensureConsistencyOfAutoCorrectionSettings();
+        updateVoiceModeSummary();
+        updateShowCorrectionSuggestionsSummary();
+        updateKeyPreviewPopupDelaySummary();
+        refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources());
+    }
+
+    private void updateShowCorrectionSuggestionsSummary() {
+        mShowCorrectionSuggestionsPreference.setSummary(
+                getResources().getStringArray(R.array.prefs_suggestion_visibilities)
+                [mShowCorrectionSuggestionsPreference.findIndexOfValue(
+                        mShowCorrectionSuggestionsPreference.getValue())]);
+    }
+
+    private void updateCustomInputStylesSummary() {
+        final PreferenceScreen customInputStyles =
+                (PreferenceScreen)findPreference(Settings.PREF_CUSTOM_INPUT_STYLES);
+        final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+        final Resources res = getResources();
+        final String prefSubtype = SettingsValues.getPrefAdditionalSubtypes(prefs, res);
+        final InputMethodSubtype[] subtypes =
+                AdditionalSubtype.createAdditionalSubtypesArray(prefSubtype);
+        final StringBuilder styles = new StringBuilder();
+        for (final InputMethodSubtype subtype : subtypes) {
+            if (styles.length() > 0) styles.append(", ");
+            styles.append(SubtypeLocale.getSubtypeDisplayName(subtype, res));
+        }
+        customInputStyles.setSummary(styles);
+    }
+
+    private void updateKeyPreviewPopupDelaySummary() {
+        final ListPreference lp = mKeyPreviewPopupDismissDelay;
+        final CharSequence[] entries = lp.getEntries();
+        if (entries == null || entries.length <= 0) return;
+        lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
+    }
+
+    private void updateVoiceModeSummary() {
+        mVoicePreference.setSummary(
+                getResources().getStringArray(R.array.voice_input_modes_summary)
+                        [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+    }
+
+    private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
+            final SharedPreferences sp, final Resources res) {
+        if (mKeypressVibrationDurationSettingsPref != null) {
+            final boolean hasVibratorHardware =
+                    AudioAndHapticFeedbackManager.getInstance().hasVibrator();
+            final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON,
+                    res.getBoolean(R.bool.config_default_vibration_enabled));
+            mKeypressVibrationDurationSettingsPref.setEnabled(
+                    hasVibratorHardware && vibrateOnByUser);
+        }
+
+        if (mKeypressSoundVolumeSettingsPref != null) {
+            final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
+                    res.getBoolean(R.bool.config_default_sound_enabled));
+            mKeypressSoundVolumeSettingsPref.setEnabled(soundOn);
+        }
+    }
+
+    private void showKeypressVibrationDurationSettingsDialog() {
+        final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
+        final Context context = getActivity();
+        final PreferenceScreen settingsPref = mKeypressVibrationDurationSettingsPref;
+        final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
+            @Override
+            public void onPositiveButtonClick(final SeekBarDialog dialog) {
+                final int ms = dialog.getValue();
+                sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, ms).apply();
+                if (settingsPref != null) {
+                    settingsPref.setSummary(dialog.getValueText());
+                }
+            }
+
+            @Override
+            public void onStopTrackingTouch(final SeekBarDialog dialog) {
+                final int ms = dialog.getValue();
+                AudioAndHapticFeedbackManager.getInstance().vibrate(ms);
+            }
+        };
+        final int currentMs = SettingsValues.getCurrentVibrationDuration(sp, getResources());
+        final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
+        builder.setTitle(R.string.prefs_keypress_vibration_duration_settings)
+                .setListener(listener)
+                .setMaxValue(AudioAndHapticFeedbackManager.MAX_KEYPRESS_VIBRATION_DURATION)
+                .setValueFromat(R.string.settings_keypress_vibration_duration)
+                .setValue(currentMs)
+                .create()
+                .show();
+    }
+
+    private static final int PERCENT_INT = 100;
+    private static final float PERCENT_FLOAT = 100.0f;
+
+    private static int getCurrentKeyPressSoundVolumePercent(final SharedPreferences sp,
+            final Resources res) {
+        return (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * PERCENT_FLOAT);
+    }
+
+    private void showKeypressSoundVolumeSettingDialog() {
+        final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
+        final Context context = getActivity();
+        final AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        final PreferenceScreen settingsPref = mKeypressSoundVolumeSettingsPref;
+        final SeekBarDialog.Listener listener = new SeekBarDialog.Adapter() {
+            @Override
+            public void onPositiveButtonClick(final SeekBarDialog dialog) {
+                final float volume = dialog.getValue() / PERCENT_FLOAT;
+                sp.edit().putFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, volume).apply();
+                if (settingsPref != null) {
+                    settingsPref.setSummary(dialog.getValueText());
+                }
+            }
+
+            @Override
+            public void onStopTrackingTouch(final SeekBarDialog dialog) {
+                final float volume = dialog.getValue() / PERCENT_FLOAT;
+                am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, volume);
+            }
+        };
+        final SeekBarDialog.Builder builder = new SeekBarDialog.Builder(context);
+        final int currentVolumeInt = getCurrentKeyPressSoundVolumePercent(sp, getResources());
+        builder.setTitle(R.string.prefs_keypress_sound_volume_settings)
+                .setListener(listener)
+                .setMaxValue(PERCENT_INT)
+                .setValue(currentVolumeInt)
+                .create()
+                .show();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 1576844..3940662 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -16,7 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -83,7 +82,7 @@
     public final boolean mGestureFloatingPreviewTextEnabled;
 
     // From the input box
-    private final InputAttributes mInputAttributes;
+    public final InputAttributes mInputAttributes;
 
     // Deduced settings
     public final int mKeypressVibrationDuration;
@@ -96,10 +95,8 @@
     private final boolean mVoiceKeyEnabled;
     private final boolean mVoiceKeyOnMain;
 
-    public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
-            final Context context) {
-        final Resources res = context.getResources();
-
+    public SettingsValues(final SharedPreferences prefs, final Resources res,
+            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);
@@ -121,7 +118,7 @@
                 res.getString(R.string.symbols_excluded_from_word_separators);
         mWordSeparators = createWordSeparators(mWeakSpaceStrippers, mWeakSpaceSwappers,
                 mSymbolsExcludedFromWordSeparators, res);
-        mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
+        mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
 
         // Store the input attributes
         if (null == inputAttributes) {
@@ -132,7 +129,7 @@
 
         // Get the settings preferences
         mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
-        mVibrateOn = isVibrateOn(context, prefs, res);
+        mVibrateOn = isVibrateOn(prefs, res);
         mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON,
                 res.getBoolean(R.bool.config_default_sound_enabled));
         mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res);
@@ -214,9 +211,8 @@
         throw new RuntimeException("Bug: visibility string is not configured correctly");
     }
 
-    private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
-            final Resources res) {
-        final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
+    private static boolean isVibrateOn(final SharedPreferences prefs, final Resources res) {
+        final boolean hasVibrator = AudioAndHapticFeedbackManager.getInstance().hasVibrator();
         return hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON,
                 res.getBoolean(R.bool.config_default_vibration_enabled));
     }
@@ -273,6 +269,7 @@
         return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
     }
 
+    // TODO: Clean up and move public helper methods to Settings class.
     // Public to access from KeyboardSwitcher. Should it have access to some
     // process-global instance instead?
     public static boolean isKeyPreviewPopupEnabled(final SharedPreferences prefs,
@@ -423,9 +420,4 @@
     public boolean isSameInputType(final EditorInfo editorInfo) {
         return mInputAttributes.isSameInputType(editorInfo);
     }
-
-    // For debug.
-    public String getInputAttributesDebugString() {
-        return mInputAttributes.toString();
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 6cb3942..a167849 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -31,14 +31,15 @@
 import java.util.Arrays;
 
 /**
- * An expandable dictionary that stores the words in the user unigram dictionary.
- *
- * Largely a copy of UserDictionary, will replace that class in the future.
+ * An expandable dictionary that stores the words in the user dictionary provider into a binary
+ * dictionary file to use it from native code.
  */
 public class UserBinaryDictionary extends ExpandableBinaryDictionary {
 
     // The user dictionary provider uses an empty string to mean "all languages".
     private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
+    private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250;
+    private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160;
 
     // TODO: use Words.SHORTCUT when we target JellyBean or above
     final static String SHORTCUT = "shortcut";
@@ -233,6 +234,19 @@
         mContext.startActivity(intent);
     }
 
+    private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) {
+        // The default frequency for the user dictionary is 250 for historical reasons.
+        // Latin IME considers a good value for the default user dictionary frequency
+        // is about 160 considering the scale we use. So we are scaling down the values.
+        if (defaultFrequency > Integer.MAX_VALUE / LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY) {
+            return (defaultFrequency / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY)
+                    * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY;
+        } else {
+            return (defaultFrequency * LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY)
+                    / HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY;
+        }
+    }
+
     private void addWords(final Cursor cursor) {
         final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
         clearFusionDictionary();
@@ -245,12 +259,13 @@
                 final String word = cursor.getString(indexWord);
                 final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
                 final int frequency = cursor.getInt(indexFrequency);
+                final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
-                    super.addWord(word, null, frequency);
+                    super.addWord(word, null, adjustedFrequency);
                 }
                 if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
-                    super.addWord(shortcut, word, frequency);
+                    super.addWord(shortcut, word, adjustedFrequency);
                 }
                 cursor.moveToNext();
             }
diff --git a/java/src/com/android/inputmethod/latin/VibratorUtils.java b/java/src/com/android/inputmethod/latin/VibratorUtils.java
deleted file mode 100644
index b6696ce..0000000
--- a/java/src/com/android/inputmethod/latin/VibratorUtils.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-import android.os.Vibrator;
-
-public final class VibratorUtils {
-    private static final VibratorUtils sInstance = new VibratorUtils();
-    private Vibrator mVibrator;
-
-    private VibratorUtils() {
-        // This utility class is not publicly instantiable.
-    }
-
-    public static VibratorUtils getInstance(Context context) {
-        if (sInstance.mVibrator == null) {
-            sInstance.mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
-        }
-        return sInstance;
-    }
-
-    public boolean hasVibrator() {
-        if (mVibrator == null) {
-            return false;
-        }
-        return mVibrator.hasVibrator();
-    }
-
-    public void vibrate(long milliseconds) {
-        if (mVibrator == null) {
-            return;
-        }
-        mVibrator.vibrate(milliseconds);
-    }
-}
diff --git a/java/src/com/android/inputmethod/research/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
new file mode 100644
index 0000000..f3302d8
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.research;
+
+import java.util.LinkedList;
+
+/**
+ * A buffer that holds a fixed number of LogUnits.
+ *
+ * LogUnits are added in and shifted out in temporal order.  Only a subset of the LogUnits are
+ * actual words; the other LogUnits do not count toward the word limit.  Once the buffer reaches
+ * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
+ * stay under the capacity limit.
+ *
+ * This variant of a LogBuffer has a limited memory footprint because of its limited size.  This
+ * makes it useful, for example, for recording a window of the user's most recent actions in case
+ * they want to report an observed error that they do not know how to reproduce.
+ */
+public class FixedLogBuffer extends LogBuffer {
+    /* package for test */ int mWordCapacity;
+    // The number of members of mLogUnits that are actual words.
+    private int mNumActualWords;
+
+    /**
+     * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
+     * unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
+     *
+     * @param wordCapacity maximum number of words
+     */
+    public FixedLogBuffer(final int wordCapacity) {
+        super();
+        if (wordCapacity <= 0) {
+            throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
+        }
+        mWordCapacity = wordCapacity;
+        mNumActualWords = 0;
+    }
+
+    protected int getNumActualWords() {
+        return mNumActualWords;
+    }
+
+    /**
+     * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
+     * (oldest first) if word capacity is reached.
+     */
+    @Override
+    public void shiftIn(final LogUnit newLogUnit) {
+        if (newLogUnit.getWord() == null) {
+            // This LogUnit isn't a word, so it doesn't count toward the word-limit.
+            super.shiftIn(newLogUnit);
+            return;
+        }
+        if (mNumActualWords == mWordCapacity) {
+            shiftOutThroughFirstWord();
+        }
+        super.shiftIn(newLogUnit);
+        mNumActualWords++; // Must be a word, or we wouldn't be here.
+    }
+
+    private void shiftOutThroughFirstWord() {
+        final LinkedList<LogUnit> logUnits = getLogUnits();
+        while (!logUnits.isEmpty()) {
+            final LogUnit logUnit = logUnits.removeFirst();
+            onShiftOut(logUnit);
+            if (logUnit.hasWord()) {
+                // Successfully shifted out a word-containing LogUnit and made space for the new
+                // LogUnit.
+                mNumActualWords--;
+                break;
+            }
+        }
+    }
+
+    /**
+     * Removes all LogUnits from the buffer without calling onShiftOut().
+     */
+    @Override
+    public void clear() {
+        super.clear();
+        mNumActualWords = 0;
+    }
+
+    /**
+     * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn.  LogUnits are
+     * removed in the order entered.  This method is not called when shiftOut is called directly.
+     *
+     * Base class does nothing; subclasses may override if they want to record non-privacy sensitive
+     * events that fall off the end.
+     */
+    protected void onShiftOut(final LogUnit logUnit) {
+    }
+
+    /**
+     * Called to deliberately remove the oldest LogUnit.  Usually called when draining the
+     * LogBuffer.
+     */
+    @Override
+    public LogUnit shiftOut() {
+        if (isEmpty()) {
+            return null;
+        }
+        final LogUnit logUnit = super.shiftOut();
+        if (logUnit.hasWord()) {
+            mNumActualWords--;
+        }
+        return logUnit;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
index cb331d7..1dfd01c 100644
--- a/java/src/com/android/inputmethod/research/JsonUtils.java
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -18,6 +18,7 @@
 
 import android.content.SharedPreferences;
 import android.util.JsonWriter;
+import android.view.MotionEvent;
 import android.view.inputmethod.CompletionInfo;
 
 import com.android.inputmethod.keyboard.Key;
@@ -27,6 +28,9 @@
 import java.io.IOException;
 import java.util.Map;
 
+/**
+ * Routines for mapping classes and variables to JSON representations for logging.
+ */
 /* package */ class JsonUtils {
     private JsonUtils() {
         // This utility class is not publicly instantiable.
@@ -100,4 +104,54 @@
         jsonWriter.endArray();
         jsonWriter.endObject();
     }
+
+    /* package */ static void writeJson(final MotionEvent me, final JsonWriter jsonWriter)
+            throws IOException {
+        jsonWriter.beginObject();
+        jsonWriter.name("pointerIds");
+        jsonWriter.beginArray();
+        final int pointerCount = me.getPointerCount();
+        for (int index = 0; index < pointerCount; index++) {
+            jsonWriter.value(me.getPointerId(index));
+        }
+        jsonWriter.endArray();
+
+        jsonWriter.name("xyt");
+        jsonWriter.beginArray();
+        final int historicalSize = me.getHistorySize();
+        for (int index = 0; index < historicalSize; index++) {
+            jsonWriter.beginObject();
+            jsonWriter.name("t");
+            jsonWriter.value(me.getHistoricalEventTime(index));
+            jsonWriter.name("d");
+            jsonWriter.beginArray();
+            for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
+                jsonWriter.beginObject();
+                jsonWriter.name("x");
+                jsonWriter.value(me.getHistoricalX(pointerIndex, index));
+                jsonWriter.name("y");
+                jsonWriter.value(me.getHistoricalY(pointerIndex, index));
+                jsonWriter.endObject();
+            }
+            jsonWriter.endArray();
+            jsonWriter.endObject();
+        }
+        jsonWriter.beginObject();
+        jsonWriter.name("t");
+        jsonWriter.value(me.getEventTime());
+        jsonWriter.name("d");
+        jsonWriter.beginArray();
+        for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) {
+            jsonWriter.beginObject();
+            jsonWriter.name("x");
+            jsonWriter.value(me.getX(pointerIndex));
+            jsonWriter.name("y");
+            jsonWriter.value(me.getY(pointerIndex));
+            jsonWriter.endObject();
+        }
+        jsonWriter.endArray();
+        jsonWriter.endObject();
+        jsonWriter.endArray();
+        jsonWriter.endObject();
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java
index a3c3e89..14e8d08 100644
--- a/java/src/com/android/inputmethod/research/LogBuffer.java
+++ b/java/src/com/android/inputmethod/research/LogBuffer.java
@@ -16,102 +16,44 @@
 
 package com.android.inputmethod.research;
 
-import com.android.inputmethod.latin.CollectionUtils;
-
 import java.util.LinkedList;
 
 /**
- * A buffer that holds a fixed number of LogUnits.
+ * Maintain a FIFO queue of LogUnits.
  *
- * LogUnits are added in and shifted out in temporal order.  Only a subset of the LogUnits are
- * actual words; the other LogUnits do not count toward the word limit.  Once the buffer reaches
- * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
- * stay under the capacity limit.
+ * This class provides an unbounded queue.  This is useful when the user is aware that their actions
+ * are being recorded, such as when they are trying to reproduce a bug.  In this case, there should
+ * not be artificial restrictions on how many events that can be saved.
  */
 public class LogBuffer {
-    protected final LinkedList<LogUnit> mLogUnits;
-    /* package for test */ int mWordCapacity;
-    // The number of members of mLogUnits that are actual words.
-    protected int mNumActualWords;
+    // TODO: Gracefully handle situations in which this LogBuffer is consuming too much memory.
+    // This may happen, for example, if the user has forgotten that data is being logged.
+    private final LinkedList<LogUnit> mLogUnits;
 
-    /**
-     * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
-     * unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
-     *
-     * @param wordCapacity maximum number of words
-     */
-    LogBuffer(final int wordCapacity) {
-        if (wordCapacity <= 0) {
-            throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
-        }
-        mLogUnits = CollectionUtils.newLinkedList();
-        mWordCapacity = wordCapacity;
-        mNumActualWords = 0;
+    public LogBuffer() {
+        mLogUnits = new LinkedList<LogUnit>();
     }
 
-    /**
-     * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
-     * (oldest first) if word capacity is reached.
-     */
-    public void shiftIn(LogUnit newLogUnit) {
-        if (newLogUnit.getWord() == null) {
-            // This LogUnit isn't a word, so it doesn't count toward the word-limit.
-            mLogUnits.add(newLogUnit);
-            return;
-        }
-        if (mNumActualWords == mWordCapacity) {
-            shiftOutThroughFirstWord();
-        }
-        mLogUnits.add(newLogUnit);
-        mNumActualWords++; // Must be a word, or we wouldn't be here.
+    protected LinkedList<LogUnit> getLogUnits() {
+        return mLogUnits;
     }
 
-    private void shiftOutThroughFirstWord() {
-        while (!mLogUnits.isEmpty()) {
-            final LogUnit logUnit = mLogUnits.removeFirst();
-            onShiftOut(logUnit);
-            if (logUnit.hasWord()) {
-                // Successfully shifted out a word-containing LogUnit and made space for the new
-                // LogUnit.
-                mNumActualWords--;
-                break;
-            }
-        }
-    }
-
-    /**
-     * Removes all LogUnits from the buffer without calling onShiftOut().
-     */
     public void clear() {
         mLogUnits.clear();
-        mNumActualWords = 0;
     }
 
-    /**
-     * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn.  LogUnits are
-     * removed in the order entered.  This method is not called when shiftOut is called directly.
-     *
-     * Base class does nothing; subclasses may override.
-     */
-    protected void onShiftOut(LogUnit logUnit) {
-    }
-
-    /**
-     * Called to deliberately remove the oldest LogUnit.  Usually called when draining the
-     * LogBuffer.
-     */
-    public LogUnit shiftOut() {
-        if (mLogUnits.isEmpty()) {
-            return null;
-        }
-        final LogUnit logUnit = mLogUnits.removeFirst();
-        if (logUnit.hasWord()) {
-            mNumActualWords--;
-        }
-        return logUnit;
+    public void shiftIn(final LogUnit logUnit) {
+        mLogUnits.add(logUnit);
     }
 
     public boolean isEmpty() {
         return mLogUnits.isEmpty();
     }
+
+    public LogUnit shiftOut() {
+        if (isEmpty()) {
+            return null;
+        }
+        return mLogUnits.removeFirst();
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 27c4027..bcb144f 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -19,6 +19,7 @@
 import android.content.SharedPreferences;
 import android.util.JsonWriter;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.inputmethod.CompletionInfo;
 
 import com.android.inputmethod.keyboard.Key;
@@ -189,6 +190,8 @@
                     JsonUtils.writeJson((Key[]) value, jsonWriter);
                 } else if (value instanceof SuggestedWords) {
                     JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
+                } else if (value instanceof MotionEvent) {
+                    JsonUtils.writeJson((MotionEvent) value, jsonWriter);
                 } else if (value == null) {
                     jsonWriter.nullValue();
                 } else {
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 0185e5f..bec21d7 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -22,15 +22,24 @@
 import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
+import java.util.LinkedList;
 import java.util.Random;
 
-public class MainLogBuffer extends LogBuffer {
+/**
+ * Provide a log buffer of fixed length that enforces privacy restrictions.
+ *
+ * The privacy restrictions include making sure that no numbers are logged, that all logged words
+ * are in the dictionary, and that words are recorded infrequently enough that the user's meaning
+ * cannot be easily determined.
+ */
+public class MainLogBuffer extends FixedLogBuffer {
     private static final String TAG = MainLogBuffer.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
 
     // The size of the n-grams logged.  E.g. N_GRAM_SIZE = 2 means to sample bigrams.
     private static final int N_GRAM_SIZE = 2;
-    // The number of words between n-grams to omit from the log.
+    // The number of words between n-grams to omit from the log.  If debugging, record 50% of all
+    // words.  Otherwise, only record 10%.
     private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES =
             ProductionFlag.IS_EXPERIMENTAL_DEBUG ? 2 : 18;
 
@@ -56,7 +65,7 @@
         mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod);
     }
 
-    public void setSuggest(Suggest suggest) {
+    public void setSuggest(final Suggest suggest) {
         mSuggest = suggest;
     }
 
@@ -108,9 +117,10 @@
         }
         // Check each word in the buffer.  If any word poses a privacy threat, we cannot upload the
         // complete buffer contents in detail.
-        final int length = mLogUnits.size();
+        final LinkedList<LogUnit> logUnits = getLogUnits();
+        final int length = logUnits.size();
         for (int i = 0; i < length; i++) {
-            final LogUnit logUnit = mLogUnits.get(i);
+            final LogUnit logUnit = logUnits.get(i);
             final String word = logUnit.getWord();
             if (word == null) {
                 // Digits outside words are a privacy threat.
@@ -133,7 +143,7 @@
     }
 
     @Override
-    protected void onShiftOut(LogUnit logUnit) {
+    protected void onShiftOut(final LogUnit logUnit) {
         if (mResearchLog != null) {
             mResearchLog.publish(logUnit, false /* isIncludingPrivateData */);
         }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 709746e..b1484e6 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -154,6 +154,11 @@
 
     private LogUnit mCurrentLogUnit = new LogUnit();
 
+    // Gestured or tapped words may be committed after the gesture of the next word has started.
+    // To ensure that the gesture data of the next word is not associated with the previous word,
+    // thereby leaking private data, we store the time of the down event that started the second
+    // gesture, and when committing the earlier word, split the LogUnit.
+    private long mSavedDownEventTime;
     private ResearchLogger() {
         mStatistics = Statistics.getInstance();
     }
@@ -377,7 +382,7 @@
             mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
             // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
             // the feedback LogUnit itself.
-            mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
+            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
         }
     }
 
@@ -638,7 +643,6 @@
         mMainKeyboardView.invalidateAllKeys();
     }
 
-
     public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
             int height) {
         // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
@@ -741,9 +745,15 @@
         return false;
     }
 
+    /**
+     * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit.
+     *
+     * After this operation completes, mCurrentLogUnit will hold any logStatements that happened
+     * after maxTime.
+     */
     private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS =
             new LogStatement("recordSplitWords", true, false);
-    public void onWordComplete(final String word, final long maxTime) {
+    /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime) {
         final Dictionary dictionary = getDictionary();
         if (word != null && word.length() > 0 && hasLetters(word)) {
             mCurrentLogUnit.setWord(word);
@@ -757,6 +767,11 @@
         mCurrentLogUnit = newLogUnit;
     }
 
+    public void onWordFinished(final String word) {
+        commitCurrentLogUnitAsWord(word, mSavedDownEventTime);
+        mSavedDownEventTime = Long.MAX_VALUE;
+    }
+
     private static int scrubDigitFromCodePoint(int codePoint) {
         return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
     }
@@ -880,8 +895,7 @@
      *
      */
     private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
-            new LogStatement("MainKeyboardViewProcessMotionEvent", true, false, "action",
-                    "eventTime", "id", "x", "y", "size", "pressure");
+            new LogStatement("MotionEvent", true, false, "action", "MotionEvent");
     public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
             final long eventTime, final int index, final int id, final int x, final int y) {
         if (me != null) {
@@ -896,10 +910,14 @@
                 case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
                 default: actionString = "ACTION_" + action; break;
             }
-            final float size = me.getSize(index);
-            final float pressure = me.getPressure(index);
-            getInstance().enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
-                    actionString, eventTime, id, x, y, size, pressure);
+            final ResearchLogger researchLogger = getInstance();
+            researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
+                    actionString, MotionEvent.obtain(me));
+            if (action == MotionEvent.ACTION_DOWN) {
+                // Subtract 1 from eventTime so the down event is included in the later
+                // LogUnit, not the earlier (the test is for inequality).
+                researchLogger.mSavedDownEventTime = eventTime - 1;
+            }
         }
     }
 
@@ -1038,6 +1056,16 @@
     }
 
     /**
+     * Log a call to LatinIME.onTextInput().
+     *
+     * SystemResponse: Raw text is added to the TextView.
+     */
+    public static void latinIME_onTextInput(final String text) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE);
+    }
+
+    /**
      * Log a call to LatinIME.pickSuggestionManually().
      *
      * UserAction: The user has chosen a specific word from the suggestion strip.
@@ -1053,7 +1081,7 @@
                 scrubDigitsFromString(replacedWord), index,
                 suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
                 Constants.SUGGESTION_STRIP_COORDINATE);
-        researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE);
+        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE);
         researchLogger.mStatistics.recordManualSuggestion();
     }
 
@@ -1069,7 +1097,7 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
                 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
-        researchLogger.onWordComplete(suggestion, Long.MAX_VALUE);
+        researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE);
     }
 
     /**
@@ -1098,8 +1126,20 @@
      */
     private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
             new LogStatement("LatinIMESwapSwapperAndSpace", false, false);
-    public static void latinIME_swapSwapperAndSpace() {
-        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE);
+    public static void latinIME_swapSwapperAndSpace(final String text) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE);
+        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE);
+    }
+
+    /**
+     * Log a call to LatinIME.maybeDoubleSpacePeriod().
+     *
+     * SystemResponse: Two spaces have been replaced by period space.
+     */
+    public static void latinIME_maybeDoubleSpacePeriod(final String text) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE);
     }
 
     /**
@@ -1156,6 +1196,7 @@
         researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord,
                 originallyTypedWord);
         researchLogger.mStatistics.recordRevertCommit();
+        researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE);
     }
 
     /**
@@ -1250,6 +1291,26 @@
     }
 
     /**
+     * Log a call to RichInputConnection.revertDoubleSpacePeriod().
+     *
+     * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces.
+     */
+    public static void richInputConnection_revertDoubleSpacePeriod(final String doubleSpace) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.commitCurrentLogUnitAsWord(doubleSpace, Long.MAX_VALUE);
+    }
+
+    /**
+     * Log a call to RichInputConnection.revertSwapPunctuation().
+     *
+     * SystemResponse: The IME has reverted a punctuation swap.
+     */
+    public static void richInputConnection_revertSwapPunctuation(final String text) {
+        final ResearchLogger researchLogger = getInstance();
+        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE);
+    }
+
+    /**
      * Log a call to LatinIME.commitCurrentAutoCorrection().
      *
      * SystemResponse: The IME has committed an auto-correction.  An auto-correction changes the raw
@@ -1265,7 +1326,7 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION,
                 scrubbedTypedWord, scrubbedAutoCorrection, separatorString);
-        researchLogger.onWordComplete(scrubbedAutoCorrection, Long.MAX_VALUE);
+        researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE);
     }
 
     private boolean isExpectingCommitText = false;
@@ -1284,7 +1345,7 @@
         final ResearchLogger researchLogger = getInstance();
         final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
         researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT);
-        researchLogger.onWordComplete(scrubbedWord, lastTimestampOfWordData);
+        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData);
         researchLogger.mStatistics.recordSplitWords();
     }
 
@@ -1303,7 +1364,7 @@
         if (!researchLogger.isExpectingCommitText) {
             researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
                     newCursorPosition);
-            researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE);
+            researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE);
         }
         researchLogger.isExpectingCommitText = false;
     }
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
index 2423bb5..4c83c58 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2011, The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
index 51fa895..c3503c8 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2011, The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,8 +20,6 @@
 #include "jni.h"
 
 namespace latinime {
-
 int register_ProximityInfo(JNIEnv *env);
-
 } // namespace latinime
 #endif // _COM_ANDROID_INPUTMETHOD_KEYBOARD_PROXIMITYINFO_H
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index c0b858e..4e34f98 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2009, The Android Open Source Project
+ * Copyright (C) 2009 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-
 #include <cstring> // for memset()
 
 #define LOG_TAG "LatinIME: jni: BinaryDictionary"
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.h b/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
index b9e944f..2a07f99 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2011, The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,8 +20,6 @@
 #include "jni.h"
 
 namespace latinime {
-
 int register_BinaryDictionary(JNIEnv *env);
-
 } // namespace latinime
 #endif // _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARY_H
diff --git a/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
index 7bb8dc5..73ac84c 100644
--- a/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.cpp
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2012, The Android Open Source Project
+ * Copyright (C) 2012 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/native/jni/com_android_inputmethod_latin_DicTraverseSession.h b/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
index 37531e9..badcbb9 100644
--- a/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
+++ b/native/jni/com_android_inputmethod_latin_DicTraverseSession.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2012, The Android Open Source Project
+ * Copyright (C) 2012 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,7 +17,6 @@
 #ifndef _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
 #define _COM_ANDROID_INPUTMETHOD_LATIN_DICTRAVERSESESSION_H
 
-#include "defines.h"
 #include "jni.h"
 
 namespace latinime {
diff --git a/native/jni/jni_common.cpp b/native/jni/jni_common.cpp
index 0da1669..7b97cf4 100644
--- a/native/jni/jni_common.cpp
+++ b/native/jni/jni_common.cpp
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2011, The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -25,45 +25,38 @@
 #include "jni.h"
 #include "jni_common.h"
 
-using namespace latinime;
-
 /*
  * Returns the JNI version on success, -1 on failure.
  */
 jint JNI_OnLoad(JavaVM *vm, void *reserved) {
     JNIEnv *env = 0;
-    jint result = -1;
 
     if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
         AKLOGE("ERROR: GetEnv failed");
-        goto bail;
+        return -1;
     }
     assert(env);
-
-    if (!register_BinaryDictionary(env)) {
+    if (!env) {
+        AKLOGE("ERROR: JNIEnv is invalid");
+        return -1;
+    }
+    if (!latinime::register_BinaryDictionary(env)) {
         AKLOGE("ERROR: BinaryDictionary native registration failed");
-        goto bail;
+        return -1;
     }
-
-    if (!register_DicTraverseSession(env)) {
+    if (!latinime::register_DicTraverseSession(env)) {
         AKLOGE("ERROR: DicTraverseSession native registration failed");
-        goto bail;
+        return -1;
     }
-
-    if (!register_ProximityInfo(env)) {
+    if (!latinime::register_ProximityInfo(env)) {
         AKLOGE("ERROR: ProximityInfo native registration failed");
-        goto bail;
+        return -1;
     }
-
     /* success -- return valid version number */
-    result = JNI_VERSION_1_6;
-
-bail:
-    return result;
+    return JNI_VERSION_1_6;
 }
 
 namespace latinime {
-
 int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods,
         int numMethods) {
     jclass clazz = env->FindClass(className);
diff --git a/native/jni/jni_common.h b/native/jni/jni_common.h
index 993f97e..f960b05 100644
--- a/native/jni/jni_common.h
+++ b/native/jni/jni_common.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, The Android Open Source Project
+ * Copyright (C) 2011 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,9 +20,7 @@
 #include "jni.h"
 
 namespace latinime {
-
 int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods,
         int numMethods);
-
 } // namespace latinime
 #endif // LATINIME_JNI_COMMON_H
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index 46ca911..24221c9 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -71,8 +71,7 @@
     mTotalTraverseCount = 0;
 }
 
-void Correction::initCorrection(const ProximityInfo *pi, const int inputSize,
-        const int maxDepth) {
+void Correction::initCorrection(const ProximityInfo *pi, const int inputSize, const int maxDepth) {
     mProximityInfo = pi;
     mInputSize = inputSize;
     mMaxDepth = maxDepth;
@@ -168,8 +167,7 @@
     return true;
 }
 
-int Correction::goDownTree(
-        const int parentIndex, const int childCount, const int firstChildPos) {
+int Correction::goDownTree(const int parentIndex, const int childCount, const int firstChildPos) {
     mCorrectionStates[mOutputIndex].mParentIndex = parentIndex;
     mCorrectionStates[mOutputIndex].mChildCount = childCount;
     mCorrectionStates[mOutputIndex].mSiblingPos = firstChildPos;
@@ -532,8 +530,7 @@
 // RankingAlgorithm //
 //////////////////////
 
-/* static */
-int Correction::RankingAlgorithm::calculateFinalProbability(const int inputIndex,
+/* static */ int Correction::RankingAlgorithm::calculateFinalProbability(const int inputIndex,
         const int outputIndex, const int freq, int *editDistanceTable, const Correction *correction,
         const int inputSize) {
     const int excessivePos = correction->getExcessivePos();
@@ -794,10 +791,9 @@
     return finalFreq;
 }
 
-/* static */
-int Correction::RankingAlgorithm::calcFreqForSplitMultipleWords(
-        const int *freqArray, const int *wordLengthArray, const int wordCount,
-        const Correction *correction, const bool isSpaceProximity, const int *word) {
+/* static */ int Correction::RankingAlgorithm::calcFreqForSplitMultipleWords(const int *freqArray,
+        const int *wordLengthArray, const int wordCount, const Correction *correction,
+        const bool isSpaceProximity, const int *word) {
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
 
     bool firstCapitalizedWordDemotion = false;
@@ -965,8 +961,8 @@
     return dp[li * lo - 1];
 }
 
-int Correction::RankingAlgorithm::editDistance(const int *before, const int beforeLength,
-        const int *after, const int afterLength) {
+/* static */ int Correction::RankingAlgorithm::editDistance(const int *before,
+        const int beforeLength, const int *after, const int afterLength) {
     int table[(beforeLength + 1) * (afterLength + 1)];
     return editDistanceInternal(table, before, beforeLength, after, afterLength);
 }
@@ -993,9 +989,8 @@
 // the result.
 // So, we can normalize original score by dividing powf(2, min(b.l(),a.l())) * 255 * 2.
 
-/* static */
-float Correction::RankingAlgorithm::calcNormalizedScore(const int *before, const int beforeLength,
-        const int *after, const int afterLength, const int score) {
+/* static */ float Correction::RankingAlgorithm::calcNormalizedScore(const int *before,
+        const int beforeLength, const int *after, const int afterLength, const int score) {
     if (0 == beforeLength || 0 == afterLength) {
         return 0.0f;
     }
diff --git a/native/jni/src/correction.h b/native/jni/src/correction.h
index 4184c64..0469355 100644
--- a/native/jni/src/correction.h
+++ b/native/jni/src/correction.h
@@ -58,8 +58,7 @@
     // Non virtual inline destructor -- never inherit this class
     ~Correction() {}
     void resetCorrection();
-    void initCorrection(
-            const ProximityInfo *pi, const int inputSize, const int maxWordLength);
+    void initCorrection(const ProximityInfo *pi, const int inputSize, const int maxWordLength);
     void initCorrectionState(const int rootPos, const int childCount, const bool traverseAll);
 
     // TODO: remove
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 7b127a2..46595d8 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2010, The Android Open Source Project
+ * Copyright (C) 2010 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/native/jni/src/dic_traverse_wrapper.h b/native/jni/src/dic_traverse_wrapper.h
index 22cf1b1..9a1db38 100644
--- a/native/jni/src/dic_traverse_wrapper.h
+++ b/native/jni/src/dic_traverse_wrapper.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2012, The Android Open Source Project
+ * Copyright (C) 2012 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/native/jni/src/hash_map_compat.h b/native/jni/src/hash_map_compat.h
index 116359a..a1e982b 100644
--- a/native/jni/src/hash_map_compat.h
+++ b/native/jni/src/hash_map_compat.h
@@ -1,11 +1,11 @@
 /*
- * Copyright (C) 2012, The Android Open Source Project
+ * Copyright (C) 2012 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *      http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/native/jni/src/suggest/suggest_interface.h b/native/jni/src/suggest/suggest_interface.h
index de58e79..0fb5426 100644
--- a/native/jni/src/suggest/suggest_interface.h
+++ b/native/jni/src/suggest/suggest_interface.h
@@ -28,8 +28,8 @@
     virtual int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs,
             int *inputYs, int *times, int *pointerIds, int *codes, int inputSize, int commitPoint,
             int *outWords, int *frequencies, int *outputIndices, int *outputTypes) const = 0;
-    SuggestInterface() {};
-    virtual ~SuggestInterface() {};
+    SuggestInterface() {}
+    virtual ~SuggestInterface() {}
  private:
     DISALLOW_COPY_AND_ASSIGN(SuggestInterface);
 };
diff --git a/native/jni/src/unigram_dictionary.cpp b/native/jni/src/unigram_dictionary.cpp
index def4a5b..ebeef13 100644
--- a/native/jni/src/unigram_dictionary.cpp
+++ b/native/jni/src/unigram_dictionary.cpp
@@ -365,7 +365,7 @@
 }
 
 void UnigramDictionary::onTerminal(const int probability,
-        const TerminalAttributes& terminalAttributes, Correction *correction,
+        const TerminalAttributes &terminalAttributes, Correction *correction,
         WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
         const int currentWordIndex) const {
     const int inputIndex = correction->getInputIndex();
@@ -390,8 +390,7 @@
 
         const int shortcutProbability = finalProbability > 0 ? finalProbability - 1 : 0;
         // Please note that the shortcut candidates will be added to the master queue only.
-        TerminalAttributes::ShortcutIterator iterator =
-                terminalAttributes.getShortcutIterator();
+        TerminalAttributes::ShortcutIterator iterator = terminalAttributes.getShortcutIterator();
         while (iterator.hasNextShortcutTarget()) {
             // TODO: addWord only supports weak ordering, meaning we have no means
             // to control the order of the shortcuts relative to one another or to the word.
diff --git a/native/jni/src/unigram_dictionary.h b/native/jni/src/unigram_dictionary.h
index 3162e46..f5850b4 100644
--- a/native/jni/src/unigram_dictionary.h
+++ b/native/jni/src/unigram_dictionary.h
@@ -59,14 +59,13 @@
             WordsPriorityQueuePool *queuePool) const;
     int getDigraphReplacement(const int *codes, const int i, const int codesSize,
             const digraph_t *const digraphs, const unsigned int digraphsSize) const;
-    void getWordWithDigraphSuggestionsRec(ProximityInfo *proximityInfo,
-        const int *xcoordinates, const int *ycoordinates, const int *codesBuffer,
-        int *xCoordinatesBuffer, int *yCoordinatesBuffer, const int codesBufferSize,
-        const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
-        const bool useFullEditDistance, const int *codesSrc, const int codesRemain,
-        const int currentDepth, int *codesDest, Correction *correction,
-        WordsPriorityQueuePool *queuePool, const digraph_t *const digraphs,
-        const unsigned int digraphsSize) const;
+    void getWordWithDigraphSuggestionsRec(ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codesBuffer, int *xCoordinatesBuffer,
+            int *yCoordinatesBuffer, const int codesBufferSize, const std::map<int, int> *bigramMap,
+            const uint8_t *bigramFilter, const bool useFullEditDistance, const int *codesSrc,
+            const int codesRemain, const int currentDepth, int *codesDest, Correction *correction,
+            WordsPriorityQueuePool *queuePool, const digraph_t *const digraphs,
+            const unsigned int digraphsSize) const;
     void initSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
             const int *ycoordinates, const int *codes, const int codesSize,
             Correction *correction) const;
@@ -79,12 +78,11 @@
             const std::map<int, int> *bigramMap, const uint8_t *bigramFilter,
             Correction *correction, WordsPriorityQueuePool *queuePool, const bool doAutoCompletion,
             const int maxErrors, const int currentWordIndex) const;
-    void getSplitMultipleWordsSuggestions(ProximityInfo *proximityInfo,
-            const int *xcoordinates, const int *ycoordinates, const int *codes,
-            const bool useFullEditDistance, const int inputSize,
-            Correction *correction, WordsPriorityQueuePool *queuePool,
+    void getSplitMultipleWordsSuggestions(ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codes, const bool useFullEditDistance,
+            const int inputSize, Correction *correction, WordsPriorityQueuePool *queuePool,
             const bool hasAutoCorrectionCandidate) const;
-    void onTerminal(const int freq, const TerminalAttributes& terminalAttributes,
+    void onTerminal(const int freq, const TerminalAttributes &terminalAttributes,
             Correction *correction, WordsPriorityQueuePool *queuePool, const bool addToMasterQueue,
             const int currentWordIndex) const;
     // Process a node by considering proximity, missing and excessive character
@@ -96,14 +94,13 @@
             Correction *correction, int *word) const;
     int getMostFrequentWordLikeInner(const int *const inWord, const int inputSize,
             int *outWord) const;
-    int getSubStringSuggestion(
-            ProximityInfo *proximityInfo, const int *xcoordinates, const int *ycoordinates,
-            const int *codes, const bool useFullEditDistance, Correction *correction,
-            WordsPriorityQueuePool *queuePool, const int inputSize,
+    int getSubStringSuggestion(ProximityInfo *proximityInfo, const int *xcoordinates,
+            const int *ycoordinates, const int *codes, const bool useFullEditDistance,
+            Correction *correction, WordsPriorityQueuePool *queuePool, const int inputSize,
             const bool hasAutoCorrectionCandidate, const int currentWordIndex,
-            const int inputWordStartPos, const int inputWordLength,
-            const int outputWordStartPos, const bool isSpaceProximity, int *freqArray,
-            int *wordLengthArray, int *outputWord, int *outputWordLength) const;
+            const int inputWordStartPos, const int inputWordLength, const int outputWordStartPos,
+            const bool isSpaceProximity, int *freqArray, int *wordLengthArray, int *outputWord,
+            int *outputWordLength) const;
     void getMultiWordsSuggestionRec(ProximityInfo *proximityInfo, const int *xcoordinates,
             const int *ycoordinates, const int *codes, const bool useFullEditDistance,
             const int inputSize, Correction *correction, WordsPriorityQueuePool *queuePool,
diff --git a/native/jni/src/words_priority_queue.h b/native/jni/src/words_priority_queue.h
index e613d2f..84b4b48 100644
--- a/native/jni/src/words_priority_queue.h
+++ b/native/jni/src/words_priority_queue.h
@@ -28,7 +28,7 @@
 class WordsPriorityQueue {
  public:
     class SuggestedWord {
-    public:
+     public:
         int mScore;
         int mWord[MAX_WORD_LENGTH_INTERNAL];
         int mWordLength;
@@ -122,7 +122,7 @@
     AK_FORCE_INLINE float getHighestNormalizedScore(const int *before, const int beforeLength,
             int **outWord, int *outScore, int *outLength) {
         if (!mHighestSuggestedWord) {
-            return 0.0;
+            return 0.0f;
         }
         return getNormalizedScore(mHighestSuggestedWord, before, beforeLength, outWord, outScore,
                 outLength);