Revise caps lock sequences

This change also fixes that the popup preview of ALT on symbol
keyboard is not showing.

Bug: 3122877
Bug: 3127255
Change-Id: I978cb30a0d05298274d8ab6541b91323a0fef211
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index a1577e4..410d34b 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -28,6 +28,7 @@
     <integer name="config_delay_before_key_repeat_start">400</integer>
     <integer name="config_key_repeat_interval">50</integer>
     <integer name="config_long_press_key_timeout">400</integer>
+    <integer name="config_long_press_shift_key_timeout">1000</integer>
     <integer name="config_multi_tap_key_timeout">800</integer>
     <string-array name="auto_complete_threshold_values">
         <!-- Off, When auto completing setting is Off, this value is not used. -->
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index bb29e63..3250cdd 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -142,9 +142,10 @@
     // Key events coming any faster than this are long-presses.
     private static final int QUICK_PRESS = 200;
 
-    static final int KEYCODE_ENTER = '\n';
-    static final int KEYCODE_SPACE = ' ';
-    static final int KEYCODE_PERIOD = '.';
+    public static final int KEYCODE_ENTER = '\n';
+    public static final int KEYCODE_TAB = '\t';
+    public static final int KEYCODE_SPACE = ' ';
+    public static final int KEYCODE_PERIOD = '.';
 
     // Contextual menu positions
     private static final int POS_METHOD = 0;
@@ -1190,66 +1191,68 @@
 
     public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
         long when = SystemClock.uptimeMillis();
-        if (primaryCode != BaseKeyboard.KEYCODE_DELETE ||
-                when > mLastKeyTime + QUICK_PRESS) {
+        if (primaryCode != BaseKeyboard.KEYCODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
             mDeleteCount = 0;
         }
         mLastKeyTime = when;
         final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
         switch (primaryCode) {
-            case BaseKeyboard.KEYCODE_DELETE:
-                handleBackspace();
-                mDeleteCount++;
-                LatinImeLogger.logOnDelete();
-                break;
-            case BaseKeyboard.KEYCODE_SHIFT:
-                // Shift key is handled in onPress() when device has distinct multi-touch panel.
-                if (!distinctMultiTouch)
-                    handleShift();
-                break;
-            case BaseKeyboard.KEYCODE_MODE_CHANGE:
-                // Symbol key is handled in onPress() when device has distinct multi-touch panel.
-                if (!distinctMultiTouch)
-                    changeKeyboardMode();
-                break;
-            case BaseKeyboard.KEYCODE_CANCEL:
-                if (!isShowingOptionDialog()) {
-                    handleClose();
-                }
-                break;
-            case LatinKeyboardView.KEYCODE_OPTIONS:
-                onOptionKeyPressed();
-                break;
-            case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS:
-                onOptionKeyLongPressed();
-                break;
-            case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
-                toggleLanguage(false, true);
-                break;
-            case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
-                toggleLanguage(false, false);
-                break;
-            case LatinKeyboardView.KEYCODE_VOICE:
-                if (VOICE_INSTALLED) {
-                    startListening(false /* was a button press, was not a swipe */);
-                }
-                break;
-            case 9 /*Tab*/:
-                sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
-                break;
-            default:
-                if (primaryCode != KEYCODE_ENTER) {
-                    mJustAddedAutoSpace = false;
-                }
-                RingCharBuffer.getInstance().push((char)primaryCode, x, y);
-                LatinImeLogger.logOnInputChar();
-                if (isWordSeparator(primaryCode)) {
-                    handleSeparator(primaryCode);
-                } else {
-                    handleCharacter(primaryCode, keyCodes);
-                }
-                // Cancel the just reverted state
-                mJustRevertedSeparator = null;
+        case BaseKeyboard.KEYCODE_DELETE:
+            handleBackspace();
+            mDeleteCount++;
+            LatinImeLogger.logOnDelete();
+            break;
+        case BaseKeyboard.KEYCODE_SHIFT:
+            // Shift key is handled in onPress() when device has distinct multi-touch panel.
+            if (!distinctMultiTouch)
+                handleShift();
+            break;
+        case BaseKeyboard.KEYCODE_MODE_CHANGE:
+            // Symbol key is handled in onPress() when device has distinct multi-touch panel.
+            if (!distinctMultiTouch)
+                changeKeyboardMode();
+            break;
+        case BaseKeyboard.KEYCODE_CANCEL:
+            if (!isShowingOptionDialog()) {
+                handleClose();
+            }
+            break;
+        case LatinKeyboardView.KEYCODE_OPTIONS:
+            onOptionKeyPressed();
+            break;
+        case LatinKeyboardView.KEYCODE_OPTIONS_LONGPRESS:
+            onOptionKeyLongPressed();
+            break;
+        case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
+            toggleLanguage(false, true);
+            break;
+        case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
+            toggleLanguage(false, false);
+            break;
+        case LatinKeyboardView.KEYCODE_CAPSLOCK:
+            handleCapsLock();
+            break;
+        case LatinKeyboardView.KEYCODE_VOICE:
+            if (VOICE_INSTALLED) {
+                startListening(false /* was a button press, was not a swipe */);
+            }
+            break;
+        case KEYCODE_TAB:
+            sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
+            break;
+        default:
+            if (primaryCode != KEYCODE_ENTER) {
+                mJustAddedAutoSpace = false;
+            }
+            RingCharBuffer.getInstance().push((char)primaryCode, x, y);
+            LatinImeLogger.logOnInputChar();
+            if (isWordSeparator(primaryCode)) {
+                handleSeparator(primaryCode);
+            } else {
+                handleCharacter(primaryCode, keyCodes);
+            }
+            // Cancel the just reverted state
+            mJustRevertedSeparator = null;
         }
         if (mKeyboardSwitcher.onKey(primaryCode)) {
             changeKeyboardMode();
@@ -1363,24 +1366,37 @@
     private void handleShiftInternal(boolean forceNormal) {
         mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
         KeyboardSwitcher switcher = mKeyboardSwitcher;
-        LatinKeyboardView inputView = switcher.getInputView();
         if (switcher.isAlphabetMode()) {
+            LatinKeyboardView inputView = switcher.getInputView();
             if (mCapsLock || forceNormal) {
                 mCapsLock = false;
                 switcher.setShifted(false);
             } else if (inputView != null) {
-                if (inputView.isShifted()) {
-                    mCapsLock = true;
-                    switcher.setShiftLocked(true);
-                } else {
-                    switcher.setShifted(true);
-                }
+                switcher.setShifted(!inputView.isShifted());
             }
         } else {
             switcher.toggleShift();
         }
     }
 
+    private void handleCapsLock() {
+        mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE);
+        KeyboardSwitcher switcher = mKeyboardSwitcher;
+        if (switcher.isAlphabetMode()) {
+            if (mCapsLock) {
+                mCapsLock = false;
+                // LatinKeyboard.setShifted(false) also disable shift locked state.
+                // Note: Caps lock LED is off when Key.on is false.
+                switcher.setShifted(false);
+            } else {
+                mCapsLock = true;
+                // LatinKeyboard.setShiftLocked(true) enable shift state too.
+                // Note: Caps lock LED is on when Key.on is true.
+                switcher.setShiftLocked(true);
+            }
+        }
+    }
+
     private void abortCorrection(boolean force) {
         if (force || TextEntryState.isCorrecting()) {
             getCurrentInputConnection().finishComposingText();
@@ -2291,7 +2307,9 @@
         final boolean distinctMultiTouch = mKeyboardSwitcher.hasDistinctMultitouch();
         if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_SHIFT) {
             mShiftKeyState.onPress();
-            handleShift();
+            // Not in caps lock mode, shift key is in effect on pressed.
+            if (mKeyboardSwitcher.isAlphabetMode() && !mCapsLock)
+                handleShift();
         } else if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_MODE_CHANGE) {
             mSymbolKeyState.onPress();
             changeKeyboardMode();
@@ -2309,6 +2327,9 @@
         if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_SHIFT) {
             if (mShiftKeyState.isMomentary())
                 resetShift();
+            // In caps lock mode, shift key is in effect on released.
+            if (mKeyboardSwitcher.isAlphabetMode() && mCapsLock)
+                handleShift();
             mShiftKeyState.onRelease();
         } else if (distinctMultiTouch && primaryCode == BaseKeyboard.KEYCODE_MODE_CHANGE) {
             if (mSymbolKeyState.isMomentary())
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
index 37ce1e8..6494bdf 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java
@@ -236,6 +236,7 @@
     }
 
     public void setShiftLocked(boolean shiftLocked) {
+        // TODO: cleanup this method with BaseKeyboard.Key
         for (final Key key : getShiftKeys()) {
             key.on = shiftLocked;
             key.icon = mShiftLockIcon;
@@ -249,6 +250,7 @@
 
     @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()) {
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
index 4e264e8..6b46ab8 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
@@ -261,6 +261,7 @@
         private static final int MSG_DISMISS_PREVIEW = 2;
         private static final int MSG_REPEAT_KEY = 3;
         private static final int MSG_LONGPRESS_KEY = 4;
+        private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
 
         private boolean mInKeyRepeat;
 
@@ -284,6 +285,11 @@
                     openPopupIfRequired(msg.arg1, tracker);
                     break;
                 }
+                case MSG_LONGPRESS_SHIFT_KEY: {
+                    final PointerTracker tracker = (PointerTracker)msg.obj;
+                    onLongPressShiftKey(tracker);
+                    break;
+                }
             }
         }
 
@@ -335,9 +341,20 @@
             removeMessages(MSG_LONGPRESS_KEY);
         }
 
+        public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
+            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
+            sendMessageDelayed(
+                    obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
+        }
+
+        public void cancelLongPressShiftTimer() {
+            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
+        }
+
         public void cancelKeyTimers() {
             cancelKeyRepeatTimer();
             cancelLongPressTimer();
+            cancelLongPressShiftTimer();
         }
 
         public void cancelAllMessages() {
@@ -869,7 +886,6 @@
                 int drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2;
                 int drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2;
                 drawIcon(canvas, key.icon, drawableX, drawableY, drawableWidth, drawableHeight);
-
             }
             if (key.hintIcon != null && drawHintIcon) {
                 int drawableWidth = key.width;
@@ -924,7 +940,7 @@
     // TODO: clean up this method.
     private void dismissKeyPreview() {
         for (PointerTracker tracker : mPointerTrackers)
-            tracker.updateKey(NOT_A_KEY);
+            tracker.releaseKey();
         showPreview(NOT_A_KEY, null);
     }
 
@@ -959,11 +975,8 @@
         // WindowManager.BadTokenException.
         if (key == null || !mInForeground)
             return;
-        if (key.icon != null) {
-            mPreviewText.setCompoundDrawables(null, null, null,
-                    key.iconPreview != null ? key.iconPreview : key.icon);
-            mPreviewText.setText(null);
-        } else {
+        // What we show as preview should match what we show on key top in onBufferDraw(). 
+        if (key.label != null) {
             // TODO Should take care of temporaryShiftLabel here.
             mPreviewText.setCompoundDrawables(null, null, null, null);
             mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
@@ -974,6 +987,10 @@
                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
                 mPreviewText.setTypeface(mKeyTextStyle);
             }
+        } else {
+            mPreviewText.setCompoundDrawables(null, null, null,
+                    key.iconPreview != null ? key.iconPreview : key.icon);
+            mPreviewText.setText(null);
         }
         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
@@ -1086,6 +1103,12 @@
         return result;
     }
 
+    private void onLongPressShiftKey(PointerTracker tracker) {
+        tracker.setAlreadyProcessed();
+        mPointerQueue.remove(tracker);
+        mKeyboardActionListener.onKey(LatinKeyboardView.KEYCODE_CAPSLOCK, null, 0, 0);
+    }
+
     private View inflateMiniKeyboardContainer(Key popupKey) {
         int popupKeyboardId = popupKey.popupResId;
         LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
index f3d045b..3542899 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java
@@ -32,12 +32,13 @@
 
 public class LatinKeyboardView extends LatinKeyboardBaseView {
 
-    static final int KEYCODE_OPTIONS = -100;
-    static final int KEYCODE_OPTIONS_LONGPRESS = -101;
-    static final int KEYCODE_VOICE = -102;
-    static final int KEYCODE_F1 = -103;
-    static final int KEYCODE_NEXT_LANGUAGE = -104;
-    static final int KEYCODE_PREV_LANGUAGE = -105;
+    public static final int KEYCODE_OPTIONS = -100;
+    public static final int KEYCODE_OPTIONS_LONGPRESS = -101;
+    public static final int KEYCODE_VOICE = -102;
+    public static final int KEYCODE_F1 = -103;
+    public static final int KEYCODE_NEXT_LANGUAGE = -104;
+    public static final int KEYCODE_PREV_LANGUAGE = -105;
+    public static final int KEYCODE_CAPSLOCK = -106;
 
     private BaseKeyboard mPhoneKeyboard;
 
diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java
index d1cdbfe..8824db6 100644
--- a/java/src/com/android/inputmethod/latin/PointerTracker.java
+++ b/java/src/com/android/inputmethod/latin/PointerTracker.java
@@ -40,6 +40,7 @@
     // Timing constants
     private final int mDelayBeforeKeyRepeatStart;
     private final int mLongPressKeyTimeout;
+    private final int mLongPressShiftKeyTimeout;
     private final int mMultiTapKeyTimeout;
 
     // Miscellaneous constants
@@ -175,6 +176,7 @@
         mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
         mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
         mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
+        mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout);
         mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
         resetMultiTap();
     }
@@ -223,9 +225,11 @@
         return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE;
     }
 
-    public void updateKey(int keyIndex) {
-        if (mKeyAlreadyProcessed)
-            return;
+    public void releaseKey() {
+        updateKeyGraphics(NOT_A_KEY);
+    }
+
+    private void updateKeyGraphics(int keyIndex) {
         int oldKeyIndex = mPreviousKey;
         mPreviousKey = keyIndex;
         if (keyIndex != oldKeyIndex) {
@@ -287,7 +291,7 @@
             }
             startLongPressTimer(keyIndex);
         }
-        showKeyPreviewAndUpdateKey(keyIndex);
+        showKeyPreviewAndUpdateKeyGraphics(keyIndex);
     }
 
     public void onMoveEvent(int x, int y, long eventTime) {
@@ -317,12 +321,13 @@
                 mHandler.cancelLongPressTimer();
             }
         }
-        showKeyPreviewAndUpdateKey(mKeyState.getKeyIndex());
+        showKeyPreviewAndUpdateKeyGraphics(mKeyState.getKeyIndex());
     }
 
     public void onUpEvent(int x, int y, long eventTime) {
         if (DEBUG)
             debugLog("onUpEvent  :", x, y);
+        showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY);
         if (mKeyAlreadyProcessed)
             return;
         mHandler.cancelKeyTimers();
@@ -334,7 +339,6 @@
             x = mKeyState.getKeyX();
             y = mKeyState.getKeyY();
         }
-        showKeyPreviewAndUpdateKey(NOT_A_KEY);
         if (!mIsRepeatableKey) {
             detectAndSendKey(keyIndex, x, y, eventTime);
         }
@@ -348,7 +352,7 @@
             debugLog("onCancelEvt:", x, y);
         mHandler.cancelKeyTimers();
         mHandler.cancelPopupPreview();
-        showKeyPreviewAndUpdateKey(NOT_A_KEY);
+        showKeyPreviewAndUpdateKeyGraphics(NOT_A_KEY);
         int keyIndex = mKeyState.getKeyIndex();
         if (isValidKeyIndex(keyIndex))
            mProxy.invalidateKey(mKeys[keyIndex]);
@@ -409,8 +413,8 @@
         return dx * dx + dy * dy;
     }
 
-    private void showKeyPreviewAndUpdateKey(int keyIndex) {
-        updateKey(keyIndex);
+    private void showKeyPreviewAndUpdateKeyGraphics(int keyIndex) {
+        updateKeyGraphics(keyIndex);
         // The modifier key, such as shift key, should not be shown as preview when multi-touch is
         // supported. On thge other hand, if multi-touch is not supported, the modifier key should
         // be shown as preview.
@@ -423,11 +427,17 @@
 
     private void startLongPressTimer(int keyIndex) {
         Key key = getKey(keyIndex);
-        // If keyboard is in temporary upper case state and the key has temporary shift label,
-        // long press should not be started.
-        if (isTemporaryUpperCase() && key.temporaryShiftLabel != null)
-            return;
-        mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
+        if (key.codes[0] == BaseKeyboard.KEYCODE_SHIFT) {
+            mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
+        } else {
+            // If keyboard is in temporary upper case state and the key has temporary shift label,
+            // non-shift long press should not be started.  On distinct multi touch device, when
+            // pressing shift key (in temporary upper case), hint icon should not be drawn on key
+            // top. So we should disable long press for such key.
+            if (isTemporaryUpperCase() && key.temporaryShiftLabel != null)
+                return;
+            mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
+        }
     }
 
     private boolean isTemporaryUpperCase() {