Merge "Revert "Reorganize Keyboard settings screen""
diff --git a/dictionaries/es_wordlist.combined.gz b/dictionaries/es_wordlist.combined.gz
index 181a958..c0a5264 100644
--- a/dictionaries/es_wordlist.combined.gz
+++ b/dictionaries/es_wordlist.combined.gz
Binary files differ
diff --git a/java/res/layout/key_preview.xml b/java/res/layout/key_preview.xml
deleted file mode 100644
index 16d4c72..0000000
--- a/java/res/layout/key_preview.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2013, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:minWidth="32dp"
-    android:gravity="center"
-    style="?attr/keyPreviewTextViewStyle"
-/>
diff --git a/java/res/raw/main_es.dict b/java/res/raw/main_es.dict
index 83eefe4..0911b70 100644
--- a/java/res/raw/main_es.dict
+++ b/java/res/raw/main_es.dict
Binary files differ
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index a1f478b..fcb919d 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -26,8 +26,6 @@
         <attr name="keyboardViewStyle" format="reference" />
         <!-- MainKeyboardView style -->
         <attr name="mainKeyboardViewStyle" format="reference" />
-        <!-- Key preview text view style -->
-        <attr name="keyPreviewTextViewStyle" format="reference"/>
         <!-- EmojiPalettesView style -->
         <attr name="emojiPalettesViewStyle" format="reference" />
         <!-- MoreKeysKeyboard style -->
@@ -106,8 +104,8 @@
         <attr name="longPressShiftLockTimeout" format="integer" />
         <!-- Ignore special key timeout while typing in millisecond. -->
         <attr name="ignoreAltCodeKeyTimeout" format="integer" />
-        <!-- Layout resource for key press feedback.-->
-        <attr name="keyPreviewLayout" format="reference" />
+        <!-- Background resource for key press feedback.-->
+        <attr name="keyPreviewBackground" format="reference" />
         <!-- Vertical offset of the key press feedback from the key. -->
         <attr name="keyPreviewOffset" format="dimension" />
         <!-- Height of the key press feedback popup. -->
diff --git a/java/res/values/themes-common.xml b/java/res/values/themes-common.xml
index 02a93ca..2b2a80a 100644
--- a/java/res/values/themes-common.xml
+++ b/java/res/values/themes-common.xml
@@ -75,7 +75,6 @@
         <item name="keyRepeatInterval">@integer/config_key_repeat_interval</item>
         <item name="longPressShiftLockTimeout">@integer/config_longpress_shift_lock_timeout</item>
         <item name="ignoreAltCodeKeyTimeout">@integer/config_ignore_alt_code_key_timeout</item>
-        <item name="keyPreviewLayout">@layout/key_preview</item>
         <item name="keyPreviewHeight">@dimen/config_key_preview_height</item>
         <!-- TODO: consolidate key preview linger timeout with the key preview animation parameters. -->
         <item name="keyPreviewLingerTimeout">@integer/config_key_preview_linger_timeout</item>
@@ -106,7 +105,6 @@
     <style
         name="MainKeyboardView"
         parent="KeyboardView" />
-    <style name="KeyPreviewTextView" />
     <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style name="EmojiPalettesView" />
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index 319b4ae..073ae90 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -23,7 +23,6 @@
         <item name="keyboardStyle">@style/Keyboard.ICS</item>
         <item name="keyboardViewStyle">@style/KeyboardView.ICS</item>
         <item name="mainKeyboardViewStyle">@style/MainKeyboardView.ICS</item>
-        <item name="keyPreviewTextViewStyle">@style/KeyPreviewTextView.ICS</item>
         <item name="emojiPalettesViewStyle">@style/EmojiPalettesView.ICS</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.ICS</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.ICS</item>
@@ -66,6 +65,7 @@
         name="MainKeyboardView.ICS"
         parent="KeyboardView.ICS"
     >
+        <item name="keyPreviewBackground">@drawable/keyboard_key_feedback_ics</item>
         <item name="keyPreviewOffset">@dimen/config_key_preview_offset_holo</item>
         <item name="gestureFloatingPreviewTextColor">@color/highlight_color_ics</item>
         <item name="gestureFloatingPreviewColor">@color/gesture_floating_preview_color_holo</item>
@@ -75,12 +75,6 @@
         <item name="languageOnSpacebarTextShadowRadius">1.0</item>
         <item name="languageOnSpacebarTextShadowColor">@color/spacebar_text_shadow_color_holo</item>
     </style>
-    <style
-        name="KeyPreviewTextView.ICS"
-        parent="KeyPreviewTextView"
-    >
-        <item name="android:background">@drawable/keyboard_key_feedback_ics</item>
-    </style>
     <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
diff --git a/java/res/values/themes-klp.xml b/java/res/values/themes-klp.xml
index 208723d..f895de5 100644
--- a/java/res/values/themes-klp.xml
+++ b/java/res/values/themes-klp.xml
@@ -23,7 +23,6 @@
         <item name="keyboardStyle">@style/Keyboard.KLP</item>
         <item name="keyboardViewStyle">@style/KeyboardView.KLP</item>
         <item name="mainKeyboardViewStyle">@style/MainKeyboardView.KLP</item>
-        <item name="keyPreviewTextViewStyle">@style/KeyPreviewTextView.KLP</item>
         <item name="emojiPalettesViewStyle">@style/EmojiPalettesView.KLP</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.KLP</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.KLP</item>
@@ -66,6 +65,7 @@
         name="MainKeyboardView.KLP"
         parent="KeyboardView.KLP"
     >
+        <item name="keyPreviewBackground">@drawable/keyboard_key_feedback_klp</item>
         <item name="keyPreviewOffset">@dimen/config_key_preview_offset_holo</item>
         <item name="gestureFloatingPreviewTextColor">@color/highlight_color_klp</item>
         <item name="gestureFloatingPreviewColor">@color/gesture_floating_preview_color_holo</item>
@@ -75,12 +75,6 @@
         <item name="languageOnSpacebarTextShadowRadius">1.0</item>
         <item name="languageOnSpacebarTextShadowColor">@color/spacebar_text_shadow_color_holo</item>
     </style>
-    <style
-        name="KeyPreviewTextView.KLP"
-        parent="KeyPreviewTextView"
-    >
-        <item name="android:background">@drawable/keyboard_key_feedback_klp</item>
-    </style>
     <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
diff --git a/java/res/values/themes-lxx-dark.xml b/java/res/values/themes-lxx-dark.xml
index e9a295c..1db8f42 100644
--- a/java/res/values/themes-lxx-dark.xml
+++ b/java/res/values/themes-lxx-dark.xml
@@ -23,7 +23,6 @@
         <item name="keyboardStyle">@style/Keyboard.LXX_Dark</item>
         <item name="keyboardViewStyle">@style/KeyboardView.LXX_Dark</item>
         <item name="mainKeyboardViewStyle">@style/MainKeyboardView.LXX_Dark</item>
-        <item name="keyPreviewTextViewStyle">@style/KeyPreviewTextView.LXX_Dark</item>
         <item name="emojiPalettesViewStyle">@style/EmojiPalettesView.LXX_Dark</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.LXX_Dark</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.LXX_Dark</item>
@@ -67,6 +66,7 @@
         name="MainKeyboardView.LXX_Dark"
         parent="KeyboardView.LXX_Dark"
     >
+        <item name="keyPreviewBackground">@drawable/keyboard_key_feedback_lxx_dark</item>
         <item name="keyPreviewOffset">@dimen/config_key_preview_offset_holo</item>
         <item name="gestureFloatingPreviewTextColor">@color/highlight_color_lxx_dark</item>
         <item name="gestureFloatingPreviewColor">@color/gesture_floating_preview_color_lxx_dark</item>
@@ -76,12 +76,6 @@
         <!-- A negative value to disable text shadow layer. -->
         <item name="languageOnSpacebarTextShadowRadius">-1.0</item>
     </style>
-    <style
-        name="KeyPreviewTextView.LXX_Dark"
-        parent="KeyPreviewTextView"
-    >
-        <item name="android:background">@drawable/keyboard_key_feedback_lxx_dark</item>
-    </style>
     <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 53628f7..8182593 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -664,10 +664,6 @@
     @Override
     public void onDismissMoreKeysPanel() {
         dimEntireKeyboard(false /* dimmed */);
-        dismissMoreKeysPanel();
-    }
-
-    private void dismissMoreKeysPanel() {
         if (isShowingMoreKeysPanel()) {
             mMoreKeysPanel.removeFromParent();
             mMoreKeysPanel = null;
@@ -733,7 +729,7 @@
     }
 
     public void onHideWindow() {
-        dismissMoreKeysPanel();
+        onDismissMoreKeysPanel();
         final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
         if (accessibilityDelegate != null) {
             accessibilityDelegate.onHideWindow();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
index d4c6710..6fc300b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
@@ -21,17 +21,12 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.DecelerateInterpolator;
-import android.widget.TextView;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.ViewLayoutUtils;
 
@@ -46,10 +41,11 @@
  * - how key previews should be shown and dismissed.
  */
 public final class KeyPreviewChoreographer {
-    // Free {@link TextView} pool that can be used for key preview.
-    private final ArrayDeque<TextView> mFreeKeyPreviewTextViews = new ArrayDeque<>();
-    // Map from {@link Key} to {@link TextView} that is currently being displayed as key preview.
-    private final HashMap<Key,TextView> mShowingKeyPreviewTextViews = new HashMap<>();
+    // Free {@link KeyPreviewView} pool that can be used for key preview.
+    private final ArrayDeque<KeyPreviewView> mFreeKeyPreviewViews = new ArrayDeque<>();
+    // Map from {@link Key} to {@link KeyPreviewView} that is currently being displayed as key
+    // preview.
+    private final HashMap<Key,KeyPreviewView> mShowingKeyPreviewViews = new HashMap<>();
 
     private final KeyPreviewDrawParams mParams;
 
@@ -57,32 +53,28 @@
         mParams = params;
     }
 
-    private TextView getKeyPreviewTextView(final Key key, final ViewGroup placerView) {
-        TextView previewTextView = mShowingKeyPreviewTextViews.remove(key);
-        if (previewTextView != null) {
-            return previewTextView;
+    public KeyPreviewView getKeyPreviewView(final Key key, final ViewGroup placerView) {
+        KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.remove(key);
+        if (keyPreviewView != null) {
+            return keyPreviewView;
         }
-        previewTextView = mFreeKeyPreviewTextViews.poll();
-        if (previewTextView != null) {
-            return previewTextView;
+        keyPreviewView = mFreeKeyPreviewViews.poll();
+        if (keyPreviewView != null) {
+            return keyPreviewView;
         }
         final Context context = placerView.getContext();
-        if (mParams.mLayoutId != 0) {
-            previewTextView = (TextView)LayoutInflater.from(context)
-                    .inflate(mParams.mLayoutId, null);
-        } else {
-            previewTextView = new TextView(context);
-        }
-        placerView.addView(previewTextView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0));
-        return previewTextView;
+        keyPreviewView = new KeyPreviewView(context, null /* attrs */);
+        keyPreviewView.setBackgroundResource(mParams.mPreviewBackgroundResId);
+        placerView.addView(keyPreviewView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0));
+        return keyPreviewView;
     }
 
     public boolean isShowingKeyPreview(final Key key) {
-        return mShowingKeyPreviewTextViews.containsKey(key);
+        return mShowingKeyPreviewViews.containsKey(key);
     }
 
     public void dismissAllKeyPreviews() {
-        for (final Key key : new HashSet<>(mShowingKeyPreviewTextViews.keySet())) {
+        for (final Key key : new HashSet<>(mShowingKeyPreviewViews.keySet())) {
             dismissKeyPreview(key, false /* withAnimation */);
         }
     }
@@ -91,11 +83,11 @@
         if (key == null) {
             return;
         }
-        final TextView previewTextView = mShowingKeyPreviewTextViews.get(key);
-        if (previewTextView == null) {
+        final KeyPreviewView keyPreviewView = mShowingKeyPreviewViews.get(key);
+        if (keyPreviewView == null) {
             return;
         }
-        final Object tag = previewTextView.getTag();
+        final Object tag = keyPreviewView.getTag();
         if (withAnimation) {
             if (tag instanceof KeyPreviewAnimations) {
                 final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag;
@@ -104,114 +96,76 @@
             }
         }
         // Dismiss preview without animation.
-        mShowingKeyPreviewTextViews.remove(key);
+        mShowingKeyPreviewViews.remove(key);
         if (tag instanceof Animator) {
             ((Animator)tag).cancel();
         }
-        previewTextView.setTag(null);
-        previewTextView.setVisibility(View.INVISIBLE);
-        mFreeKeyPreviewTextViews.add(previewTextView);
+        keyPreviewView.setTag(null);
+        keyPreviewView.setVisibility(View.INVISIBLE);
+        mFreeKeyPreviewViews.add(keyPreviewView);
     }
 
-    // Background state set
-    private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
-        { // STATE_MIDDLE
-            {},
-            { R.attr.state_has_morekeys }
-        },
-        { // STATE_LEFT
-            { R.attr.state_left_edge },
-            { R.attr.state_left_edge, R.attr.state_has_morekeys }
-        },
-        { // STATE_RIGHT
-            { R.attr.state_right_edge },
-            { R.attr.state_right_edge, R.attr.state_has_morekeys }
-        }
-    };
-    private static final int STATE_MIDDLE = 0;
-    private static final int STATE_LEFT = 1;
-    private static final int STATE_RIGHT = 2;
-    private static final int STATE_NORMAL = 0;
-    private static final int STATE_HAS_MOREKEYS = 1;
-
     public void placeKeyPreviewAndShow(final Key key, final KeyboardIconsSet iconsSet,
             final KeyDrawParams drawParams, final int keyboardViewWidth, final int[] keyboardOrigin,
             final ViewGroup placerView, final boolean withAnimation) {
-        final TextView previewTextView = getKeyPreviewTextView(key, placerView);
+        final KeyPreviewView keyPreviewView = getKeyPreviewView(key, placerView);
         placeKeyPreview(
-                key, previewTextView, iconsSet, drawParams, keyboardViewWidth, keyboardOrigin);
-        showKeyPreview(key, previewTextView, withAnimation);
+                key, keyPreviewView, iconsSet, drawParams, keyboardViewWidth, keyboardOrigin);
+        showKeyPreview(key, keyPreviewView, withAnimation);
     }
 
-    private void placeKeyPreview(final Key key, final TextView previewTextView,
+    private void placeKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
             final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams,
             final int keyboardViewWidth, final int[] originCoords) {
-        previewTextView.setTextColor(drawParams.mPreviewTextColor);
-        final Drawable background = previewTextView.getBackground();
-        final String label = key.getPreviewLabel();
-        // What we show as preview should match what we show on a key top in onDraw().
-        if (label != null) {
-            // TODO Should take care of temporaryShiftLabel here.
-            previewTextView.setCompoundDrawables(null, null, null, null);
-            previewTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
-                    key.selectPreviewTextSize(drawParams));
-            previewTextView.setTypeface(key.selectPreviewTypeface(drawParams));
-            previewTextView.setText(label);
-        } else {
-            previewTextView.setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet));
-            previewTextView.setText(null);
-        }
-
-        previewTextView.measure(
+        keyPreviewView.setPreviewVisual(key, iconsSet, drawParams);
+        keyPreviewView.measure(
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        mParams.setGeometry(previewTextView);
-        final int previewWidth = previewTextView.getMeasuredWidth();
+        mParams.setGeometry(keyPreviewView);
+        final int previewWidth = keyPreviewView.getMeasuredWidth();
         final int previewHeight = mParams.mPreviewHeight;
         final int keyDrawWidth = key.getDrawWidth();
         // The key preview is horizontally aligned with the center of the visible part of the
         // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
         // the left/right background is used if such background is specified.
-        final int statePosition;
+        final int keyPreviewPosition;
         int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2
                 + CoordinateUtils.x(originCoords);
         if (previewX < 0) {
             previewX = 0;
-            statePosition = STATE_LEFT;
+            keyPreviewPosition = KeyPreviewView.POSITION_LEFT;
         } else if (previewX > keyboardViewWidth - previewWidth) {
             previewX = keyboardViewWidth - previewWidth;
-            statePosition = STATE_RIGHT;
+            keyPreviewPosition = KeyPreviewView.POSITION_RIGHT;
         } else {
-            statePosition = STATE_MIDDLE;
+            keyPreviewPosition = KeyPreviewView.POSITION_MIDDLE;
         }
+        final boolean hasMoreKeys = (key.getMoreKeys() != null);
+        keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
         // The key preview is placed vertically above the top edge of the parent key with an
         // arbitrary offset.
         final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset
                 + CoordinateUtils.y(originCoords);
 
-        if (background != null) {
-            final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
-            background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
-        }
         ViewLayoutUtils.placeViewAt(
-                previewTextView, previewX, previewY, previewWidth, previewHeight);
-        previewTextView.setPivotX(previewWidth / 2.0f);
-        previewTextView.setPivotY(previewHeight);
+                keyPreviewView, previewX, previewY, previewWidth, previewHeight);
+        keyPreviewView.setPivotX(previewWidth / 2.0f);
+        keyPreviewView.setPivotY(previewHeight);
     }
 
-    private void showKeyPreview(final Key key, final TextView previewTextView,
+    private void showKeyPreview(final Key key, final KeyPreviewView keyPreviewView,
             final boolean withAnimation) {
         if (!withAnimation) {
-            previewTextView.setVisibility(View.VISIBLE);
-            mShowingKeyPreviewTextViews.put(key, previewTextView);
+            keyPreviewView.setVisibility(View.VISIBLE);
+            mShowingKeyPreviewViews.put(key, keyPreviewView);
             return;
         }
 
         // Show preview with animation.
-        final Animator showUpAnimation = createShowUpAniation(key, previewTextView);
-        final Animator dismissAnimation = createDismissAnimation(key, previewTextView);
+        final Animator showUpAnimation = createShowUpAniation(key, keyPreviewView);
+        final Animator dismissAnimation = createDismissAnimation(key, keyPreviewView);
         final KeyPreviewAnimations animation = new KeyPreviewAnimations(
                 showUpAnimation, dismissAnimation);
-        previewTextView.setTag(animation);
+        keyPreviewView.setTag(animation);
         animation.startShowUp();
     }
 
@@ -221,13 +175,13 @@
     private static final DecelerateInterpolator DECELERATE_INTERPOLATOR =
             new DecelerateInterpolator();
 
-    private Animator createShowUpAniation(final Key key, final TextView previewTextView) {
+    private Animator createShowUpAniation(final Key key, final KeyPreviewView keyPreviewView) {
         // TODO: Optimization for no scale animation and no duration.
         final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat(
-                previewTextView, View.SCALE_X, mParams.getShowUpStartScale(),
+                keyPreviewView, View.SCALE_X, mParams.getShowUpStartScale(),
                 KEY_PREVIEW_SHOW_UP_END_SCALE);
         final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat(
-                previewTextView, View.SCALE_Y, mParams.getShowUpStartScale(),
+                keyPreviewView, View.SCALE_Y, mParams.getShowUpStartScale(),
                 KEY_PREVIEW_SHOW_UP_END_SCALE);
         final AnimatorSet showUpAnimation = new AnimatorSet();
         showUpAnimation.play(scaleXAnimation).with(scaleYAnimation);
@@ -236,18 +190,18 @@
         showUpAnimation.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(final Animator animation) {
-                showKeyPreview(key, previewTextView, false /* withAnimation */);
+                showKeyPreview(key, keyPreviewView, false /* withAnimation */);
             }
         });
         return showUpAnimation;
     }
 
-    private Animator createDismissAnimation(final Key key, final TextView previewTextView) {
+    private Animator createDismissAnimation(final Key key, final KeyPreviewView keyPreviewView) {
         // TODO: Optimization for no scale animation and no duration.
         final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat(
-                previewTextView, View.SCALE_X, mParams.getDismissEndScale());
+                keyPreviewView, View.SCALE_X, mParams.getDismissEndScale());
         final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat(
-                previewTextView, View.SCALE_Y, mParams.getDismissEndScale());
+                keyPreviewView, View.SCALE_Y, mParams.getDismissEndScale());
         final AnimatorSet dismissAnimation = new AnimatorSet();
         dismissAnimation.play(scaleXAnimation).with(scaleYAnimation);
         final int dismissDuration = Math.min(
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
index 37e5c88..68c9831 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
@@ -23,9 +23,9 @@
 
 public final class KeyPreviewDrawParams {
     // XML attributes of {@link MainKeyboardView}.
-    public final int mLayoutId;
     public final int mPreviewOffset;
     public final int mPreviewHeight;
+    public final int mPreviewBackgroundResId;
     private int mShowUpDuration;
     private int mDismissDuration;
     private float mShowUpStartScale;
@@ -63,13 +63,10 @@
                 R.styleable.MainKeyboardView_keyPreviewOffset, 0);
         mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
                 R.styleable.MainKeyboardView_keyPreviewHeight, 0);
+        mPreviewBackgroundResId = mainKeyboardViewAttr.getResourceId(
+                R.styleable.MainKeyboardView_keyPreviewBackground, 0);
         mLingerTimeout = mainKeyboardViewAttr.getInt(
                 R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0);
-        mLayoutId = mainKeyboardViewAttr.getResourceId(
-                R.styleable.MainKeyboardView_keyPreviewLayout, 0);
-        if (mLayoutId == 0) {
-            mShowPopup = false;
-        }
     }
 
     public void setVisibleOffset(final int previewVisibleOffset) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java
new file mode 100644
index 0000000..360faf8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 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.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.R;
+
+/**
+ * The pop up key preview view.
+ */
+public class KeyPreviewView extends TextView {
+    public static final int POSITION_MIDDLE = 0;
+    public static final int POSITION_LEFT = 1;
+    public static final int POSITION_RIGHT = 2;
+
+    public KeyPreviewView(final Context context, final AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyPreviewView(final Context context, final AttributeSet attrs, final int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setGravity(Gravity.CENTER);
+    }
+
+    public void setPreviewVisual(final Key key, final KeyboardIconsSet iconsSet,
+            final KeyDrawParams drawParams) {
+        // What we show as preview should match what we show on a key top in onDraw().
+        final int iconId = key.getIconId();
+        if (iconId != KeyboardIconsSet.ICON_UNDEFINED) {
+            setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet));
+            setText(null);
+            return;
+        }
+
+        setCompoundDrawables(null, null, null, null);
+        setTextColor(drawParams.mPreviewTextColor);
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectPreviewTextSize(drawParams));
+        setTypeface(key.selectPreviewTypeface(drawParams));
+        // TODO Should take care of temporaryShiftLabel here.
+        setText(key.getPreviewLabel());
+    }
+
+    // Background state set
+    private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
+        { // POSITION_MIDDLE
+            {},
+            { R.attr.state_has_morekeys }
+        },
+        { // POSITION_LEFT
+            { R.attr.state_left_edge },
+            { R.attr.state_left_edge, R.attr.state_has_morekeys }
+        },
+        { // POSITION_RIGHT
+            { R.attr.state_right_edge },
+            { R.attr.state_right_edge, R.attr.state_has_morekeys }
+        }
+    };
+    private static final int STATE_NORMAL = 0;
+    private static final int STATE_HAS_MOREKEYS = 1;
+
+    public void setPreviewBackground(final boolean hasMoreKeys, final int position) {
+        final Drawable background = getBackground();
+        if (background == null) {
+            return;
+        }
+        final int hasMoreKeysState = hasMoreKeys ? STATE_HAS_MOREKEYS : STATE_NORMAL;
+        background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[position][hasMoreKeysState]);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 543f74f..42105e2 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -28,7 +28,6 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.makedict.WordProperty;
-import com.android.inputmethod.latin.settings.NativeSuggestOptions;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
@@ -49,10 +48,6 @@
 public final class BinaryDictionary extends Dictionary {
     private static final String TAG = BinaryDictionary.class.getSimpleName();
 
-    // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
-    private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
-    // Must be equal to MAX_RESULTS in native/jni/src/defines.h
-    private static final int MAX_RESULTS = 18;
     // The cutoff returned by native for auto-commit confidence.
     // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h
     private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000;
@@ -88,21 +83,10 @@
     private final Locale mLocale;
     private final long mDictSize;
     private final String mDictFilePath;
+    private final boolean mUseFullEditDistance;
     private final boolean mIsUpdatable;
     private boolean mHasUpdated;
 
-    private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
-    private final int[] mOutputSuggestionCount = new int[1];
-    private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS];
-    private final int[] mSpaceIndices = new int[MAX_RESULTS];
-    private final int[] mOutputScores = new int[MAX_RESULTS];
-    private final int[] mOutputTypes = new int[MAX_RESULTS];
-    // Only one result is ever used
-    private final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
-    private final float[] mInputOutputLanguageWeight = new float[1];
-
-    private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
-
     private final SparseArray<DicTraverseSession> mDicTraverseSessions = new SparseArray<>();
 
     // TODO: There should be a way to remove used DicTraverseSession objects from
@@ -136,7 +120,7 @@
         mDictFilePath = filename;
         mIsUpdatable = isUpdatable;
         mHasUpdated = false;
-        mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
+        mUseFullEditDistance = useFullEditDistance;
         loadDictionary(filename, offset, length, isUpdatable);
     }
 
@@ -148,7 +132,6 @@
      * @param formatVersion the format version of the dictionary
      * @param attributeMap the attributes of the dictionary
      */
-    @UsedForTesting
     public BinaryDictionary(final String filename, final boolean useFullEditDistance,
             final Locale locale, final String dictType, final long formatVersion,
             final Map<String, String> attributeMap) {
@@ -159,7 +142,7 @@
         // On memory dictionary is always updatable.
         mIsUpdatable = true;
         mHasUpdated = false;
-        mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
+        mUseFullEditDistance = useFullEditDistance;
         final String[] keyArray = new String[attributeMap.size()];
         final String[] valueArray = new String[attributeMap.size()];
         int index = 0;
@@ -274,8 +257,8 @@
         if (!isValidDictionary()) {
             return null;
         }
-
-        Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
+        final DicTraverseSession session = getTraverseSession(sessionId);
+        Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
         // TODO: toLowerCase in the native code
         final int[] prevWordCodePointArray = (null == prevWordsInfo.mPrevWord)
                 ? null : StringUtils.toCodePointArray(prevWordsInfo.mPrevWord);
@@ -284,47 +267,50 @@
         final int inputSize;
         if (!isGesture) {
             inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
-                    mInputCodePoints);
+                    session.mInputCodePoints);
             if (inputSize < 0) {
                 return null;
             }
         } else {
             inputSize = inputPointers.getPointerSize();
         }
-
-        mNativeSuggestOptions.setIsGesture(isGesture);
-        mNativeSuggestOptions.setBlockOffensiveWords(blockOffensiveWords);
-        mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
+        session.mNativeSuggestOptions.setUseFullEditDistance(mUseFullEditDistance);
+        session.mNativeSuggestOptions.setIsGesture(isGesture);
+        session.mNativeSuggestOptions.setBlockOffensiveWords(blockOffensiveWords);
+        session.mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
         if (inOutLanguageWeight != null) {
-            mInputOutputLanguageWeight[0] = inOutLanguageWeight[0];
+            session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0];
         } else {
-            mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
+            session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
         }
         // proximityInfo and/or prevWordForBigrams may not be null.
         getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
                 getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
                 inputPointers.getYCoordinates(), inputPointers.getTimes(),
-                inputPointers.getPointerIds(), mInputCodePoints, inputSize,
-                mNativeSuggestOptions.getOptions(), prevWordCodePointArray,
-                prevWordsInfo.mIsBeginningOfSentence, mOutputSuggestionCount,
-                mOutputCodePoints, mOutputScores, mSpaceIndices, mOutputTypes,
-                mOutputAutoCommitFirstWordConfidence, mInputOutputLanguageWeight);
+                inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
+                session.mNativeSuggestOptions.getOptions(), prevWordCodePointArray,
+                prevWordsInfo.mIsBeginningOfSentence, session.mOutputSuggestionCount,
+                session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices,
+                session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence,
+                session.mInputOutputLanguageWeight);
         if (inOutLanguageWeight != null) {
-            inOutLanguageWeight[0] = mInputOutputLanguageWeight[0];
+            inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0];
         }
-        final int count = mOutputSuggestionCount[0];
+        final int count = session.mOutputSuggestionCount[0];
         final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
         for (int j = 0; j < count; ++j) {
-            final int start = j * MAX_WORD_LENGTH;
+            final int start = j * Constants.DICTIONARY_MAX_WORD_LENGTH;
             int len = 0;
-            while (len < MAX_WORD_LENGTH && mOutputCodePoints[start + len] != 0) {
+            while (len < Constants.DICTIONARY_MAX_WORD_LENGTH
+                    && session.mOutputCodePoints[start + len] != 0) {
                 ++len;
             }
             if (len > 0) {
-                suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
-                        mOutputScores[j], mOutputTypes[j], this /* sourceDict */,
-                        mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
-                        mOutputAutoCommitFirstWordConfidence[0]));
+                suggestions.add(new SuggestedWordInfo(
+                        new String(session.mOutputCodePoints, start, len),
+                        session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */,
+                        session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
+                        session.mOutputAutoCommitFirstWordConfidence[0]));
             }
         }
         return suggestions;
@@ -377,7 +363,7 @@
             return null;
         }
         final int[] codePoints = StringUtils.toCodePointArray(word);
-        final int[] outCodePoints = new int[MAX_WORD_LENGTH];
+        final int[] outCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
         final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
         final int[] outProbabilityInfo =
                 new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
@@ -412,7 +398,7 @@
      * If token is 0, this method newly starts iterating the dictionary.
      */
     public GetNextWordPropertyResult getNextWordProperty(final int token) {
-        final int[] codePoints = new int[MAX_WORD_LENGTH];
+        final int[] codePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
         final int nextToken = getNextWordNative(mNativeDict, token, codePoints);
         final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints);
         return new GetNextWordPropertyResult(getWordProperty(word), nextToken);
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index 8d295ad..8bbf426 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.latin.settings.NativeSuggestOptions;
 import com.android.inputmethod.latin.utils.JniUtils;
 
 import java.util.Locale;
@@ -24,6 +25,20 @@
     static {
         JniUtils.loadNativeLibrary();
     }
+    // Must be equal to MAX_RESULTS in native/jni/src/defines.h
+    private static final int MAX_RESULTS = 18;
+    public final int[] mInputCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
+    public final int[] mOutputSuggestionCount = new int[1];
+    public final int[] mOutputCodePoints =
+            new int[Constants.DICTIONARY_MAX_WORD_LENGTH * MAX_RESULTS];
+    public final int[] mSpaceIndices = new int[MAX_RESULTS];
+    public final int[] mOutputScores = new int[MAX_RESULTS];
+    public final int[] mOutputTypes = new int[MAX_RESULTS];
+    // Only one result is ever used
+    public final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
+    public final float[] mInputOutputLanguageWeight = new float[1];
+
+    public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
 
     private static native long setDicTraverseSessionNative(String locale, long dictSize);
     private static native void initDicTraverseSessionNative(long nativeDicTraverseSession,
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index 47b5c33..72f8f87 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -92,3 +92,6 @@
 
 #################### Unit test on host environment
 include $(LOCAL_PATH)/HostUnitTests.mk
+
+#################### Unit test on target environment
+include $(LOCAL_PATH)/TargetUnitTests.mk
diff --git a/native/jni/TargetUnitTests.mk b/native/jni/TargetUnitTests.mk
new file mode 100644
index 0000000..12aae44
--- /dev/null
+++ b/native/jni/TargetUnitTests.mk
@@ -0,0 +1,55 @@
+# Copyright (C) 2014 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.
+
+LOCAL_PATH := $(call my-dir)
+
+######################################
+include $(CLEAR_VARS)
+
+include $(LOCAL_PATH)/NativeFileList.mk
+
+#################### Target library for unit test
+LATIN_IME_SRC_DIR := src
+LOCAL_CFLAGS += -std=c++11 -Wno-unused-parameter -Wno-unused-function
+LOCAL_CLANG := true
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_SRC_DIR)
+LOCAL_MODULE := liblatinime_target_static_for_unittests
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(addprefix $(LATIN_IME_SRC_DIR)/, $(LATIN_IME_CORE_SRC_FILES))
+# Here intentionally use libc++_shared rather than libc++_static because
+# $(BUILD_NATIVE_TEST) has not yet supported libc++_static.
+LOCAL_SDK_VERSION := 14
+LOCAL_NDK_STL_VARIANT := c++_shared
+include $(BUILD_STATIC_LIBRARY)
+
+#################### Target native tests
+include $(CLEAR_VARS)
+LATIN_IME_TEST_SRC_DIR := tests
+LOCAL_CFLAGS += -std=c++11 -Wno-unused-parameter -Wno-unused-function
+LOCAL_CLANG := true
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_SRC_DIR)
+LOCAL_MODULE := liblatinime_target_unittests
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES :=  \
+    $(addprefix $(LATIN_IME_TEST_SRC_DIR)/, $(LATIN_IME_CORE_TEST_FILES))
+LOCAL_STATIC_LIBRARIES += liblatinime_target_static_for_unittests
+# Here intentionally include external/libcxx/libcxx.mk rather because
+# $(BUILD_NATIVE_TEST) fails when LOCAL_NDK_STL_VARIANT is specified.
+include external/libcxx/libcxx.mk
+include $(BUILD_NATIVE_TEST)
+
+#################### Clean up the tmp vars
+LATIN_IME_SRC_DIR :=
+LATIN_IME_TEST_SRC_DIR :=
+include $(LOCAL_PATH)/CleanupNativeFileList.mk
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 6e2219d..c2cd2ad 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -304,17 +304,18 @@
         jlong dict, jint token, jintArray outCodePoints) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return 0;
-    const jsize outCodePointsLength = env->GetArrayLength(outCodePoints);
-    if (outCodePointsLength != MAX_WORD_LENGTH) {
-        AKLOGE("Invalid outCodePointsLength: %d", outCodePointsLength);
+    const jsize codePointBufSize = env->GetArrayLength(outCodePoints);
+    if (codePointBufSize != MAX_WORD_LENGTH) {
+        AKLOGE("Invalid outCodePointsLength: %d", codePointBufSize);
         ASSERT(false);
         return 0;
     }
-    int wordCodePoints[outCodePointsLength];
-    memset(wordCodePoints, 0, sizeof(wordCodePoints));
-    const int nextToken = dictionary->getNextWordAndNextToken(token, wordCodePoints);
+    int wordCodePoints[codePointBufSize];
+    int wordCodePointCount = 0;
+    const int nextToken = dictionary->getNextWordAndNextToken(token, wordCodePoints,
+            &wordCodePointCount);
     JniDataUtils::outputCodePoints(env, outCodePoints, 0 /* start */,
-            MAX_WORD_LENGTH /* maxLength */, wordCodePoints, outCodePointsLength,
+            MAX_WORD_LENGTH /* maxLength */, wordCodePoints, wordCodePointCount,
             false /* needsNullTermination */);
     return nextToken;
 }
@@ -555,12 +556,13 @@
 
     // TODO: Migrate historical information.
     int wordCodePoints[MAX_WORD_LENGTH];
+    int wordCodePointCount = 0;
     int token = 0;
     // Add unigrams.
     do {
-        token = dictionary->getNextWordAndNextToken(token, wordCodePoints);
-        const int wordLength = CharUtils::getCodePointCount(MAX_WORD_LENGTH, wordCodePoints);
-        const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints, wordLength);
+        token = dictionary->getNextWordAndNextToken(token, wordCodePoints, &wordCodePointCount);
+        const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints,
+                wordCodePointCount);
         if (dictionaryStructureWithBufferPolicy->needsToRunGC(true /* mindsBlockByGC */)) {
             dictionaryStructureWithBufferPolicy = runGCAndGetNewStructurePolicy(
                     std::move(dictionaryStructureWithBufferPolicy), dictFilePathChars);
@@ -569,8 +571,8 @@
                 return false;
             }
         }
-        if (!dictionaryStructureWithBufferPolicy->addUnigramEntry(wordCodePoints, wordLength,
-                wordProperty.getUnigramProperty())) {
+        if (!dictionaryStructureWithBufferPolicy->addUnigramEntry(wordCodePoints,
+                wordCodePointCount, wordProperty.getUnigramProperty())) {
             LogUtils::logToJava(env, "Cannot add unigram to the new dict.");
             return false;
         }
@@ -578,9 +580,9 @@
 
     // Add bigrams.
     do {
-        token = dictionary->getNextWordAndNextToken(token, wordCodePoints);
-        const int wordLength = CharUtils::getCodePointCount(MAX_WORD_LENGTH, wordCodePoints);
-        const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints, wordLength);
+        token = dictionary->getNextWordAndNextToken(token, wordCodePoints, &wordCodePointCount);
+        const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints,
+                wordCodePointCount);
         if (dictionaryStructureWithBufferPolicy->needsToRunGC(true /* mindsBlockByGC */)) {
             dictionaryStructureWithBufferPolicy = runGCAndGetNewStructurePolicy(
                     std::move(dictionaryStructureWithBufferPolicy), dictFilePathChars);
@@ -589,8 +591,8 @@
                 return false;
             }
         }
-        const PrevWordsInfo prevWordsInfo(wordCodePoints, wordLength,
-                false /* isStartOfSentence */);
+        const PrevWordsInfo prevWordsInfo(wordCodePoints, wordCodePointCount,
+                false /* isBeginningOfSentence */);
         for (const BigramProperty &bigramProperty : *wordProperty.getBigramProperties()) {
             if (!dictionaryStructureWithBufferPolicy->addNgramEntry(&prevWordsInfo,
                     &bigramProperty)) {
diff --git a/native/jni/run-tests.sh b/native/jni/run-tests.sh
index 5b60e0d..3da4527 100755
--- a/native/jni/run-tests.sh
+++ b/native/jni/run-tests.sh
@@ -13,17 +13,56 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+function usage() {
+    echo "usage: source run-tests.sh [--host] [--target] [-h] [--help]"  1>&2
+    echo "    --host: run test on the host environment"  1>&2
+    echo "    --no-host: skip host test"  1>&2
+    echo "    --target: run test on the target environment"  1>&2
+    echo "    --no-target: skip target device test"  1>&2
+}
+
+# check script arguments
 if [[ $(type -t mmm) != function ]]; then
-echo "Usage:" 1>&2
-echo "    source $0" 1>&2
-echo "  or" 1>&2
-echo "    . $0" 1>&2
+usage
 if [[ ${BASH_SOURCE[0]} != $0 ]]; then return; else exit 1; fi
 fi
 
+show_usage=no
+enable_host_test=yes
+enable_target_device_test=no
+while [ "$1" != "" ]
+  do
+  case "$1" in
+    "-h") show_usage=yes;;
+    "--help") show_usage=yes;;
+    "--target") enable_target_device_test=yes;;
+    "--no-target") enable_target_device_test=no;;
+    "--host") enable_host_test=yes;;
+    "--no-host") enable_host_test=no;;
+  esac
+  shift
+done
+
+if [[ $show_usage == yes ]]; then
+  usage
+  if [[ ${BASH_SOURCE[0]} != $0 ]]; then return; else exit 1; fi
+fi
+
+target_test_name=liblatinime_target_unittests
+host_test_name=liblatinime_host_unittests
+
 pushd $PWD > /dev/null
 cd $(gettop)
 mmm -j16 packages/inputmethods/LatinIME/native/jni || \
-    make -j16 liblatinime_host_unittests
-${ANDROID_HOST_OUT}/bin/liblatinime_host_unittests
-popd > /dev/null
\ No newline at end of file
+    make -j16 adb $target_test_name $host_test_name
+if [[ $enable_host_test == yes ]]; then
+  $ANDROID_HOST_OUT/bin/$host_test_name
+fi
+if [[ $enable_target_device_test == yes ]]; then
+  target_test_local=$ANDROID_PRODUCT_OUT/data/nativetest/$target_test_name/$target_test_name
+  target_test_device=/data/nativetest/$target_test_name/$target_test_name
+  adb push $target_test_local $target_test_device
+  adb shell $target_test_device
+  adb shell rm -rf $target_test_device
+fi
+popd > /dev/null
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 0bcde22..2282602 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -145,10 +145,11 @@
             codePoints, codePointCount);
 }
 
-int Dictionary::getNextWordAndNextToken(const int token, int *const outCodePoints) {
+int Dictionary::getNextWordAndNextToken(const int token, int *const outCodePoints,
+        int *const outCodePointCount) {
     TimeKeeper::setCurrentTime();
     return mDictionaryStructureWithBufferPolicy->getNextWordAndNextToken(
-            token, outCodePoints);
+            token, outCodePoints, outCodePointCount);
 }
 
 void Dictionary::logDictionaryInfo(JNIEnv *const env) const {
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 542ba72..247ee24 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -103,7 +103,8 @@
     // Method to iterate all words in the dictionary.
     // The returned token has to be used to get the next word. If token is 0, this method newly
     // starts iterating the dictionary.
-    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+    int getNextWordAndNextToken(const int token, int *const outCodePoints,
+            int *const outCodePointCount);
 
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const {
         return mDictionaryStructureWithBufferPolicy.get();
diff --git a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
index e2771f9..b726011 100644
--- a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
@@ -104,7 +104,8 @@
     // Method to iterate all words in the dictionary.
     // The returned token has to be used to get the next word. If token is 0, this method newly
     // starts iterating the dictionary.
-    virtual int getNextWordAndNextToken(const int token, int *const outCodePoints) = 0;
+    virtual int getNextWordAndNextToken(const int token, int *const outCodePoints,
+            int *const outCodePointCount) = 0;
 
     virtual bool isCorrupted() const = 0;
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
index 4ac0f40..9780ae0 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
@@ -478,10 +478,9 @@
     return WordProperty(&codePointVector, &unigramProperty, &bigrams);
 }
 
-int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints) {
-    // TODO: Return code point count like other methods.
-    // Null termination.
-    outCodePoints[0] = 0;
+int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
+        int *const outCodePointCount) {
+    *outCodePointCount = 0;
     if (token == 0) {
         mTerminalPtNodePositionsForIteratingWords.clear();
         DynamicPtReadingHelper::TraversePolicyToGetAllTerminalPtNodePositions traversePolicy(
@@ -498,13 +497,8 @@
     }
     const int terminalPtNodePos = mTerminalPtNodePositionsForIteratingWords[token];
     int unigramProbability = NOT_A_PROBABILITY;
-    const int codePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(
+    *outCodePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(
             terminalPtNodePos, MAX_WORD_LENGTH, outCodePoints, &unigramProbability);
-    if (codePointCount < MAX_WORD_LENGTH) {
-        // Null termination. outCodePoints have to be null terminated or contain MAX_WORD_LENGTH
-        // code points.
-        outCodePoints[codePointCount] = 0;
-    }
     const int nextToken = token + 1;
     if (nextToken >= terminalPtNodePositionsVectorSize) {
         // All words have been iterated.
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.h
index 2e948ac..16b1bd2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.h
@@ -134,7 +134,8 @@
     const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const;
 
-    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+    int getNextWordAndNextToken(const int token, int *const outCodePoints,
+            int *const outCodePointCount);
 
     bool isCorrupted() const {
         return mIsCorrupted;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_gc_event_listeners.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_gc_event_listeners.cpp
index 1f00fc6..db1a802 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_gc_event_listeners.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_gc_event_listeners.cpp
@@ -65,7 +65,7 @@
 
 bool DynamicPtGcEventListeners::TraversePolicyToUpdateBigramProbability
         ::onVisitingPtNode(const PtNodeParams *const ptNodeParams) {
-    if (!ptNodeParams->isDeleted() && ptNodeParams->hasBigrams()) {
+    if (!ptNodeParams->isDeleted()) {
         int bigramEntryCount = 0;
         if (!mPtNodeWriter->updateAllBigramEntriesAndDeleteUselessEntries(ptNodeParams,
                 &bigramEntryCount)) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
index e77d39b..f31c914 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
@@ -270,8 +270,8 @@
         const bool isNotAWord, const bool isBlacklisted, const bool isTerminal, const int parentPos,
         const int codePointCount, const int *const codePoints, const int probability) const {
     const PatriciaTrieReadingUtils::NodeFlags flags = PatriciaTrieReadingUtils::createAndGetFlags(
-            isBlacklisted, isNotAWord, isTerminal, originalPtNodeParams->hasShortcutTargets(),
-            originalPtNodeParams->hasBigrams(), codePointCount > 1 /* hasMultipleChars */,
+            isBlacklisted, isNotAWord, isTerminal, false /* hasShortcutTargets */,
+            false /* hasBigrams */, codePointCount > 1 /* hasMultipleChars */,
             CHILDREN_POSITION_FIELD_SIZE);
     return PtNodeParams(originalPtNodeParams, flags, parentPos, codePointCount, codePoints,
             probability);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
index 7e1f3b2..5c62b9c 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
@@ -391,7 +391,9 @@
     return WordProperty(&codePointVector, &unigramProperty, &bigrams);
 }
 
-int PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints) {
+int PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
+        int *const outCodePointCount) {
+    *outCodePointCount = 0;
     if (token == 0) {
         // Start iterating the dictionary.
         mTerminalPtNodePositionsForIteratingWords.clear();
@@ -409,8 +411,8 @@
     }
     const int terminalPtNodePos = mTerminalPtNodePositionsForIteratingWords[token];
     int unigramProbability = NOT_A_PROBABILITY;
-    getCodePointsAndProbabilityAndReturnCodePointCount(terminalPtNodePos, MAX_WORD_LENGTH,
-            outCodePoints, &unigramProbability);
+    *outCodePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(terminalPtNodePos,
+            MAX_WORD_LENGTH, outCodePoints, &unigramProbability);
     const int nextToken = token + 1;
     if (nextToken >= terminalPtNodePositionsVectorSize) {
         // All words have been iterated.
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
index dce9436..ec84074 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
@@ -137,7 +137,8 @@
     const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const;
 
-    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+    int getNextWordAndNextToken(const int token, int *const outCodePoints,
+            int *const outCodePointCount);
 
     bool isCorrupted() const {
         return mIsCorrupted;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
index f89d3d7..3d8da91 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
@@ -231,14 +231,6 @@
                 sourcePtNodeParams->getTerminalId(), targetPtNodeParam->getTerminalId());
         return false;
     }
-    if (!sourcePtNodeParams->hasBigrams()) {
-        // Update has bigrams flag.
-        return updatePtNodeFlags(sourcePtNodeParams->getHeadPos(),
-                sourcePtNodeParams->isBlacklisted(), sourcePtNodeParams->isNotAWord(),
-                sourcePtNodeParams->isTerminal(), sourcePtNodeParams->hasShortcutTargets(),
-                true /* hasBigrams */,
-                sourcePtNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
-    }
     return true;
 }
 
@@ -303,28 +295,9 @@
         AKLOGE("Cannot add new shortuct entry. terminalId: %d", ptNodeParams->getTerminalId());
         return false;
     }
-    if (!ptNodeParams->hasShortcutTargets()) {
-        // Update has shortcut targets flag.
-        return updatePtNodeFlags(ptNodeParams->getHeadPos(),
-                ptNodeParams->isBlacklisted(), ptNodeParams->isNotAWord(),
-                ptNodeParams->isTerminal(), true /* hasShortcutTargets */,
-                ptNodeParams->hasBigrams(),
-                ptNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
-    }
     return true;
 }
 
-bool Ver4PatriciaTrieNodeWriter::updatePtNodeHasBigramsAndShortcutTargetsFlags(
-        const PtNodeParams *const ptNodeParams) {
-    const bool hasBigrams = mBuffers->getBigramDictContent()->getBigramListHeadPos(
-            ptNodeParams->getTerminalId()) != NOT_A_DICT_POS;
-    const bool hasShortcutTargets = mBuffers->getShortcutDictContent()->getShortcutListHeadPos(
-            ptNodeParams->getTerminalId()) != NOT_A_DICT_POS;
-    return updatePtNodeFlags(ptNodeParams->getHeadPos(), ptNodeParams->isBlacklisted(),
-            ptNodeParams->isNotAWord(), ptNodeParams->isTerminal(), hasShortcutTargets,
-            hasBigrams, ptNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
-}
-
 bool Ver4PatriciaTrieNodeWriter::writePtNodeAndGetTerminalIdAndAdvancePosition(
         const PtNodeParams *const ptNodeParams, int *const outTerminalId,
         int *const ptNodeWritingPos) {
@@ -377,8 +350,7 @@
         return false;
     }
     return updatePtNodeFlags(nodePos, ptNodeParams->isBlacklisted(), ptNodeParams->isNotAWord(),
-            isTerminal, ptNodeParams->hasShortcutTargets(), ptNodeParams->hasBigrams(),
-            ptNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
+            isTerminal, ptNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
 }
 
 const ProbabilityEntry Ver4PatriciaTrieNodeWriter::createUpdatedEntryFrom(
@@ -402,11 +374,11 @@
 
 bool Ver4PatriciaTrieNodeWriter::updatePtNodeFlags(const int ptNodePos,
         const bool isBlacklisted, const bool isNotAWord, const bool isTerminal,
-        const bool hasShortcutTargets, const bool hasBigrams, const bool hasMultipleChars) {
+        const bool hasMultipleChars) {
     // Create node flags and write them.
     PatriciaTrieReadingUtils::NodeFlags nodeFlags =
             PatriciaTrieReadingUtils::createAndGetFlags(isBlacklisted, isNotAWord, isTerminal,
-                    hasShortcutTargets, hasBigrams, hasMultipleChars,
+                    false /* hasShortcutTargets */, false /* hasBigrams */, hasMultipleChars,
                     CHILDREN_POSITION_FIELD_SIZE);
     if (!DynamicPtWritingUtils::writeFlags(mTrieBuffer, nodeFlags, ptNodePos)) {
         AKLOGE("Cannot write PtNode flags. flags: %x, pos: %d", nodeFlags, ptNodePos);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
index e90bc44..162dc9b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
@@ -93,8 +93,6 @@
             const int *const targetCodePoints, const int targetCodePointCount,
             const int shortcutProbability);
 
-    bool updatePtNodeHasBigramsAndShortcutTargetsFlags(const PtNodeParams *const ptNodeParams);
-
  private:
     DISALLOW_COPY_AND_ASSIGN(Ver4PatriciaTrieNodeWriter);
 
@@ -110,8 +108,7 @@
             const UnigramProperty *const unigramProperty) const;
 
     bool updatePtNodeFlags(const int ptNodePos, const bool isBlacklisted, const bool isNotAWord,
-            const bool isTerminal, const bool hasShortcutTargets, const bool hasBigrams,
-            const bool hasMultipleChars);
+            const bool isTerminal, const bool hasMultipleChars);
 
     static const int CHILDREN_POSITION_FIELD_SIZE;
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index f7f2a32..46107d9 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -489,10 +489,9 @@
     return WordProperty(&codePointVector, &unigramProperty, &bigrams);
 }
 
-int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints) {
-    // TODO: Return code point count like other methods.
-    // Null termination.
-    outCodePoints[0] = 0;
+int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
+        int *const outCodePointCount) {
+    *outCodePointCount = 0;
     if (token == 0) {
         mTerminalPtNodePositionsForIteratingWords.clear();
         DynamicPtReadingHelper::TraversePolicyToGetAllTerminalPtNodePositions traversePolicy(
@@ -509,13 +508,8 @@
     }
     const int terminalPtNodePos = mTerminalPtNodePositionsForIteratingWords[token];
     int unigramProbability = NOT_A_PROBABILITY;
-    const int codePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(
+    *outCodePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(
             terminalPtNodePos, MAX_WORD_LENGTH, outCodePoints, &unigramProbability);
-    if (codePointCount < MAX_WORD_LENGTH) {
-        // Null termination. outCodePoints have to be null terminated or contain MAX_WORD_LENGTH
-        // code points.
-        outCodePoints[codePointCount] = 0;
-    }
     const int nextToken = token + 1;
     if (nextToken >= terminalPtNodePositionsVectorSize) {
         // All words have been iterated.
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
index 0a20965..5d66a2c 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
@@ -113,7 +113,8 @@
     const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const;
 
-    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+    int getNextWordAndNextToken(const int token, int *const outCodePoints,
+            int *const outCodePointCount);
 
     bool isCorrupted() const {
         return mIsCorrupted;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
index 3eedcf2..40fdfa0 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
@@ -286,8 +286,9 @@
     }
     if (!mPtNodeWriter->updateTerminalId(ptNodeParams, it->second)) {
         AKLOGE("Cannot update terminal id. %d -> %d", it->first, it->second);
+        return false;
     }
-    return mPtNodeWriter->updatePtNodeHasBigramsAndShortcutTargetsFlags(ptNodeParams);
+    return true;
 }
 
 } // namespace latinime
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
index 2cbc041..3ef03f4 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
@@ -26,11 +26,13 @@
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.util.ArrayList;
 
@@ -51,14 +53,17 @@
     public final static String ENCRYPTION = "encrypted";
 
     private final static int MAX_DECODE_DEPTH = 8;
+    private final static int COPY_BUFFER_SIZE = 8192;
 
     public static class DecoderChainSpec {
         ArrayList<String> mDecoderSpec = new ArrayList<>();
         File mFile;
+
         public DecoderChainSpec addStep(final String stepDescription) {
             mDecoderSpec.add(stepDescription);
             return this;
         }
+
         public String describeChain() {
             final StringBuilder s = new StringBuilder("raw");
             for (final String step : mDecoderSpec) {
@@ -70,13 +75,10 @@
     }
 
     public static void copy(final InputStream input, final OutputStream output) throws IOException {
-        final byte[] buffer = new byte[1000];
-        final BufferedInputStream in = new BufferedInputStream(input);
-        final BufferedOutputStream out = new BufferedOutputStream(output);
-        for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer))
+        final byte[] buffer = new byte[COPY_BUFFER_SIZE];
+        for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) {
             output.write(buffer, 0, readBytes);
-        in.close();
-        out.close();
+        }
     }
 
     /**
@@ -131,11 +133,15 @@
         try {
             final File dst = File.createTempFile(PREFIX, SUFFIX);
             dst.deleteOnExit();
-            final FileOutputStream dstStream = new FileOutputStream(dst);
-            copy(Compress.getUncompressedStream(new BufferedInputStream(new FileInputStream(src))),
-                    new BufferedOutputStream(dstStream)); // #copy() closes the streams
-            return dst;
-        } catch (IOException e) {
+            try (
+                final InputStream input = Compress.getUncompressedStream(
+                        new BufferedInputStream(new FileInputStream(src)));
+                final OutputStream output = new BufferedOutputStream(new FileOutputStream(dst))
+            ) {
+                copy(input, output);
+                return dst;
+            }
+        } catch (final IOException e) {
             // Could not uncompress the file: presumably the file is simply not a compressed file
             return null;
         }
@@ -150,20 +156,20 @@
         try {
             final File dst = File.createTempFile(PREFIX, SUFFIX);
             dst.deleteOnExit();
-            final FileOutputStream dstStream = new FileOutputStream(dst);
-            copy(Crypt.getDecryptedStream(new BufferedInputStream(new FileInputStream(src))),
-                    dstStream); // #copy() closes the streams
-            return dst;
-        } catch (IOException e) {
+            try (
+                final InputStream input = Crypt.getDecryptedStream(
+                        new BufferedInputStream(new FileInputStream(src)));
+                final OutputStream output = new BufferedOutputStream(new FileOutputStream(dst))
+            ) {
+                copy(input, output);
+                return dst;
+            }
+        } catch (final IOException e) {
             // Could not decrypt the file: presumably the file is simply not a crypted file
             return null;
         }
     }
 
-    static void crash(final String filename, final Exception e) {
-        throw new RuntimeException("Can't read file " + filename, e);
-    }
-
     static FusionDictionary getDictionary(final String filename, final boolean report) {
         final File file = new File(filename);
         if (report) {
@@ -172,45 +178,40 @@
         }
         try {
             if (XmlDictInputOutput.isXmlUnigramDictionary(filename)) {
-                if (report) System.out.println("Format : XML unigram list");
+                if (report) {
+                    System.out.println("Format : XML unigram list");
+                }
                 return XmlDictInputOutput.readDictionaryXml(
                         new BufferedInputStream(new FileInputStream(file)),
                         null /* shortcuts */, null /* bigrams */);
-            } else {
-                final DecoderChainSpec decodedSpec = getRawDictionaryOrNull(file);
-                if (null == decodedSpec) {
-                    crash(filename, new RuntimeException(
-                            filename + " does not seem to be a dictionary file"));
-                } else if (CombinedInputOutput.isCombinedDictionary(
-                        decodedSpec.mFile.getAbsolutePath())){
-                    if (report) {
-                        System.out.println("Format : Combined format");
-                        System.out.println("Packaging : " + decodedSpec.describeChain());
-                        System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
-                    }
-                    return CombinedInputOutput.readDictionaryCombined(
-                            new BufferedInputStream(new FileInputStream(decodedSpec.mFile)));
-                } else {
-                    final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(
-                            decodedSpec.mFile, 0, decodedSpec.mFile.length(),
-                            DictDecoder.USE_BYTEARRAY);
-                    if (report) {
-                        System.out.println("Format : Binary dictionary format");
-                        System.out.println("Packaging : " + decodedSpec.describeChain());
-                        System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
-                    }
-                    return dictDecoder.readDictionaryBinary(false /* deleteDictIfBroken */);
+            }
+            final DecoderChainSpec decodedSpec = getRawDictionaryOrNull(file);
+            if (null == decodedSpec) {
+                throw new RuntimeException("Does not seem to be a dictionary file " + filename);
+            }
+            if (CombinedInputOutput.isCombinedDictionary(decodedSpec.mFile.getAbsolutePath())) {
+                if (report) {
+                    System.out.println("Format : Combined format");
+                    System.out.println("Packaging : " + decodedSpec.describeChain());
+                    System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
+                }
+                try (final BufferedReader reader = new BufferedReader(
+                        new InputStreamReader(new FileInputStream(decodedSpec.mFile), "UTF-8"))) {
+                    return CombinedInputOutput.readDictionaryCombined(reader);
                 }
             }
-        } catch (IOException e) {
-            crash(filename, e);
-        } catch (SAXException e) {
-            crash(filename, e);
-        } catch (ParserConfigurationException e) {
-            crash(filename, e);
-        } catch (UnsupportedFormatException e) {
-            crash(filename, e);
+            final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(
+                    decodedSpec.mFile, 0, decodedSpec.mFile.length(),
+                    DictDecoder.USE_BYTEARRAY);
+            if (report) {
+                System.out.println("Format : Binary dictionary format");
+                System.out.println("Packaging : " + decodedSpec.describeChain());
+                System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
+            }
+            return dictDecoder.readDictionaryBinary(false /* deleteDictIfBroken */);
+        } catch (final IOException | SAXException | ParserConfigurationException |
+                UnsupportedFormatException e) {
+            throw new RuntimeException("Can't read file " + filename, e);
         }
-        return null;
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
index 6a0e1b7..23cbee8 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
@@ -26,13 +26,9 @@
 import com.android.inputmethod.latin.utils.CombinedFormatUtils;
 
 import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
+import java.io.BufferedWriter;
 import java.io.FileReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Writer;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.TreeSet;
@@ -57,27 +53,15 @@
      * @return true if the file is in the combined format, false otherwise
      */
     public static boolean isCombinedDictionary(final String filename) {
-        BufferedReader reader = null;
-        try {
-            reader = new BufferedReader(new FileReader(new File(filename)));
+        try (final BufferedReader reader = new BufferedReader(new FileReader(filename))) {
             String firstLine = reader.readLine();
             while (firstLine.startsWith(COMMENT_LINE_STARTER)) {
                 firstLine = reader.readLine();
             }
             return firstLine.matches(
                     "^" + CombinedFormatUtils.DICTIONARY_TAG + "=[^:]+(:[^=]+=[^:]+)*");
-        } catch (FileNotFoundException e) {
+        } catch (final IOException e) {
             return false;
-        } catch (IOException e) {
-            return false;
-        } finally {
-            if (reader != null) {
-                try {
-                    reader.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
         }
     }
 
@@ -87,12 +71,11 @@
      * This is the public method that will read a combined file and return the corresponding memory
      * representation.
      *
-     * @param source the file to read the data from.
+     * @param reader the buffered reader to read the data from.
      * @return the in-memory representation of the dictionary.
      */
-    public static FusionDictionary readDictionaryCombined(final InputStream source)
+    public static FusionDictionary readDictionaryCombined(final BufferedReader reader)
             throws IOException {
-        final BufferedReader reader = new BufferedReader(new InputStreamReader(source, "UTF-8"));
         String headerLine = reader.readLine();
         while (headerLine.startsWith(COMMENT_LINE_STARTER)) {
             headerLine = reader.readLine();
@@ -218,11 +201,11 @@
     /**
      * Writes a dictionary to a combined file.
      *
-     * @param destination a destination stream to write to.
+     * @param destination a destination writer.
      * @param dict the dictionary to write.
      */
-    public static void writeDictionaryCombined(
-            final Writer destination, final FusionDictionary dict) throws IOException {
+    public static void writeDictionaryCombined(final BufferedWriter destination,
+            final FusionDictionary dict) throws IOException {
         final TreeSet<WordProperty> wordPropertiesInDict = new TreeSet<>();
         for (final WordProperty wordProperty : dict) {
             // This for ordering by frequency, then by asciibetic order
@@ -232,6 +215,5 @@
         for (final WordProperty wordProperty : wordPropertiesInDict) {
             destination.write(CombinedFormatUtils.formatWordProperty(wordProperty));
         }
-        destination.close();
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Compress.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Compress.java
index b7f48b5..728a159 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Compress.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Compress.java
@@ -16,11 +16,6 @@
 
 package com.android.inputmethod.latin.dicttool;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -32,8 +27,7 @@
         // This container class is not publicly instantiable.
     }
 
-    public static OutputStream getCompressedStream(final OutputStream out)
-        throws java.io.IOException {
+    public static OutputStream getCompressedStream(final OutputStream out) throws IOException {
         return new GZIPOutputStream(out);
     }
 
@@ -43,7 +37,6 @@
 
     static public class Compressor extends Dicttool.Command {
         public static final String COMMAND = "compress";
-        public static final String STDIN_OR_STDOUT = "-";
 
         public Compressor() {
         }
@@ -61,17 +54,18 @@
             }
             final String inFilename = mArgs.length >= 1 ? mArgs[0] : STDIN_OR_STDOUT;
             final String outFilename = mArgs.length >= 2 ? mArgs[1] : STDIN_OR_STDOUT;
-            final InputStream input = inFilename.equals(STDIN_OR_STDOUT) ? System.in
-                    : new BufferedInputStream(new FileInputStream(new File(inFilename)));
-            final OutputStream output = outFilename.equals(STDIN_OR_STDOUT) ? System.out
-                    : new BufferedOutputStream(new FileOutputStream(new File(outFilename)));
-            BinaryDictOffdeviceUtils.copy(input, new GZIPOutputStream(output));
+            try (
+                final InputStream input = getFileInputStreamOrStdIn(inFilename);
+                final OutputStream compressedOutput = getCompressedStream(
+                        getFileOutputStreamOrStdOut(outFilename))
+            ) {
+                BinaryDictOffdeviceUtils.copy(input, compressedOutput);
+            }
         }
     }
 
     static public class Uncompressor extends Dicttool.Command {
         public static final String COMMAND = "uncompress";
-        public static final String STDIN_OR_STDOUT = "-";
 
         public Uncompressor() {
         }
@@ -89,11 +83,13 @@
             }
             final String inFilename = mArgs.length >= 1 ? mArgs[0] : STDIN_OR_STDOUT;
             final String outFilename = mArgs.length >= 2 ? mArgs[1] : STDIN_OR_STDOUT;
-            final InputStream input = inFilename.equals(STDIN_OR_STDOUT) ? System.in
-                    : new BufferedInputStream(new FileInputStream(new File(inFilename)));
-            final OutputStream output = outFilename.equals(STDIN_OR_STDOUT) ? System.out
-                    : new BufferedOutputStream(new FileOutputStream(new File(outFilename)));
-            BinaryDictOffdeviceUtils.copy(new GZIPInputStream(input), output);
+            try (
+                final InputStream uncompressedInput = getUncompressedStream(
+                        getFileInputStreamOrStdIn(inFilename));
+                final OutputStream output = getFileOutputStreamOrStdOut(outFilename)
+            ) {
+                BinaryDictOffdeviceUtils.copy(uncompressedInput, output);
+            }
         }
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
index 37c8d41..3d0557b 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
@@ -27,19 +27,23 @@
 import com.android.inputmethod.latin.makedict.Ver2DictEncoder;
 import com.android.inputmethod.latin.makedict.Ver4DictEncoder;
 
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.util.Arrays;
 import java.util.LinkedList;
 
 import javax.xml.parsers.ParserConfigurationException;
 
-import org.xml.sax.SAXException;
-
 /**
  * Main class/method for DictionaryMaker.
  */
@@ -279,22 +283,21 @@
      */
     private static FusionDictionary readCombinedFile(final String combinedFilename)
         throws FileNotFoundException, IOException {
-        FileInputStream inStream = null;
-        try {
-            final File file = new File(combinedFilename);
-            inStream = new FileInputStream(file);
-            return CombinedInputOutput.readDictionaryCombined(inStream);
-        } finally {
-            if (null != inStream) {
-                try {
-                    inStream.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
+        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(
+                new FileInputStream(combinedFilename), "UTF-8"))
+        ) {
+            return CombinedInputOutput.readDictionaryCombined(reader);
         }
     }
 
+    private static BufferedInputStream getBufferedFileInputStream(final String filename)
+            throws FileNotFoundException {
+        if (filename == null) {
+            return null;
+        }
+        return new BufferedInputStream(new FileInputStream(filename));
+    }
+
     /**
      * Read a dictionary from a unigram XML file, and optionally a bigram XML file.
      *
@@ -310,12 +313,13 @@
     private static FusionDictionary readXmlFile(final String unigramXmlFilename,
             final String shortcutXmlFilename, final String bigramXmlFilename)
             throws FileNotFoundException, SAXException, IOException, ParserConfigurationException {
-        final FileInputStream unigrams = new FileInputStream(new File(unigramXmlFilename));
-        final FileInputStream shortcuts = null == shortcutXmlFilename ? null :
-                new FileInputStream(new File(shortcutXmlFilename));
-        final FileInputStream bigrams = null == bigramXmlFilename ? null :
-                new FileInputStream(new File(bigramXmlFilename));
-        return XmlDictInputOutput.readDictionaryXml(unigrams, shortcuts, bigrams);
+        try (
+            final BufferedInputStream unigrams = getBufferedFileInputStream(unigramXmlFilename);
+            final BufferedInputStream shortcuts = getBufferedFileInputStream(shortcutXmlFilename);
+            final BufferedInputStream bigrams = getBufferedFileInputStream(bigramXmlFilename);
+        ) {
+            return XmlDictInputOutput.readDictionaryXml(unigrams, shortcuts, bigrams);
+        }
     }
 
     /**
@@ -374,8 +378,9 @@
      */
     private static void writeXmlDictionary(final String outputFilename,
             final FusionDictionary dict) throws FileNotFoundException, IOException {
-        XmlDictInputOutput.writeDictionaryXml(new BufferedWriter(new FileWriter(outputFilename)),
-                dict);
+        try (final BufferedWriter writer = new BufferedWriter(new FileWriter(outputFilename))) {
+            XmlDictInputOutput.writeDictionaryXml(writer, dict);
+        }
     }
 
     /**
@@ -388,7 +393,8 @@
      */
     private static void writeCombinedDictionary(final String outputFilename,
             final FusionDictionary dict) throws FileNotFoundException, IOException {
-        CombinedInputOutput.writeDictionaryCombined(
-                new BufferedWriter(new FileWriter(outputFilename)), dict);
+        try (final BufferedWriter writer = new BufferedWriter(new FileWriter(outputFilename))) {
+            CombinedInputOutput.writeDictionaryCombined(writer, dict);
+        }
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Dicttool.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Dicttool.java
index 8ae035f..e49b350 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Dicttool.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Dicttool.java
@@ -16,23 +16,63 @@
 
 package com.android.inputmethod.latin.dicttool;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.HashMap;
 
 public class Dicttool {
 
     public static abstract class Command {
+        public static final String STDIN_OR_STDOUT = "-";
         protected String[] mArgs;
+
         public void setArgs(String[] args) throws IllegalArgumentException {
             mArgs = args;
         }
+
+        protected static InputStream getFileInputStreamOrStdIn(final String inFilename)
+                throws FileNotFoundException {
+            if (STDIN_OR_STDOUT.equals(inFilename)) {
+                return System.in;
+            }
+            return getFileInputStream(new File(inFilename));
+        }
+
+        protected static InputStream getFileInputStream(final File inFile)
+                throws FileNotFoundException {
+            return new BufferedInputStream(new FileInputStream(inFile));
+        }
+
+        protected static OutputStream getFileOutputStreamOrStdOut(final String outFilename)
+                throws FileNotFoundException {
+            if (STDIN_OR_STDOUT.equals(outFilename)) {
+                return System.out;
+            }
+            return getFileOutputStream(new File(outFilename));
+        }
+
+        protected static OutputStream getFileOutputStream(final File outFile)
+                throws FileNotFoundException {
+            return new BufferedOutputStream(new FileOutputStream(outFile));
+        }
+
         abstract public String getHelp();
         abstract public void run() throws Exception;
     }
+
     static HashMap<String, Class<? extends Command>> sCommands = new HashMap<>();
+
     static {
         CommandList.populate();
     }
+
     public static void addCommand(final String commandName, final Class<? extends Command> cls) {
         sCommands.put(commandName, cls);
     }
@@ -60,7 +100,7 @@
         return sCommands.containsKey(commandName);
     }
 
-    private Command getCommand(final String[] arguments) {
+    private static Command getCommand(final String[] arguments) {
         final String commandName = arguments[0];
         if (!isCommand(commandName)) {
             throw new RuntimeException("Unknown command : " + commandName);
@@ -76,7 +116,7 @@
      * @param arguments the arguments passed to dicttool.
      * @return 0 for success, an error code otherwise (always 1 at the moment)
      */
-    private int execute(final String[] arguments) {
+    private static int execute(final String[] arguments) {
         final Command command = getCommand(arguments);
         try {
             command.run();
@@ -95,6 +135,6 @@
             return;
         }
         // Exit with the success/error code from #execute() as status.
-        System.exit(new Dicttool().execute(arguments));
+        System.exit(execute(arguments));
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
index dff3387..1f67982 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
@@ -21,8 +21,9 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 
 public class Package {
     private Package() {
@@ -86,9 +87,13 @@
             }
             System.out.println("Packaging : " + decodedSpec.describeChain());
             System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
-            final FileOutputStream dstStream = new FileOutputStream(new File(mArgs[1]));
-            BinaryDictOffdeviceUtils.copy(new BufferedInputStream(
-                    new FileInputStream(decodedSpec.mFile)), new BufferedOutputStream(dstStream));
+            try (
+                final InputStream input = getFileInputStream(decodedSpec.mFile);
+                final OutputStream output = new BufferedOutputStream(
+                        getFileOutputStreamOrStdOut(mArgs[1]))
+            ) {
+                BinaryDictOffdeviceUtils.copy(input, output);
+            }
         }
     }
 }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/XmlDictInputOutput.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
index 7435fa7..bdec447 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/XmlDictInputOutput.java
@@ -23,13 +23,16 @@
 import com.android.inputmethod.latin.makedict.WeightedString;
 import com.android.inputmethod.latin.makedict.WordProperty;
 
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
+import java.io.BufferedWriter;
+import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.Writer;
+import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.TreeSet;
@@ -38,10 +41,6 @@
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
 
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
 /**
  * Reads and writes XML files for a FusionDictionary.
  *
@@ -57,8 +56,6 @@
     private static final String WORD_ATTR = "word";
     private static final String NOT_A_WORD_ATTR = "not_a_word";
 
-    private static final String OPTIONS_KEY = "options";
-
     /**
      * SAX handler for a unigram XML file.
      */
@@ -120,7 +117,6 @@
                     final String attrName = attrs.getLocalName(attrIndex);
                     attributes.put(attrName, attrs.getValue(attrIndex));
                 }
-                final String optionsString = attributes.get(OPTIONS_KEY);
                 mDictionary = new FusionDictionary(new PtNodeArray(),
                         new DictionaryOptions(attributes));
             } else {
@@ -244,14 +240,13 @@
         protected int getValueFromFreqString(final String freqString) {
             if (WHITELIST_MARKER.equals(freqString)) {
                 return WHITELIST_FREQ_VALUE;
-            } else {
-                final int intValue = super.getValueFromFreqString(freqString);
-                if (intValue < MIN_FREQ || intValue > MAX_FREQ) {
-                    throw new RuntimeException("Shortcut freq out of range. Accepted range is "
-                            + MIN_FREQ + ".." + MAX_FREQ);
-                }
-                return intValue;
             }
+            final int intValue = super.getValueFromFreqString(freqString);
+            if (intValue < MIN_FREQ || intValue > MAX_FREQ) {
+                throw new RuntimeException("Shortcut freq out of range. Accepted range is "
+                        + MIN_FREQ + ".." + MAX_FREQ);
+            }
+            return intValue;
         }
 
         // As per getAssocMap(), this never returns null.
@@ -269,23 +264,12 @@
      * @return true if the file is in the unigram XML format, false otherwise
      */
     public static boolean isXmlUnigramDictionary(final String filename) {
-        BufferedReader reader = null;
-        try {
-            reader = new BufferedReader(new FileReader(new File(filename)));
+        try (final BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(filename), "UTF-8"))) {
             final String firstLine = reader.readLine();
             return firstLine.matches("^\\s*<wordlist .*>\\s*$");
-        } catch (FileNotFoundException e) {
+        } catch (final IOException e) {
             return false;
-        } catch (IOException e) {
-            return false;
-        } finally {
-            if (reader != null) {
-                try {
-                    reader.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
         }
     }
 
@@ -300,8 +284,8 @@
      * @param bigrams the file to read the bigrams from, or null.
      * @return the in-memory representation of the dictionary.
      */
-    public static FusionDictionary readDictionaryXml(final InputStream unigrams,
-            final InputStream shortcuts, final InputStream bigrams)
+    public static FusionDictionary readDictionaryXml(final BufferedInputStream unigrams,
+            final BufferedInputStream shortcuts, final BufferedInputStream bigrams)
             throws SAXException, IOException, ParserConfigurationException {
         final SAXParserFactory factory = SAXParserFactory.newInstance();
         factory.setNamespaceAware(true);
@@ -350,8 +334,8 @@
      * @param destination a destination stream to write to.
      * @param dict the dictionary to write.
      */
-    public static void writeDictionaryXml(Writer destination, FusionDictionary dict)
-            throws IOException {
+    public static void writeDictionaryXml(final BufferedWriter destination,
+            final FusionDictionary dict) throws IOException {
         final TreeSet<WordProperty> wordPropertiesInDict = new TreeSet<>();
         for (WordProperty wordProperty : dict) {
             wordPropertiesInDict.add(wordProperty);
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index fccb654..0236a44 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -62,13 +62,13 @@
 
         final File dst = File.createTempFile("testGetRawDict", ".tmp");
         dst.deleteOnExit();
-
-        final OutputStream out = Compress.getCompressedStream(
+        try (final OutputStream out = Compress.getCompressedStream(
                 Compress.getCompressedStream(
                         Compress.getCompressedStream(
-                                new BufferedOutputStream(new FileOutputStream(dst)))));
-        final DictEncoder dictEncoder = new Ver2DictEncoder(out);
-        dictEncoder.writeDictionary(dict, new FormatOptions(2, false));
+                                new BufferedOutputStream(new FileOutputStream(dst)))))) {
+            final DictEncoder dictEncoder = new Ver2DictEncoder(out);
+            dictEncoder.writeDictionary(dict, new FormatOptions(2, false));
+        }
 
         // Test for an actually compressed dictionary and its contents
         final BinaryDictOffdeviceUtils.DecoderChainSpec decodeSpec =
@@ -96,11 +96,11 @@
         // Randomly create some 4k file containing garbage
         final File dst = File.createTempFile("testGetRawDict", ".tmp");
         dst.deleteOnExit();
-        final OutputStream out = new BufferedOutputStream(new FileOutputStream(dst));
-        for (int i = 0; i < 1024; ++i) {
-            out.write(0x12345678);
+        try (final OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
+            for (int i = 0; i < 1024; ++i) {
+                out.write(0x12345678);
+            }
         }
-        out.close();
 
         // Test that a random data file actually fails
         assertNull("Wrongly identified data file",
@@ -108,12 +108,12 @@
 
         final File gzDst = File.createTempFile("testGetRawDict", ".tmp");
         gzDst.deleteOnExit();
-        final OutputStream gzOut =
-                Compress.getCompressedStream(new BufferedOutputStream(new FileOutputStream(gzDst)));
-        for (int i = 0; i < 1024; ++i) {
-            gzOut.write(0x12345678);
+        try (final OutputStream gzOut = Compress.getCompressedStream(
+                new BufferedOutputStream(new FileOutputStream(gzDst)))) {
+            for (int i = 0; i < 1024; ++i) {
+                gzOut.write(0x12345678);
+            }
         }
-        gzOut.close();
 
         // Test that a compressed random data file actually fails
         assertNull("Wrongly identified data file",