diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
index 702ed2075263335b2d957778dcac0b932f249327..546fa8140d55077f6575ea28ba738234c793f70d 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
@@ -505,10 +505,7 @@ public final class EmojiKeyboardView extends LinearLayout implements OnTabChange
 
     @Override
     public void onKeyClick(final Key key) {
-        // TODO: Save emoticons to recents
-        if (mEmojiCategory.getCurrentCategoryId() != CATEGORY_ID_EMOTICONS) {
-            mEmojiKeyboardAdapter.addRecentKey(key);
-        }
+        mEmojiKeyboardAdapter.addRecentKey(key);
         mEmojiCategory.saveLastTypedCategoryPage();
         final int code = key.getCode();
         if (code == Constants.CODE_OUTPUT_TEXT) {
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 23f037fbdb749de7a535fb123d8fcfd4d3aa2605..bc1383affe92d52c7fb9d326c231c5d10547ed9f 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -155,6 +155,15 @@ public class Keyboard {
         return mKeys;
     }
 
+    public Key getKeyFromOutputText(final String outputText) {
+        for (final Key key : getKeys()) {
+            if (outputText.equals(key.getOutputText())) {
+                return key;
+            }
+        }
+        return null;
+    }
+
     public Key getKey(final int code) {
         if (code == Constants.CODE_UNSPECIFIED) {
             return null;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
index f203eb7d79c0d41ca7111dc31c77594a56070ed0..2976e2323631fc2b21802fbbb634e61e2cd3b92b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -18,25 +18,27 @@ package com.android.inputmethod.keyboard.internal;
 
 import android.content.SharedPreferences;
 import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.inputmethod.keyboard.EmojiKeyboardView;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 /**
  * This is a Keyboard class where you can add keys dynamically shown in a grid layout
  */
 public class DynamicGridKeyboard extends Keyboard {
+    private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
     private static final int TEMPLATE_KEY_CODE_0 = 0x30;
     private static final int TEMPLATE_KEY_CODE_1 = 0x31;
-    // Recent codes are saved as an integer array, so we use comma as a separater.
-    private static final String RECENT_KEY_SEPARATOR = Constants.STRING_COMMA;
 
     private final SharedPreferences mPrefs;
     private final int mLeftPadding;
@@ -84,6 +86,9 @@ public class DynamicGridKeyboard extends Keyboard {
     }
 
     private void addKey(final Key usedKey, final boolean addFirst) {
+        if (usedKey == null) {
+            return;
+        }
         synchronized (mGridKeys) {
             mCachedGridKeys = null;
             final GridKey key = new GridKey(usedKey);
@@ -109,28 +114,45 @@ public class DynamicGridKeyboard extends Keyboard {
     }
 
     private void saveRecentKeys() {
-        final StringBuilder sb = new StringBuilder();
+        final ArrayList<Object> keys = CollectionUtils.newArrayList();
         for (final Key key : mGridKeys) {
-            sb.append(key.getCode()).append(RECENT_KEY_SEPARATOR);
+            if (key.getOutputText() != null) {
+                keys.add(key.getOutputText());
+            } else {
+                keys.add(key.getCode());
+            }
         }
-        Settings.writeEmojiRecentKeys(mPrefs, sb.toString());
+        final String jsonStr = StringUtils.listToJsonStr(keys);
+        Settings.writeEmojiRecentKeys(mPrefs, jsonStr);
     }
 
-    public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) {
-        final String str = Settings.readEmojiRecentKeys(mPrefs);
-        for (String s : str.split(RECENT_KEY_SEPARATOR)) {
-            if (TextUtils.isEmpty(s)) {
-                continue;
-            }
-            final int code = Integer.valueOf(s);
-            for (DynamicGridKeyboard kbd : keyboards) {
+    private static Key getKey(final Collection<DynamicGridKeyboard> keyboards, final Object o) {
+        for (final DynamicGridKeyboard kbd : keyboards) {
+            if (o instanceof Integer) {
+                final int code = (Integer) o;
                 final Key key = kbd.getKey(code);
                 if (key != null) {
-                    addKeyLast(key);
-                    break;
+                    return key;
+                }
+            } else if (o instanceof String) {
+                final String outputText = (String) o;
+                final Key key = kbd.getKeyFromOutputText(outputText);
+                if (key != null) {
+                    return key;
                 }
+            } else {
+                Log.w(TAG, "Invalid object: " + o);
             }
         }
+        return null;
+    }
+
+    public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) {
+        final String str = Settings.readEmojiRecentKeys(mPrefs);
+        final List<Object> keys = StringUtils.jsonStrToList(str);
+        for (final Object o : keys) {
+            addKeyLast(getKey(keyboards, o));
+        }
     }
 
     private int getKeyX(final int index) {
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 029ba02ed0bbab39b10aba64565cbdb4232e2658..04ca60dbbeff32147f911ba948860cef2a1b6f9c 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -221,7 +221,6 @@ public final class Constants {
     }
 
     public static final int MAX_INT_BIT_COUNT = 32;
-    public static final String STRING_COMMA = ",";
 
     private Constants() {
         // This utility class is not publicly instantiable.
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index be418409303e800c47513c5e980d26714e2b3960..121aecf0f98c556651e197732861bb0dc68ddb85 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -16,16 +16,25 @@
 
 package com.android.inputmethod.latin.utils;
 
-import android.text.TextUtils;
-
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.settings.SettingsValues;
 
+import android.text.TextUtils;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
 
 public final class StringUtils {
+    private static final String TAG = StringUtils.class.getSimpleName();
     public static final int CAPITALIZE_NONE = 0;  // No caps, or mixed case
     public static final int CAPITALIZE_FIRST = 1; // First only
     public static final int CAPITALIZE_ALL = 2;   // All caps
@@ -390,4 +399,67 @@ public final class StringUtils {
         }
         return bytes;
     }
+
+    public static List<Object> jsonStrToList(String s) {
+        final ArrayList<Object> retval = CollectionUtils.newArrayList();
+        final JsonReader reader = new JsonReader(new StringReader(s));
+        try {
+            reader.beginArray();
+            while(reader.hasNext()) {
+                reader.beginObject();
+                while (reader.hasNext()) {
+                    final String name = reader.nextName();
+                    if (name.equals(Integer.class.getSimpleName())) {
+                        retval.add(reader.nextInt());
+                    } else if (name.equals(String.class.getSimpleName())) {
+                        retval.add(reader.nextString());
+                    } else {
+                        Log.w(TAG, "Invalid name: " + name);
+                        reader.skipValue();
+                    }
+                }
+                reader.endObject();
+            }
+            reader.endArray();
+            return retval;
+        } catch (IOException e) {
+        } finally {
+            try {
+                reader.close();
+            } catch (IOException e) {
+            }
+        }
+        return Collections.<Object>emptyList();
+    }
+
+    public static String listToJsonStr(List<Object> list) {
+        if (list == null || list.isEmpty()) {
+            return "";
+        }
+        final StringWriter sw = new StringWriter();
+        final JsonWriter writer = new JsonWriter(sw);
+        try {
+            writer.beginArray();
+            for (final Object o : list) {
+                writer.beginObject();
+                if (o instanceof Integer) {
+                    writer.name(Integer.class.getSimpleName()).value((Integer)o);
+                } else if (o instanceof String) {
+                    writer.name(String.class.getSimpleName()).value((String)o);
+                }
+                writer.endObject();
+            }
+            writer.endArray();
+            return sw.toString();
+        } catch (IOException e) {
+        } finally {
+            try {
+                if (writer != null) {
+                    writer.close();
+                }
+            } catch (IOException e) {
+            }
+        }
+        return "";
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
index c6fa943fe4b1f07d30c2ec47c74b66801cd48d4a..4e396a1cf68ded88ec9a7f1bef3047bfb894f0e2 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringUtilsTests.java
@@ -21,6 +21,8 @@ import com.android.inputmethod.latin.settings.SettingsValues;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.Locale;
 
 @SmallTest
@@ -268,4 +270,14 @@ public class StringUtilsTests extends AndroidTestCase {
         final String bytesStr2 = StringUtils.byteArrayToHexString(bytes2);
         assertTrue(bytesStr.equals(bytesStr2));
     }
+
+    public void testJsonStringUtils() {
+        final Object[] objs = new Object[] { 1, "aaa", "bbb", 3 };
+        final List<Object> objArray = Arrays.asList(objs);
+        final String str = StringUtils.listToJsonStr(objArray);
+        final List<Object> newObjArray = StringUtils.jsonStrToList(str);
+        for (int i = 0; i < objs.length; ++i) {
+            assertEquals(objs[i], newObjArray.get(i));
+        }
+    }
 }