Implement both automatic and manual temporary upper cases

With this change,
- Shift and Shift lock state of keyboard is maintained by
  LatinKeyboard.ShiftState.
- Shift key state is maintained by ShiftKeyState object in
  KeyboardSwitcher.
- LatinIME informs KeyboardSwitcher that shift key press, release and
  long press and KeyboardSwitcher determines which state LatinKeyboard
  and ShiftLeyState should be.

Bug: 3193390
Change-Id: I948ef26fda512eb1cb0ebddc89d322c4f4f4d670
diff --git a/java/src/com/android/inputmethod/latin/BaseKeyboard.java b/java/src/com/android/inputmethod/latin/BaseKeyboard.java
index 0f8b75d..9dbfff3 100644
--- a/java/src/com/android/inputmethod/latin/BaseKeyboard.java
+++ b/java/src/com/android/inputmethod/latin/BaseKeyboard.java
@@ -626,7 +626,7 @@
         return false;
     }
 
-    public boolean isShifted() {
+    public boolean isShiftedOrShiftLocked() {
         return mShifted;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/BaseKeyboardView.java b/java/src/com/android/inputmethod/latin/BaseKeyboardView.java
index ad0e8d4..89c9572 100644
--- a/java/src/com/android/inputmethod/latin/BaseKeyboardView.java
+++ b/java/src/com/android/inputmethod/latin/BaseKeyboardView.java
@@ -683,7 +683,7 @@
     }
 
     protected CharSequence adjustCase(CharSequence label) {
-        if (mKeyboard.isShifted() && label != null && label.length() < 3
+        if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
                 && Character.isLowerCase(label.charAt(0))) {
             label = label.toString().toUpperCase();
         }
@@ -772,8 +772,8 @@
         final int kbdPaddingTop = getPaddingTop();
         final Key[] keys = mKeys;
         final Key invalidKey = mInvalidatedKey;
-        final boolean isTemporaryUpperCase = (mKeyboard instanceof LatinKeyboard
-                && ((LatinKeyboard)mKeyboard).isTemporaryUpperCase());
+        final boolean isManualTemporaryUpperCase = (mKeyboard instanceof LatinKeyboard
+                && ((LatinKeyboard)mKeyboard).isManualTemporaryUpperCase());
 
         paint.setColor(mKeyTextColor);
         boolean drawSingleKey = false;
@@ -853,7 +853,7 @@
                 int drawableHeight = key.height;
                 int drawableX = 0;
                 int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
-                Drawable icon = (isTemporaryUpperCase
+                Drawable icon = (isManualTemporaryUpperCase
                         && key.manualTemporaryUpperCaseHintIcon != null)
                         ? key.manualTemporaryUpperCaseHintIcon : key.hintIcon;
                 drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight);
@@ -1226,7 +1226,7 @@
         // TODO: change the below line to use getLatinKeyboard() instead of getKeyboard()
         BaseKeyboard baseMiniKeyboard = mMiniKeyboard.getKeyboard();
         if (baseMiniKeyboard != null && baseMiniKeyboard.setShifted(mKeyboard == null
-                ? false : mKeyboard.isShifted())) {
+                ? false : mKeyboard.isShiftedOrShiftLocked())) {
             mMiniKeyboard.invalidateAllKeys();
         }
         // Mini keyboard needs no pop-up key preview displayed.
diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
index 3f6b6ca..15a2279 100644
--- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.preference.PreferenceManager;
 import android.util.Log;
@@ -32,6 +31,7 @@
 public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String TAG = "KeyboardSwitcher";
     private static final boolean DEBUG = false;
+    public static final boolean DEBUG_STATE = false;
 
     public static final int MODE_TEXT = 0;
     public static final int MODE_URL = 1;
@@ -76,8 +76,8 @@
     private LatinKeyboardView mInputView;
     private LatinIME mInputMethodService;
 
-    private ShiftKeyState mShiftState = new ShiftKeyState();
-    private ModifierKeyState mSymbolKeyState = new ModifierKeyState();
+    private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
+    private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
 
     private KeyboardId mSymbolsId;
     private KeyboardId mSymbolsShiftedId;
@@ -374,10 +374,10 @@
             latinKeyboard.keyReleased();
     }
 
-    public boolean isShifted() {
+    public boolean isShiftedOrShiftLocked() {
         LatinKeyboard latinKeyboard = getLatinKeyboard();
         if (latinKeyboard != null)
-            return latinKeyboard.isShifted();
+            return latinKeyboard.isShiftedOrShiftLocked();
         return false;
     }
 
@@ -388,7 +388,21 @@
         return false;
     }
 
-    private void setShifted(boolean shifted) {
+    public boolean isAutomaticTemporaryUpperCase() {
+        LatinKeyboard latinKeyboard = getLatinKeyboard();
+        if (latinKeyboard != null)
+            return latinKeyboard.isAutomaticTemporaryUpperCase();
+        return false;
+    }
+
+    public boolean isManualTemporaryUpperCase() {
+        LatinKeyboard latinKeyboard = getLatinKeyboard();
+        if (latinKeyboard != null)
+            return latinKeyboard.isManualTemporaryUpperCase();
+        return false;
+    }
+
+    private void setManualTemporaryUpperCase(boolean shifted) {
         LatinKeyboard latinKeyboard = getLatinKeyboard();
         if (latinKeyboard != null && latinKeyboard.setShifted(shifted)) {
             mInputView.invalidateAllKeys();
@@ -403,21 +417,13 @@
     }
 
     public void toggleShift() {
-        handleShiftInternal(false);
-    }
-
-    private void resetShift() {
-        handleShiftInternal(true);
-    }
-
-    private void handleShiftInternal(boolean forceNormal) {
         mInputMethodService.mHandler.cancelUpdateShiftState();
+        if (DEBUG_STATE)
+            Log.d(TAG, "toggleShift:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + mShiftKeyState);
         if (isAlphabetMode()) {
-            if (forceNormal) {
-                setShifted(false);
-            } else {
-                setShifted(!isShifted());
-            }
+            setManualTemporaryUpperCase(!isShiftedOrShiftLocked());
         } else {
             toggleShiftInSymbol();
         }
@@ -425,27 +431,50 @@
 
     public void toggleCapsLock() {
         mInputMethodService.mHandler.cancelUpdateShiftState();
+        if (DEBUG_STATE)
+            Log.d(TAG, "toggleCapsLock:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + mShiftKeyState);
         if (isAlphabetMode()) {
             if (isShiftLocked()) {
-                // setShifted(false) also disable shift locked state.
-                // Note: Caps lock LED is off when Key.on is false.
-                setShifted(false);
+                // Shift key is long pressed while caps lock state, we will toggle back to normal
+                // state. And mark as if shift key is released.
+                setShiftLocked(false);
+                mShiftKeyState.onRelease();
             } else {
-                // setShiftLocked(true) enable shift state too.
-                // Note: Caps lock LED is on when Key.on is true.
                 setShiftLocked(true);
             }
         }
     }
 
+    private void setAutomaticTemporaryUpperCase() {
+        LatinKeyboard latinKeyboard = getLatinKeyboard();
+        if (latinKeyboard != null) {
+            latinKeyboard.setAutomaticTemporaryUpperCase();
+            mInputView.invalidateAllKeys();
+        }
+    }
+
     public void updateShiftState() {
-        if (isAlphabetMode() && !mShiftState.isIgnoring()) {
-            final boolean autoCapsMode = mInputMethodService.getCurrentAutoCapsState();
-            setShifted(mShiftState.isMomentary() || isShiftLocked() || autoCapsMode);
+        if (DEBUG_STATE)
+            Log.d(TAG, "updateShiftState:"
+                    + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState()
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + mShiftKeyState);
+        if (isAlphabetMode() && !isShiftLocked() && !mShiftKeyState.isIgnoring()) {
+            if (mInputMethodService.getCurrentAutoCapsState()) {
+                setAutomaticTemporaryUpperCase();
+            } else {
+                setManualTemporaryUpperCase(mShiftKeyState.isMomentary());
+            }
         }
     }
 
     public void changeKeyboardMode() {
+        if (DEBUG_STATE)
+            Log.d(TAG, "changeKeyboardMode:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + mShiftKeyState);
         toggleKeyboardMode();
         if (isShiftLocked() && isAlphabetMode())
             setShiftLocked(true);
@@ -455,47 +484,82 @@
     public void onPressShift() {
         if (!isKeyboardAvailable())
             return;
-        if (isAlphabetMode() && isShifted()) {
-            // In alphabet mode, we don't call toggleShift() when we are already in the shifted
-            // state.
-            mShiftState.onPressOnShifted();
+        ShiftKeyState shiftKeyState = mShiftKeyState;
+        if (DEBUG_STATE)
+            Log.d(TAG, "onPressShift:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + shiftKeyState);
+        if (isAlphabetMode()) {
+            if (isShiftLocked()) {
+                // Shift key is pressed while caps lock state, we will treat this state as shifted
+                // caps lock state and mark as if shift key pressed while normal state.
+                setManualTemporaryUpperCase(true);
+                shiftKeyState.onPress();
+            } else if (isAutomaticTemporaryUpperCase()) {
+                // Shift key is pressed while automatic temporary upper case, we have to move to
+                // manual temporary upper case.
+                setManualTemporaryUpperCase(true);
+                shiftKeyState.onPressOnShifted();
+            } else if (isShiftedOrShiftLocked()) {
+                // In manual upper case state, we just record shift key has been pressing while
+                // shifted state.
+                shiftKeyState.onPressOnShifted();
+            } else {
+                // In base layout, chording or manual temporary upper case mode is started.
+                toggleShift();
+                shiftKeyState.onPress();
+            }
         } else {
-            // In alphabet mode, we call toggleShift() to go into the shifted mode only when we are
-            // not in the shifted state.
-            // This else clause also handles shift key pressing in symbol mode.
-            mShiftState.onPress();
+            // In symbol mode, just toggle symbol and symbol more keyboard.
             toggleShift();
+            shiftKeyState.onPress();
         }
     }
 
     public void onReleaseShift() {
         if (!isKeyboardAvailable())
             return;
+        ShiftKeyState shiftKeyState = mShiftKeyState;
+        if (DEBUG_STATE)
+            Log.d(TAG, "onReleaseShift:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " shiftKeyState=" + shiftKeyState);
         if (isAlphabetMode()) {
-            if (mShiftState.isMomentary()) {
-                resetShift();
-            } else if (isShifted() && mShiftState.isPressingOnShifted()) {
-                // In alphabet mode, we call toggleShift() to go into the non shifted state only
-                // when we are in the shifted state -- temporary shifted mode or caps lock mode.
+            if (shiftKeyState.isMomentary()) {
+                // After chording input while normal state.
+                toggleShift();
+            } else if (isShiftLocked() && !shiftKeyState.isIgnoring()) {
+                // Shift has been pressed without chording while caps lock state.
+                toggleCapsLock();
+            } else if (isShiftedOrShiftLocked() && shiftKeyState.isPressingOnShifted()) {
+                // Shift has been pressed without chording while shifted state.
                 toggleShift();
             }
         }
-        mShiftState.onRelease();
+        shiftKeyState.onRelease();
     }
 
     public void onPressSymbol() {
+        if (DEBUG_STATE)
+            Log.d(TAG, "onReleaseShift:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " symbolKeyState=" + mSymbolKeyState);
         changeKeyboardMode();
         mSymbolKeyState.onPress();
     }
 
     public void onReleaseSymbol() {
+        if (DEBUG_STATE)
+            Log.d(TAG, "onReleaseShift:"
+                    + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
+                    + " symbolKeyState=" + mSymbolKeyState);
         if (mSymbolKeyState.isMomentary())
             changeKeyboardMode();
         mSymbolKeyState.onRelease();
     }
 
     public void onOtherKeyPressed() {
-        mShiftState.onOtherKeyPressed();
+        mShiftKeyState.onOtherKeyPressed();
         mSymbolKeyState.onOtherKeyPressed();
     }
 
@@ -521,7 +585,7 @@
         mInputView.setKeyboard(keyboard);
     }
 
-    public void toggleKeyboardMode() {
+    private void toggleKeyboardMode() {
         loadKeyboardInternal(mMode, mImeOptions, mVoiceButtonEnabled, mVoiceButtonOnPrimary,
                 !mIsSymbols);
         if (mIsSymbols) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 295a03b..307021a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1004,7 +1004,8 @@
                     return true;
                 }
                 // Enable shift key and DPAD to do selections
-                if (mKeyboardSwitcher.isInputViewShown() && mKeyboardSwitcher.isShifted()) {
+                if (mKeyboardSwitcher.isInputViewShown()
+                        && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
                     event = new KeyEvent(event.getDownTime(), event.getEventTime(),
                             event.getAction(), event.getKeyCode(), event.getRepeatCount(),
                             event.getDeviceId(), event.getScanCode(),
@@ -1373,7 +1374,7 @@
             }
         }
         KeyboardSwitcher switcher = mKeyboardSwitcher;
-        if (switcher.isShifted()) {
+        if (switcher.isShiftedOrShiftLocked()) {
             if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
                     || keyCodes[0] > Character.MAX_CODE_POINT) {
                 return;
@@ -1392,7 +1393,8 @@
             }
         }
         if (mPredicting) {
-            if (mComposing.length() == 0 && switcher.isAlphabetMode() && switcher.isShifted()) {
+            if (mComposing.length() == 0 && switcher.isAlphabetMode()
+                    && switcher.isShiftedOrShiftLocked()) {
                 mWord.setFirstCharCapitalized(true);
             }
             mComposing.append((char) primaryCode);
@@ -1677,7 +1679,7 @@
         final List<CharSequence> nBest = new ArrayList<CharSequence>();
         KeyboardSwitcher switcher = mKeyboardSwitcher;
         boolean capitalizeFirstWord = preferCapitalization()
-                || (switcher.isAlphabetMode() && switcher.isShifted());
+                || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked());
         for (String c : mVoiceResults.candidates) {
             if (capitalizeFirstWord) {
                 c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
index 6520d08..6fcea22 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
@@ -46,9 +46,9 @@
     private static final int OPACITY_FULLY_OPAQUE = 255;
     private static final int SPACE_LED_LENGTH_PERCENT = 80;
 
-    private Drawable mShiftLockIcon;
+    private Drawable mShiftedIcon;
     private Drawable mShiftLockPreviewIcon;
-    private final HashMap<Key, Drawable> mOldShiftIcons = new HashMap<Key, Drawable>();
+    private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>();
     private Drawable mSpaceIcon;
     private Drawable mSpaceAutoCompletionIndicator;
     private Drawable mSpacePreviewIcon;
@@ -92,11 +92,7 @@
     // TODO: generalize for any keyboardId
     private boolean mIsBlackSym;
 
-    private static final int SHIFT_OFF = 0;
-    private static final int SHIFT_ON = 1;
-    private static final int SHIFT_LOCKED = 2;
-    
-    private int mShiftState = SHIFT_OFF;
+    private LatinKeyboardShiftState mShiftState = new LatinKeyboardShiftState();
 
     private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
     private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f;
@@ -117,7 +113,7 @@
         mContext = context;
         mRes = res;
         mMode = id.mMode;
-        mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
+        mShiftedIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
         mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked);
         setDefaultBounds(mShiftLockPreviewIcon);
         mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
@@ -226,61 +222,62 @@
             if (key instanceof LatinKey) {
                 ((LatinKey)key).enableShiftLock();
             }
-            mOldShiftIcons.put(key, key.icon);
+            mNormalShiftIcons.put(key, key.icon);
         }
     }
 
-    public boolean setShiftLocked(boolean shiftLocked) {
-        // TODO: cleanup this method with BaseKeyboard.Key
+    public boolean setShiftLocked(boolean newShiftLockState) {
         for (final Key key : getShiftKeys()) {
-            key.on = shiftLocked;
-            key.icon = mShiftLockIcon;
+            key.on = newShiftLockState;
+            key.icon = newShiftLockState ? mShiftedIcon : mNormalShiftIcons.get(key);
         }
-        mShiftState = shiftLocked ? SHIFT_LOCKED : SHIFT_ON;
+        mShiftState.setShiftLocked(newShiftLockState);
         return true;
     }
 
     public boolean isShiftLocked() {
-        return mShiftState == SHIFT_LOCKED;
+        return mShiftState.isShiftLocked();
     }
 
     @Override
-    public boolean setShifted(boolean shiftState) {
-        // TODO: cleanup this method with BaseKeyboard.Key.
-        boolean shiftChanged = false;
-        if (getShiftKeys().size() > 0) {
-            for (final Key key : getShiftKeys()) {
-                if (shiftState == false) {
-                    key.on = false;
-                    key.icon = mOldShiftIcons.get(key);
-                } else if (mShiftState == SHIFT_OFF) {
-                    key.icon = mShiftLockIcon;
-                }
+    public boolean setShifted(boolean newShiftState) {
+        if (getShiftKeys().size() == 0)
+            return super.setShifted(newShiftState);
+
+        for (final Key key : getShiftKeys()) {
+            if (!newShiftState && !mShiftState.isShiftLocked()) {
+                key.icon = mNormalShiftIcons.get(key);
+            } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) {
+                key.icon = mShiftedIcon;
             }
-            if (shiftState == false) {
-                shiftChanged = mShiftState != SHIFT_OFF;
-                mShiftState = SHIFT_OFF;
-            } else if (mShiftState == SHIFT_OFF) {
-                shiftChanged = mShiftState == SHIFT_OFF;
-                mShiftState = SHIFT_ON;
-            }
-            return shiftChanged;
-        } else {
-            return super.setShifted(shiftState);
         }
+        return mShiftState.setShifted(newShiftState);
     }
 
     @Override
-    public boolean isShifted() {
+    public boolean isShiftedOrShiftLocked() {
         if (getShiftKeys().size() > 0) {
-            return mShiftState != SHIFT_OFF;
+            return mShiftState.isShiftedOrShiftLocked();
         } else {
-            return super.isShifted();
+            return super.isShiftedOrShiftLocked();
         }
     }
 
-    public boolean isTemporaryUpperCase() {
-        return mIsAlphaKeyboard && isShifted() && !isShiftLocked();
+    public void setAutomaticTemporaryUpperCase() {
+        setShifted(true);
+        mShiftState.setAutomaticTemporaryUpperCase();
+    }
+
+    public boolean isAutomaticTemporaryUpperCase() {
+        return mIsAlphaKeyboard && mShiftState.isAutomaticTemporaryUpperCase();
+    }
+
+    public boolean isManualTemporaryUpperCase() {
+        return mIsAlphaKeyboard && mShiftState.isManualTemporaryUpperCase();
+    }
+
+    /* package */ LatinKeyboardShiftState getKeyboardShiftState() {
+        return mShiftState;
     }
 
     public boolean isAlphaKeyboard() {
@@ -291,12 +288,12 @@
         mIsBlackSym = isBlack;
         final Resources res = mRes;
         if (isBlack) {
-            mShiftLockIcon = res.getDrawable(R.drawable.sym_bkeyboard_shift_locked);
+            mShiftedIcon = res.getDrawable(R.drawable.sym_bkeyboard_shift_locked);
             mSpaceIcon = res.getDrawable(R.drawable.sym_bkeyboard_space);
             mMicIcon = res.getDrawable(R.drawable.sym_bkeyboard_mic);
             m123MicIcon = res.getDrawable(R.drawable.sym_bkeyboard_123_mic);
         } else {
-            mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
+            mShiftedIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked);
             mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
             mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic);
             m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic);
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java b/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java
new file mode 100644
index 0000000..74c4742
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardShiftState.java
@@ -0,0 +1,88 @@
+package com.android.inputmethod.latin;
+
+import android.util.Log;
+
+public class LatinKeyboardShiftState {
+    private static final String TAG = "LatinKeyboardShiftState";
+    private static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
+
+    private static final int NORMAL = 0;
+    private static final int MANUAL_SHIFTED = 1;
+    private static final int SHIFT_LOCKED = 2;
+    private static final int AUTO_SHIFTED = 3;
+    private static final int SHIFT_LOCK_SHIFTED = 4;
+
+    private int mState = NORMAL;
+
+    public boolean setShifted(boolean newShiftState) {
+        final int oldState = mState;
+        if (newShiftState) {
+            if (oldState == NORMAL || oldState == AUTO_SHIFTED) {
+                mState = MANUAL_SHIFTED;
+            } else if (oldState == SHIFT_LOCKED) {
+                mState = SHIFT_LOCK_SHIFTED;
+            }
+        } else {
+            if (oldState == MANUAL_SHIFTED || oldState == AUTO_SHIFTED) {
+                mState = NORMAL;
+            } else if (oldState == SHIFT_LOCK_SHIFTED) {
+                mState = SHIFT_LOCKED;
+                            }
+        }
+        if (DEBUG)
+            Log.d(TAG, "setShifted: " + toString(oldState) + " > " + this);
+        return mState != oldState;
+    }
+
+    public void setShiftLocked(boolean newShiftLockState) {
+        final int oldState = mState;
+        if (newShiftLockState) {
+            if (oldState == NORMAL || oldState == MANUAL_SHIFTED || oldState == AUTO_SHIFTED)
+                mState = SHIFT_LOCKED;
+        } else {
+            if (oldState == SHIFT_LOCKED || oldState == SHIFT_LOCK_SHIFTED)
+                mState = NORMAL;
+        }
+        if (DEBUG)
+            Log.d(TAG, "setShiftLocked: " + toString(oldState) + " > " + this);
+    }
+
+    public void setAutomaticTemporaryUpperCase() {
+        final int oldState = mState;
+        mState = AUTO_SHIFTED;
+        if (DEBUG)
+            Log.d(TAG, "setAutomaticTemporaryUpperCase: " + toString(oldState) + " > " + this);
+    }
+
+    public boolean isShiftedOrShiftLocked() {
+        return mState != NORMAL;
+    }
+
+    public boolean isShiftLocked() {
+        return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED;
+    }
+
+    public boolean isAutomaticTemporaryUpperCase() {
+        return mState == AUTO_SHIFTED;
+    }
+
+    public boolean isManualTemporaryUpperCase() {
+        return mState == MANUAL_SHIFTED || mState == SHIFT_LOCK_SHIFTED;
+    }
+
+    @Override
+    public String toString() {
+        return toString(mState);
+    }
+
+    private static String toString(int state) {
+        switch (state) {
+        case NORMAL: return "NORMAL";
+        case MANUAL_SHIFTED: return "MANUAL_SHIFTED";
+        case SHIFT_LOCKED: return "SHIFT_LOCKED";
+        case AUTO_SHIFTED: return "AUTO_SHIFTED";
+        case SHIFT_LOCK_SHIFTED: return "SHIFT_LOCK_SHIFTED";
+        default: return "UKNOWN";
+        }
+    }
+}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
index 3bcac4e..a215621 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
@@ -119,7 +119,7 @@
     protected CharSequence adjustCase(CharSequence label) {
         LatinKeyboard keyboard = getLatinKeyboard();
         if (keyboard.isAlphaKeyboard()
-                && keyboard.isShifted()
+                && keyboard.isShiftedOrShiftLocked()
                 && !TextUtils.isEmpty(label) && label.length() < 3
                 && Character.isLowerCase(label.charAt(0))) {
             label = label.toString().toUpperCase();
diff --git a/java/src/com/android/inputmethod/latin/ModifierKeyState.java b/java/src/com/android/inputmethod/latin/ModifierKeyState.java
index 8139ec5..07a5c4f 100644
--- a/java/src/com/android/inputmethod/latin/ModifierKeyState.java
+++ b/java/src/com/android/inputmethod/latin/ModifierKeyState.java
@@ -16,26 +16,60 @@
 
 package com.android.inputmethod.latin;
 
+import android.util.Log;
+
 public class ModifierKeyState {
+    protected static final String TAG = "ModifierKeyState";
+    protected static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
+
     protected static final int RELEASING = 0;
     protected static final int PRESSING = 1;
     protected static final int MOMENTARY = 2;
 
+    protected final String mName;
     protected int mState = RELEASING;
 
+    public ModifierKeyState(String name) {
+        mName = name;
+    }
+
     public void onPress() {
+        final int oldState = mState;
         mState = PRESSING;
+        if (DEBUG)
+            Log.d(TAG, mName + ".onPress: " + toString(oldState) + " > " + this);
     }
 
     public void onRelease() {
+        final int oldState = mState;
         mState = RELEASING;
+        if (DEBUG)
+            Log.d(TAG, mName + ".onRelease: " + toString(oldState) + " > " + this);
     }
 
     public void onOtherKeyPressed() {
-        mState = MOMENTARY;
+        final int oldState = mState;
+        if (mState == PRESSING)
+            mState = MOMENTARY;
+        if (DEBUG)
+            Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
     }
 
     public boolean isMomentary() {
         return mState == MOMENTARY;
     }
+
+    @Override
+    public String toString() {
+        return toString(mState);
+    }
+
+    protected static String toString(int state) {
+        switch (state) {
+        case RELEASING: return "RELEASING";
+        case PRESSING: return "PRESSING";
+        case MOMENTARY: return "MOMENTARY";
+        default: return "UNKNOWN";
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java
index e76f8c7..4d91330 100644
--- a/java/src/com/android/inputmethod/latin/PointerTracker.java
+++ b/java/src/com/android/inputmethod/latin/PointerTracker.java
@@ -423,9 +423,9 @@
         }
     }
 
-    private boolean isTemporaryUpperCase() {
+    private boolean isManualTemporaryUpperCase() {
         return mKeyboard instanceof LatinKeyboard
-                && ((LatinKeyboard)mKeyboard).isTemporaryUpperCase();
+                && ((LatinKeyboard)mKeyboard).isManualTemporaryUpperCase();
     }
 
     private void detectAndSendKey(int index, int x, int y, long eventTime) {
@@ -458,7 +458,7 @@
 
                 // If keyboard is in manual temporary upper case state and key has manual temporary
                 // shift code, alternate character code should be sent.
-                if (isTemporaryUpperCase() && key.manualTemporaryUpperCaseCode != 0) {
+                if (isManualTemporaryUpperCase() && key.manualTemporaryUpperCaseCode != 0) {
                     code = key.manualTemporaryUpperCaseCode;
                     codes[0] = code;
                 }
diff --git a/java/src/com/android/inputmethod/latin/ShiftKeyState.java b/java/src/com/android/inputmethod/latin/ShiftKeyState.java
index 5312ce2..ef13ddc 100644
--- a/java/src/com/android/inputmethod/latin/ShiftKeyState.java
+++ b/java/src/com/android/inputmethod/latin/ShiftKeyState.java
@@ -16,21 +16,33 @@
 
 package com.android.inputmethod.latin;
 
+import android.util.Log;
+
 public class ShiftKeyState extends ModifierKeyState {
     private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked
     private static final int IGNORING = 4;
 
+    public ShiftKeyState(String name) {
+        super(name);
+    }
+
     @Override
     public void onOtherKeyPressed() {
+        int oldState = mState;
         if (mState == PRESSING) {
             mState = MOMENTARY;
         } else if (mState == PRESSING_ON_SHIFTED) {
             mState = IGNORING;
         }
+        if (DEBUG)
+            Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
     }
 
     public void onPressOnShifted() {
+        int oldState = mState;
         mState = PRESSING_ON_SHIFTED;
+        if (DEBUG)
+            Log.d(TAG, mName + ".onPressOnShifted: " + toString(oldState) + " > " + this);
     }
 
     public boolean isPressingOnShifted() {
@@ -40,4 +52,17 @@
     public boolean isIgnoring() {
         return mState == IGNORING;
     }
+
+    @Override
+    public String toString() {
+        return toString(mState);
+    }
+
+    protected static String toString(int state) {
+        switch (state) {
+        case PRESSING_ON_SHIFTED: return "PRESSING_ON_SHIFTED";
+        case IGNORING: return "IGNORING";
+        default: return ModifierKeyState.toString(state);
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Tutorial.java b/java/src/com/android/inputmethod/latin/Tutorial.java
index 3563a8c..cd7636f 100644
--- a/java/src/com/android/inputmethod/latin/Tutorial.java
+++ b/java/src/com/android/inputmethod/latin/Tutorial.java
@@ -217,7 +217,7 @@
             return;
         }
         if (mBubbleIndex == 3 || mBubbleIndex == 4) {
-            mKeyboardSwitcher.toggleKeyboardMode();
+            mKeyboardSwitcher.changeKeyboardMode();
         }
         mHandler.sendMessageDelayed(
                 mHandler.obtainMessage(MSG_SHOW_BUBBLE, mBubbles.get(mBubbleIndex)), 500);