Merge "Import translations. DO NOT MERGE" into jb-ub-latinimegoogle
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
index 9779c68..f123735 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
@@ -162,9 +162,11 @@
                 addShownCategoryId(CATEGORY_ID_OBJECTS);
                 addShownCategoryId(CATEGORY_ID_NATURE);
                 addShownCategoryId(CATEGORY_ID_PLACES);
-                mCurrentCategoryId = CATEGORY_ID_PEOPLE;
+                mCurrentCategoryId =
+                        Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_PEOPLE);
             } else {
-                mCurrentCategoryId = CATEGORY_ID_SYMBOLS;
+                mCurrentCategoryId =
+                        Settings.readLastShownEmojiCategoryId(mPrefs, CATEGORY_ID_SYMBOLS);
             }
             addShownCategoryId(CATEGORY_ID_SYMBOLS);
             addShownCategoryId(CATEGORY_ID_EMOTICONS);
@@ -222,6 +224,7 @@
 
         public void setCurrentCategoryId(int categoryId) {
             mCurrentCategoryId = categoryId;
+            Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId);
         }
 
         public void setCurrentCategoryPageId(int id) {
@@ -233,7 +236,7 @@
         }
 
         public void saveLastTypedCategoryPage() {
-            Settings.writeEmojiCategoryLastTypedId(
+            Settings.writeLastTypedEmojiCategoryPageId(
                     mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
         }
 
@@ -254,7 +257,7 @@
         // Returns the view pager's page position for the categoryId
         public int getPageIdFromCategoryId(int categoryId) {
             final int lastSavedCategoryPageId =
-                    Settings.readEmojiCategoryLastTypedId(mPrefs, categoryId);
+                    Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId);
             int sum = 0;
             for (int i = 0; i < mShownCategories.size(); ++i) {
                 final CategoryProperties props = mShownCategories.get(i);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 9760983..b7521b9 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -155,7 +155,7 @@
     }
 
     public void saveKeyboardState() {
-        if (getKeyboard() != null || isShowingEmojiKeyboard()) {
+        if (getKeyboard() != null || isShowingEmojiPalettes()) {
             mState.onSaveKeyboardState();
         }
     }
@@ -316,19 +316,23 @@
         mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
     }
 
-    public boolean isShowingEmojiKeyboard() {
-        return mEmojiPalettesView != null && mEmojiPalettesView.getVisibility() == View.VISIBLE;
+    private boolean isShowingMainKeyboard() {
+        return null != mKeyboardView && mKeyboardView.isShown();
+    }
+
+    public boolean isShowingEmojiPalettes() {
+        return mEmojiPalettesView != null && mEmojiPalettesView.isShown();
     }
 
     public boolean isShowingMoreKeysPanel() {
-        if (isShowingEmojiKeyboard()) {
+        if (isShowingEmojiPalettes()) {
             return false;
         }
         return mKeyboardView.isShowingMoreKeysPanel();
     }
 
     public View getVisibleKeyboardView() {
-        if (isShowingEmojiKeyboard()) {
+        if (isShowingEmojiPalettes()) {
             return mEmojiPalettesView;
         }
         return mKeyboardView;
@@ -348,6 +352,10 @@
         }
     }
 
+    public boolean isShowingMainKeyboardOrEmojiPalettes() {
+        return isShowingMainKeyboard() || isShowingEmojiPalettes();
+    }
+
     public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) {
         if (mKeyboardView != null) {
             mKeyboardView.closing();
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 642b3a4..2e9280c 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.latin.utils.StringUtils;
-
 import android.text.TextUtils;
 
 /**
@@ -85,8 +83,4 @@
     private boolean didCommitTypedWord() {
         return TextUtils.equals(mTypedWord, mCommittedWord);
     }
-
-    public static int getSeparatorLength(final String separatorString) {
-        return StringUtils.codePointCount(separatorString);
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 46b7512..fac595e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1261,9 +1261,7 @@
             final boolean needsInputViewShown) {
         // TODO: Modify this if we support suggestions with hard keyboard
         if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
-            final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
-            final boolean inputViewShown = (mainKeyboardView != null)
-                    ? mainKeyboardView.isShown() : false;
+            final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes();
             final boolean shouldShowSuggestions = shown
                     && (needsInputViewShown ? inputViewShown : true);
             if (isFullscreenMode()) {
@@ -1329,7 +1327,7 @@
         if (visibleKeyboardView.isShown()) {
             // Note that the height of Emoji layout is the same as the height of the main keyboard
             // and the suggestion strip
-            if (mKeyboardSwitcher.isShowingEmojiKeyboard()
+            if (mKeyboardSwitcher.isShowingEmojiPalettes()
                     || mSuggestionStripView.getVisibility() == View.VISIBLE) {
                 visibleTopY -= suggestionsHeight;
             }
@@ -1713,7 +1711,8 @@
         mSpaceState = SPACE_STATE_NONE;
         final boolean didAutoCorrect;
         final SettingsValues settingsValues = mSettings.getCurrent();
-        if (settingsValues.isWordSeparator(primaryCode)) {
+        if (settingsValues.isWordSeparator(primaryCode)
+                || Character.getType(primaryCode) == Character.OTHER_SYMBOL) {
             didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
         } else {
             didAutoCorrect = false;
@@ -2980,8 +2979,8 @@
         final String originallyTypedWord = mLastComposedWord.mTypedWord;
         final String committedWord = mLastComposedWord.mCommittedWord;
         final int cancelLength = committedWord.length();
-        final int separatorLength = LastComposedWord.getSeparatorLength(
-                mLastComposedWord.mSeparatorString);
+        // We want java chars, not codepoints for the following.
+        final int separatorLength = mLastComposedWord.mSeparatorString.length();
         // TODO: should we check our saved separator against the actual contents of the text view?
         final int deleteLength = cancelLength + separatorLength;
         if (DEBUG) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
index 9f7f502..fda97da 100644
--- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
@@ -60,7 +60,8 @@
                         0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
                         0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
                         new FormatOptions(version,
-                                0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE)));
+                                0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE),
+                                0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG)));
         return header;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
index 3362771..28da9ff 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -217,6 +218,25 @@
     }
 
     /**
+     * Converts a list of WeightedString to a list of PendingAttribute.
+     */
+    public static ArrayList<PendingAttribute> resolveBigramPositions(final DictUpdater dictUpdater,
+            final ArrayList<WeightedString> bigramStrings)
+                    throws IOException, UnsupportedFormatException {
+        if (bigramStrings == null) return CollectionUtils.newArrayList();
+        final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList();
+        for (final WeightedString bigram : bigramStrings) {
+            final int pos = dictUpdater.getTerminalPosition(bigram.mWord);
+            if (pos == FormatSpec.NOT_VALID_WORD) {
+                // TODO: figure out what is the correct thing to do here.
+            } else {
+                bigrams.add(new PendingAttribute(bigram.mFrequency, pos));
+            }
+        }
+        return bigrams;
+    }
+
+    /**
      * Insert a word into a binary dictionary.
      *
      * @param dictUpdater the dict updater.
@@ -238,18 +258,9 @@
             final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
             final boolean isBlackListEntry)
                     throws IOException, UnsupportedFormatException {
-        final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
+        final ArrayList<PendingAttribute> bigrams = resolveBigramPositions(dictUpdater,
+                bigramStrings);
         final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
-        if (bigramStrings != null) {
-            for (final WeightedString bigram : bigramStrings) {
-                int position = dictUpdater.getTerminalPosition(bigram.mWord);
-                if (position == FormatSpec.NOT_VALID_WORD) {
-                    // TODO: figure out what is the correct thing to do here.
-                } else {
-                    bigrams.add(new PendingAttribute(bigram.mFrequency, position));
-                }
-            }
-        }
 
         final boolean isTerminal = true;
         final boolean hasBigrams = !bigrams.isEmpty();
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 5a5d7af..6d58270 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -37,13 +37,15 @@
      * sion
      *
      * o |
-     * p | not used                                4 bits
-     * t | has bigrams ?                           1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG
-     * i | FRENCH_LIGATURE_PROCESSING_FLAG
-     * o | supports dynamic updates ?              1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE
-     * n | GERMAN_UMLAUT_PROCESSING_FLAG
-     * f |
-     * lags
+     * p | not used                                3 bits
+     * t | each unigram and bigram entry has a time stamp?
+     * i |                                         1 bit, 1 = yes, 0 = no : CONTAINS_TIMESTAMP_FLAG
+     * o | has bigrams ?                           1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG
+     * n | FRENCH_LIGATURE_PROCESSING_FLAG
+     * f | supports dynamic updates ?              1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE
+     * l | GERMAN_UMLAUT_PROCESSING_FLAG
+     * a |
+     * gs
      *
      * h |
      * e | size of the file header, 4bytes
@@ -211,6 +213,7 @@
     static final int SUPPORTS_DYNAMIC_UPDATE = 0x2;
     static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
     static final int CONTAINS_BIGRAMS_FLAG = 0x8;
+    static final int CONTAINS_TIMESTAMP_FLAG = 0x10;
 
     // TODO: Make this value adaptative to content data, store it in the header, and
     // use it in the reading code.
@@ -263,6 +266,7 @@
     // These values are used only by version 4 or later.
     static final String TRIE_FILE_EXTENSION = ".trie";
     static final String FREQ_FILE_EXTENSION = ".freq";
+    static final String UNIGRAM_TIMESTAMP_FILE_EXTENSION = ".timestamp";
     // tat = Terminal Address Table
     static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat";
     static final String BIGRAM_FILE_EXTENSION = ".bigram";
@@ -271,14 +275,20 @@
     static final String CONTENT_TABLE_FILE_SUFFIX = "_index";
     static final int FREQUENCY_AND_FLAGS_SIZE = 2;
     static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
+    static final int UNIGRAM_TIMESTAMP_SIZE = 4;
 
     // With the English main dictionary as of October 2013, the size of bigram address table is
     // is 584KB with the block size being 4.
     // This is 91% of that of full address table.
     static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4;
-    static final int BIGRAM_CONTENT_COUNT = 1;
+    static final int BIGRAM_CONTENT_COUNT = 2;
     static final int BIGRAM_FREQ_CONTENT_INDEX = 0;
+    static final int BIGRAM_TIMESTAMP_CONTENT_INDEX = 1;
     static final String BIGRAM_FREQ_CONTENT_ID = "_freq";
+    static final String BIGRAM_TIMESTAMP_CONTENT_ID = "_timestamp";
+    static final int BIGRAM_TIMESTAMP_SIZE = 4;
+    static final int BIGRAM_COUNTER_SIZE = 1;
+    static final int BIGRAM_LEVEL_SIZE = 1;
 
     static final int SHORTCUT_CONTENT_COUNT = 1;
     static final int SHORTCUT_CONTENT_INDEX = 0;
@@ -321,6 +331,7 @@
         public final int mVersion;
         public final boolean mSupportsDynamicUpdate;
         public final boolean mHasTerminalId;
+        public final boolean mHasTimestamp;
         @UsedForTesting
         public FormatOptions(final int version) {
             this(version, false);
@@ -328,6 +339,11 @@
 
         @UsedForTesting
         public FormatOptions(final int version, final boolean supportsDynamicUpdate) {
+            this(version, supportsDynamicUpdate, false /* hasTimestamp */);
+        }
+
+        public FormatOptions(final int version, final boolean supportsDynamicUpdate,
+                final boolean hasTimestamp) {
             mVersion = version;
             if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) {
                 throw new RuntimeException("Dynamic updates are only supported with versions "
@@ -335,6 +351,7 @@
             }
             mSupportsDynamicUpdate = supportsDynamicUpdate;
             mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID);
+            mHasTimestamp = hasTimestamp;
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 5372907..734223e 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -153,8 +153,12 @@
         final File contentFile = new File(mDictDirectory, mDictDirectory.getName()
                 + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
                 + FormatSpec.SHORTCUT_CONTENT_ID);
+        final File timestampsFile = new File(mDictDirectory, mDictDirectory.getName()
+                + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
+                + FormatSpec.SHORTCUT_CONTENT_ID);
         mShortcutAddressTable = SparseTable.readFromFiles(lookupIndexFile,
-                new File[] { contentFile }, FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE);
+                new File[] { contentFile, timestampsFile },
+                FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE);
     }
 
     protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader {
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index f9dcacf..5d5ab04 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -45,6 +45,7 @@
     private int mHeaderSize;
     private OutputStream mTrieOutStream;
     private OutputStream mFreqOutStream;
+    private OutputStream mUnigramTimestampOutStream;
     private OutputStream mTerminalAddressTableOutStream;
     private File mDictDir;
     private String mBaseFilename;
@@ -69,16 +70,16 @@
         private final File[] mContentFiles;
         protected final OutputStream[] mContentOutStreams;
 
-        public SparseTableContentWriter(final String name, final int contentCount,
-                final int initialCapacity, final int blockSize, final File baseDir,
-                final String[] contentFilenames, final String[] contentIds) {
+        public SparseTableContentWriter(final String name, final int initialCapacity,
+                final int blockSize, final File baseDir, final String[] contentFilenames,
+                final String[] contentIds) {
             if (contentFilenames.length != contentIds.length) {
                 throw new RuntimeException("The length of contentFilenames and the length of"
                         + " contentIds are different " + contentFilenames.length + ", "
                         + contentIds.length);
             }
-            mContentCount = contentCount;
-            mSparseTable = new SparseTable(initialCapacity, blockSize, contentCount);
+            mContentCount = contentFilenames.length;
+            mSparseTable = new SparseTable(initialCapacity, blockSize, mContentCount);
             mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
             mAddressTableFiles = new File[mContentCount];
             mContentFiles = new File[mContentCount];
@@ -113,16 +114,40 @@
     }
 
     private static class BigramContentWriter extends SparseTableContentWriter {
+        private final boolean mWriteTimestamp;
 
         public BigramContentWriter(final String name, final int initialCapacity,
-                final File baseDir) {
-            super(name + FormatSpec.BIGRAM_FILE_EXTENSION, FormatSpec.BIGRAM_CONTENT_COUNT,
-                    initialCapacity, FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
-                    new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION },
-                    new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID });
+                final File baseDir, final boolean writeTimestamp) {
+            super(name + FormatSpec.BIGRAM_FILE_EXTENSION, initialCapacity,
+                    FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+                    getContentFilenames(name, writeTimestamp), getContentIds(writeTimestamp));
+            mWriteTimestamp = writeTimestamp;
         }
 
-        public void writeBigramsForOneWord(final int terminalId,
+        private static String[] getContentFilenames(final String name,
+                final boolean writeTimestamp) {
+            final String[] contentFilenames;
+            if (writeTimestamp) {
+                contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION,
+                        name + FormatSpec.BIGRAM_FILE_EXTENSION };
+            } else {
+                contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION };
+            }
+            return contentFilenames;
+        }
+
+        private static String[] getContentIds(final boolean writeTimestamp) {
+            final String[] contentIds;
+            if (writeTimestamp) {
+                contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID,
+                        FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID };
+            } else {
+                contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID };
+            }
+            return contentIds;
+        }
+
+        public void writeBigramsForOneWord(final int terminalId, final int bigramCount,
                 final Iterator<WeightedString> bigramIterator, final FusionDictionary dict)
                         throws IOException {
             write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId,
@@ -130,8 +155,16 @@
                         @Override
                         public void write(final OutputStream outStream) throws IOException {
                             writeBigramsForOneWordInternal(outStream, bigramIterator, dict);
-                        }
-            });
+                        }});
+            if (mWriteTimestamp) {
+                write(FormatSpec.BIGRAM_TIMESTAMP_CONTENT_INDEX, terminalId,
+                        new SparseTableContentWriterInterface() {
+                            @Override
+                            public void write(final OutputStream outStream) throws IOException {
+                                initBigramTimestampsCountersAndLevelsForOneWordInternal(outStream,
+                                        bigramCount);
+                            }});
+            }
         }
 
         private void writeBigramsForOneWordInternal(final OutputStream outStream,
@@ -151,13 +184,26 @@
                         FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE);
             }
         }
+
+        private void initBigramTimestampsCountersAndLevelsForOneWordInternal(
+                final OutputStream outStream, final int bigramCount) throws IOException {
+            for (int i = 0; i < bigramCount; ++i) {
+                // TODO: Figure out what initial values should be.
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
+                        FormatSpec.BIGRAM_TIMESTAMP_SIZE);
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
+                        FormatSpec.BIGRAM_COUNTER_SIZE);
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */,
+                        FormatSpec.BIGRAM_LEVEL_SIZE);
+            }
+        }
     }
 
     private static class ShortcutContentWriter extends SparseTableContentWriter {
         public ShortcutContentWriter(final String name, final int initialCapacity,
                 final File baseDir) {
-            super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, FormatSpec.SHORTCUT_CONTENT_COUNT,
-                    initialCapacity, FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+            super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, initialCapacity,
+                    FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
                     new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION },
                     new String[] { FormatSpec.SHORTCUT_CONTENT_ID });
         }
@@ -193,18 +239,20 @@
         mDictDir = new File(mDictPlacedDir, mBaseFilename);
         final File trieFile = new File(mDictDir, mBaseFilename + FormatSpec.TRIE_FILE_EXTENSION);
         final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION);
+        final File timestampFile = new File(mDictDir,
+                mBaseFilename + FormatSpec.UNIGRAM_TIMESTAMP_FILE_EXTENSION);
         final File terminalAddressTableFile = new File(mDictDir,
                 mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
         if (!mDictDir.isDirectory()) {
             if (mDictDir.exists()) mDictDir.delete();
             mDictDir.mkdirs();
         }
-        if (!trieFile.exists()) trieFile.createNewFile();
-        if (!freqFile.exists()) freqFile.createNewFile();
-        if (!terminalAddressTableFile.exists()) terminalAddressTableFile.createNewFile();
         mTrieOutStream = new FileOutputStream(trieFile);
         mFreqOutStream = new FileOutputStream(freqFile);
         mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile);
+        if (formatOptions.mHasTimestamp) {
+            mUnigramTimestampOutStream = new FileOutputStream(timestampFile);
+        }
     }
 
     private void close() throws IOException {
@@ -218,6 +266,9 @@
             if (mTerminalAddressTableOutStream != null) {
                 mTerminalAddressTableOutStream.close();
             }
+            if (mUnigramTimestampOutStream != null) {
+                mUnigramTimestampOutStream.close();
+            }
         } finally {
             mTrieOutStream = null;
             mFreqOutStream = null;
@@ -257,7 +308,11 @@
         if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes);
 
         writeTerminalData(flatNodes, terminalCount);
-        mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir);
+        if (formatOptions.mHasTimestamp) {
+            initUnigramTimestamps(terminalCount);
+        }
+        mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir,
+                formatOptions.mHasTimestamp);
         writeBigrams(flatNodes, dict);
         mShortcutWriter = new ShortcutContentWriter(mBaseFilename, terminalCount, mDictDir);
         writeShortcuts(flatNodes);
@@ -348,7 +403,7 @@
         for (final PtNodeArray nodeArray : flatNodes) {
             for (final PtNode ptNode : nodeArray.mData) {
                 if (ptNode.mBigrams != null) {
-                    mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId,
+                    mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId, ptNode.mBigrams.size(),
                             ptNode.mBigrams.iterator(), dict);
                 }
             }
@@ -408,4 +463,11 @@
         mFreqOutStream.write(freqBuf);
         mTerminalAddressTableOutStream.write(terminalAddressTableBuf);
     }
+
+    private void initUnigramTimestamps(final int terminalCount) throws IOException {
+        // Initial value of time stamps for each word is 0.
+        final byte[] unigramTimestampBuf =
+                new byte[terminalCount * FormatSpec.UNIGRAM_TIMESTAMP_SIZE];
+        mUnigramTimestampOutStream.write(unigramTimestampBuf);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 1a0fecc..dc005bb 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -101,6 +101,7 @@
     // Emoji
     public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys";
     public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
+    public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id";
 
     private Resources mRes;
     private SharedPreferences mPrefs;
@@ -383,15 +384,25 @@
         return prefs.getString(PREF_EMOJI_RECENT_KEYS, "");
     }
 
-    public static void writeEmojiCategoryLastTypedId(
-            final SharedPreferences prefs, final int category, final int id) {
-        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
-        prefs.edit().putInt(key, id).apply();
+    public static void writeLastTypedEmojiCategoryPageId(
+            final SharedPreferences prefs, final int categoryId, final int categoryPageId) {
+        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
+        prefs.edit().putInt(key, categoryPageId).apply();
     }
 
-    public static int readEmojiCategoryLastTypedId(
-            final SharedPreferences prefs, final int category) {
-        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
+    public static int readLastTypedEmojiCategoryPageId(
+            final SharedPreferences prefs, final int categoryId) {
+        final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + categoryId;
         return prefs.getInt(key, 0);
     }
+
+    public static void writeLastShownEmojiCategoryId(
+            final SharedPreferences prefs, final int categoryId) {
+        prefs.edit().putInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, categoryId).apply();
+    }
+
+    public static int readLastShownEmojiCategoryId(
+            final SharedPreferences prefs, final int defValue) {
+        return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_ID, defValue);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 8d2689a..faa5560 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -302,18 +302,19 @@
         final int countInStrip = mSuggestionsCountInStrip;
         setupWordViewsTextAndColor(suggestedWords, countInStrip);
         final TextView centerWordView = mWordViews.get(mCenterPositionInStrip);
-        final int stripWidth = placerView.getWidth();
-        final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth);
+        final int availableStripWidth = placerView.getWidth()
+                - placerView.getPaddingRight() - placerView.getPaddingLeft();
+        final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, availableStripWidth);
         if (getTextScaleX(centerWordView.getText(), centerWidth, centerWordView.getPaint())
                 < MIN_TEXT_XSCALE) {
             // Layout only the most relevant suggested word at the center of the suggestion strip
             // by consolidating all slots in the strip.
             mMoreSuggestionsAvailable = (suggestedWords.size() > 1);
-            layoutWord(mCenterPositionInStrip, stripWidth);
+            layoutWord(mCenterPositionInStrip, availableStripWidth - mPadding);
             stripView.addView(centerWordView);
             setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT);
             if (SuggestionStripView.DBG) {
-                layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth);
+                layoutDebugInfo(mCenterPositionInStrip, placerView, availableStripWidth);
             }
             return;
         }
@@ -328,7 +329,7 @@
                 x += divider.getMeasuredWidth();
             }
 
-            final int width = getSuggestionWidth(positionInStrip, stripWidth);
+            final int width = getSuggestionWidth(positionInStrip, availableStripWidth);
             final TextView wordView = layoutWord(positionInStrip, width);
             stripView.addView(wordView);
             setLayoutWeight(wordView, getSuggestionWeight(positionInStrip),
@@ -373,9 +374,9 @@
         // Disable this suggestion if the suggestion is null or empty.
         wordView.setEnabled(!TextUtils.isEmpty(word));
         final CharSequence text = getEllipsizedText(word, width, wordView.getPaint());
-        final float scaleX = wordView.getTextScaleX();
+        final float scaleX = getTextScaleX(word, width, wordView.getPaint());
         wordView.setText(text); // TextView.setText() resets text scale x to 1.0.
-        wordView.setTextScaleX(scaleX);
+        wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE));
         return wordView;
     }
 
@@ -545,8 +546,24 @@
 
         // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
         // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
-        final CharSequence ellipsized = TextUtils.ellipsize(
-                text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
+        final float upscaledWidth = maxWidth / MIN_TEXT_XSCALE;
+        CharSequence ellipsized = TextUtils.ellipsize(
+                text, paint, upscaledWidth, TextUtils.TruncateAt.MIDDLE);
+        // For an unknown reason, ellipsized seems to return a text that does indeed fit inside the
+        // passed width according to paint.measureText, but not according to paint.getTextWidths.
+        // But when rendered, the text seems to actually take up as many pixels as returned by
+        // paint.getTextWidths, hence problem.
+        // To save this case, we compare the measured size of the new text, and if it's too much,
+        // try it again removing the difference. This may still give a text too long by one or
+        // two pixels so we take an additional 2 pixels cushion and call it a day.
+        // TODO: figure out why getTextWidths and measureText don't agree with each other, and
+        // remove the following code.
+        final float ellipsizedTextWidth = getTextWidth(ellipsized, paint);
+        if (upscaledWidth <= ellipsizedTextWidth) {
+            ellipsized = TextUtils.ellipsize(
+                    text, paint, upscaledWidth - (ellipsizedTextWidth - upscaledWidth) - 2,
+                    TextUtils.TruncateAt.MIDDLE);
+        }
         paint.setTextScaleX(MIN_TEXT_XSCALE);
         return ellipsized;
     }
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index 6bc8b9d..8ad8689 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -183,7 +183,7 @@
         final String[] STRINGS_TO_TYPE =
                 new String[] { "this   ", "a+  ", "\u1F607  ", "..  ", ")  ", "(  ", "%  " };
         final String[] EXPECTED_RESULTS =
-                new String[] { "this.  ", "a+. ", "\u1F607. ", "..  ", "). ", "(  ", "%  " };
+                new String[] { "this.  ", "a+. ", "\u1F607. ", "..  ", "). ", "(  ", "%. " };
         for (int i = 0; i < STRINGS_TO_TYPE.length; ++i) {
             mEditText.setText("");
             type(STRINGS_TO_TYPE[i]);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 0189b33..32c07e1 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -80,6 +80,9 @@
             new FormatSpec.FormatOptions(4, false /* supportsDynamicUpdate */);
     private static final FormatSpec.FormatOptions VERSION4_WITH_DYNAMIC_UPDATE =
             new FormatSpec.FormatOptions(4, true /* supportsDynamicUpdate */);
+    private static final FormatSpec.FormatOptions VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP =
+            new FormatSpec.FormatOptions(4, true /* supportsDynamicUpdate */,
+                    true /* hasTimestamp */);
 
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
 
@@ -363,6 +366,7 @@
         runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION3_WITH_DYNAMIC_UPDATE);
         runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE);
+        runReadAndWriteTests(results, USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -377,6 +381,7 @@
         runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION3_WITH_DYNAMIC_UPDATE);
         runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE);
+        runReadAndWriteTests(results, USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -508,6 +513,8 @@
         runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION3_WITH_DYNAMIC_UPDATE);
         runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_BUFFER,
+                VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -522,6 +529,8 @@
         runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION3_WITH_DYNAMIC_UPDATE);
         runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE);
+        runReadUnigramsAndBigramsTests(results, USE_BYTE_ARRAY,
+                VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -634,12 +643,14 @@
         runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION3_WITH_DYNAMIC_UPDATE);
         runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION2);
         runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION3_WITHOUT_DYNAMIC_UPDATE);
         runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION3_WITH_DYNAMIC_UPDATE);
         runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION4_WITHOUT_DYNAMIC_UPDATE);
         runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE_AND_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);