diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index da1232f5e59dea0c6a16fb85f3315d641ca5c068..4db0a906e2f3d25f921119599fc8ceb1bf6fa1b4 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -161,6 +161,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
             mPositionalInfoForUserDictPendingAddition = null;
     private final WordComposer mWordComposer = new WordComposer();
     private final RichInputConnection mConnection = new RichInputConnection(this);
+    private RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(-1, -1, "",
+            Locale.getDefault(), ""); // Dummy object that will match no real recapitalize
 
     // Keep track of the last selection range to decide if we need to show word alternatives
     private static final int NOT_A_CURSOR_POSITION = -1;
@@ -1387,8 +1389,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
             LatinImeLogger.logOnDelete(x, y);
             break;
         case Constants.CODE_SHIFT:
+            // Note: calling back to the keyboard on Shift key is handled in onPressKey()
+            // and onReleaseKey().
+            handleRecapitalize();
+            break;
         case Constants.CODE_SWITCH_ALPHA_SYMBOL:
-            // Shift and symbol key is handled in onPressKey() and onReleaseKey().
+            // Note: calling back to the keyboard on symbol key is handled in onPressKey()
+            // and onReleaseKey().
             break;
         case Constants.CODE_SETTINGS:
             onSettingsKeyPressed();
@@ -1943,6 +1950,37 @@ public final class LatinIME extends InputMethodService implements KeyboardAction
         }
     }
 
+    private void handleRecapitalize() {
+        if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
+        // If we have a recapitalize in progress, use it; otherwise, create a new one.
+        if (null == mRecapitalizeStatus
+                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+            mRecapitalizeStatus =
+                    new RecapitalizeStatus(mLastSelectionStart, mLastSelectionEnd,
+                    mConnection.getSelectedText(0 /* flags, 0 for no styles */).toString(),
+                    mSettings.getCurrentLocale(), mSettings.getWordSeparators());
+            // We trim leading and trailing whitespace.
+            mRecapitalizeStatus.trim();
+            // Trimming the object may have changed the length of the string, and we need to
+            // reposition the selection handles accordingly. As this result in an IPC call,
+            // only do it if it's actually necessary, in other words if the recapitalize status
+            // is not set at the same place as before.
+            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+                mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+            }
+        }
+        mRecapitalizeStatus.rotate();
+        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+        mConnection.deleteSurroundingText(numCharsDeleted, 0);
+        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
+        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+    }
+
     // Returns true if we did an autocorrection, false otherwise.
     private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
diff --git a/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..9edd3a160c7daf6bd9eb461b4f1ef170e1860020
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.StringUtils;
+
+import java.util.Locale;
+
+/**
+ * The status of the current recapitalize process.
+ */
+public class RecapitalizeStatus {
+    public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0;
+    public static final int CAPS_MODE_ALL_LOWER = 1;
+    public static final int CAPS_MODE_FIRST_WORD_UPPER = 2;
+    public static final int CAPS_MODE_ALL_UPPER = 3;
+    // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant.
+    public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER;
+
+    private static final int[] ROTATION_STYLE = {
+        CAPS_MODE_ORIGINAL_MIXED_CASE,
+        CAPS_MODE_ALL_LOWER,
+        CAPS_MODE_FIRST_WORD_UPPER,
+        CAPS_MODE_ALL_UPPER
+    };
+    private static final int getStringMode(final String string, final String separators) {
+        if (StringUtils.isIdenticalAfterUpcase(string)) {
+            return CAPS_MODE_ALL_UPPER;
+        } else if (StringUtils.isIdenticalAfterDowncase(string)) {
+            return CAPS_MODE_ALL_LOWER;
+        } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) {
+            return CAPS_MODE_FIRST_WORD_UPPER;
+        } else {
+            return CAPS_MODE_ORIGINAL_MIXED_CASE;
+        }
+    }
+
+    /**
+     * We store the location of the cursor and the string that was there before the undoable
+     * action was done, and the location of the cursor and the string that was there after.
+     */
+    private int mCursorStartBefore;
+    private int mCursorEndBefore;
+    private String mStringBefore;
+    private int mCursorStartAfter;
+    private int mCursorEndAfter;
+    private int mRotationStyleCurrentIndex;
+    private final boolean mSkipOriginalMixedCaseMode;
+    private final Locale mLocale;
+    private final String mSeparators;
+    private String mStringAfter;
+
+    public RecapitalizeStatus(final int cursorStart, final int cursorEnd, final String string,
+            final Locale locale, final String separators) {
+        mCursorStartBefore = cursorStart;
+        mCursorEndBefore = cursorEnd;
+        mStringBefore = string;
+        mCursorStartAfter = cursorStart;
+        mCursorEndAfter = cursorEnd;
+        mStringAfter = string;
+        final int initialMode = getStringMode(mStringBefore, separators);
+        mLocale = locale;
+        mSeparators = separators;
+        if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
+            mRotationStyleCurrentIndex = 0;
+            mSkipOriginalMixedCaseMode = false;
+        } else {
+            // Find the current mode in the array.
+            int currentMode;
+            for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) {
+                if (ROTATION_STYLE[currentMode] == initialMode) {
+                    break;
+                }
+            }
+            mRotationStyleCurrentIndex = currentMode;
+            mSkipOriginalMixedCaseMode = true;
+        }
+    }
+
+    public boolean isSetAt(final int cursorStart, final int cursorEnd) {
+        return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter;
+    }
+
+    /**
+     * Rotate through the different possible capitalization modes.
+     */
+    public void rotate() {
+        final String oldResult = mStringAfter;
+        int count = 0; // Protection against infinite loop.
+        do {
+            mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+            if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex]
+                    && mSkipOriginalMixedCaseMode) {
+                mRotationStyleCurrentIndex =
+                        (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length;
+            }
+            ++count;
+            switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) {
+                case CAPS_MODE_ORIGINAL_MIXED_CASE:
+                    mStringAfter = mStringBefore;
+                    break;
+                case CAPS_MODE_ALL_LOWER:
+                    mStringAfter = mStringBefore.toLowerCase(mLocale);
+                    break;
+                case CAPS_MODE_FIRST_WORD_UPPER:
+                    mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators,
+                            mLocale);
+                    break;
+                case CAPS_MODE_ALL_UPPER:
+                    mStringAfter = mStringBefore.toUpperCase(mLocale);
+                    break;
+                default:
+                    mStringAfter = mStringBefore;
+            }
+        } while (mStringAfter.equals(oldResult) && count < 5);
+        mCursorEndAfter = mCursorStartAfter + mStringAfter.length();
+    }
+
+    /**
+     * Remove leading/trailing whitespace from the considered string.
+     */
+    public void trim() {
+        final int len = mStringBefore.length();
+        int nonWhitespaceStart = 0;
+        for (; nonWhitespaceStart < len;
+                nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) {
+            final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart);
+            if (!Character.isWhitespace(codePoint)) break;
+        }
+        int nonWhitespaceEnd = len;
+        for (; nonWhitespaceEnd > 0;
+                nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) {
+            final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
+            if (!Character.isWhitespace(codePoint)) break;
+        }
+        if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) {
+            mCursorEndBefore = mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
+            mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
+            mStringAfter = mStringBefore =
+                    mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd);
+        }
+    }
+
+    public String getRecapitalizedString() {
+        return mStringAfter;
+    }
+
+    public int getNewCursorStart() {
+        return mCursorStartAfter;
+    }
+
+    public int getNewCursorEnd() {
+        return mCursorEndAfter;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index b74ea593d52ab5b94eec60f9f914bcc9941025c1..e178466182c32f6a05b08c73874aadaa80928743 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -183,6 +183,11 @@ public final class RichInputConnection {
         }
     }
 
+    public CharSequence getSelectedText(final int flags) {
+        if (null == mIC) return null;
+        return mIC.getSelectedText(flags);
+    }
+
     /**
      * Gets the caps modes we should be in after this specific string.
      *
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 318d2b23fcd22d12b3d197d00aed64992cc96755..72e08700ae636eee07e089cc4aca6c64f1c91b3e 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -138,6 +138,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
         return mSettingsValues.mWordSeparators;
     }
 
+    public Locale getCurrentLocale() {
+        return mCurrentLocale;
+    }
+
     // Accessed from the settings interface, hence public
     public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
             final Resources res) {
diff --git a/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java b/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..4dfae4c94d321f64c30c01071db864fdd10ea9bf
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/RecapitalizeStatusTests.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Locale;
+
+@SmallTest
+public class RecapitalizeStatusTests extends AndroidTestCase {
+    public void testTrim() {
+        RecapitalizeStatus status = new RecapitalizeStatus(30, 40, "abcdefghij",
+                Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+
+        status = new RecapitalizeStatus(30, 44, "    abcdefghij",
+                Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(34, status.getNewCursorStart());
+        assertEquals(44, status.getNewCursorEnd());
+
+        status = new RecapitalizeStatus(30, 40, "abcdefgh  ",
+                Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefgh", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(38, status.getNewCursorEnd());
+
+        status = new RecapitalizeStatus(30, 45, "   abcdefghij  ",
+                Locale.ENGLISH, " ");
+        status.trim();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(33, status.getNewCursorStart());
+        assertEquals(43, status.getNewCursorEnd());
+    }
+
+    public void testRotate() {
+        RecapitalizeStatus status = new RecapitalizeStatus(29, 40, "abcd efghij",
+                Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+
+        status = new RecapitalizeStatus(29, 40, "Abcd Efghij",
+                Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+
+        status = new RecapitalizeStatus(29, 40, "ABCD EFGHIJ",
+                Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+
+        status = new RecapitalizeStatus(29, 39, "AbCDefghij",
+                Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(39, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcdefghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCDEFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("AbCDefghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcdefghij", status.getRecapitalizedString());
+
+        status = new RecapitalizeStatus(29, 40, "Abcd efghij",
+                Locale.ENGLISH, " ");
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+        assertEquals(29, status.getNewCursorStart());
+        assertEquals(40, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Abcd Efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("Abcd efghij", status.getRecapitalizedString());
+        status.rotate();
+        assertEquals("abcd efghij", status.getRecapitalizedString());
+
+        status = new RecapitalizeStatus(30, 34, "grüß", Locale.GERMAN, " ");
+        status.rotate();
+        assertEquals("Grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("GRÃœSS", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Grüß", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(34, status.getNewCursorEnd());
+
+
+        status = new RecapitalizeStatus(30, 33, "Å“uf", Locale.FRENCH, " ");
+        status.rotate();
+        assertEquals("Å’uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å’UF", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å“uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å’uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+
+        status = new RecapitalizeStatus(30, 33, "Å“Uf", Locale.FRENCH, " ");
+        status.rotate();
+        assertEquals("Å“uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å’uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å’UF", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å“Uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("Å“uf", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(33, status.getNewCursorEnd());
+
+        status = new RecapitalizeStatus(30, 35, "école", Locale.FRENCH, " ");
+        status.rotate();
+        assertEquals("École", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("ÉCOLE", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("école", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+        status.rotate();
+        assertEquals("École", status.getRecapitalizedString());
+        assertEquals(30, status.getNewCursorStart());
+        assertEquals(35, status.getNewCursorEnd());
+    }
+}