Merge "Add a layout for a black-characters based theme"
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 81ae64e..39dce9d 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -27,4 +27,5 @@
          will not go into extract (fullscreen) mode. -->
     <dimen name="max_height_for_fullscreen">2.5in</dimen>
     <dimen name="key_text_size">22sp</dimen>
+    <dimen name="key_debounce_hysteresis_distance">0.05in</dimen>
 </resources>
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
index ba7aa5d..9570da7 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
@@ -197,7 +197,6 @@
 
     private KeyDebouncer mDebouncer;
 
-    private int[] mKeyIndices = new int[12];
     private GestureDetector mGestureDetector;
     private int mPopupX;
     private int mPopupY;
@@ -273,6 +272,7 @@
         }
 
         public void popupPreview(int keyIndex, long delay) {
+            removeMessages(MSG_POPUP_PREVIEW);
             sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0), delay);
         }
 
@@ -332,12 +332,10 @@
         private long mLastMoveTime;
         private long mCurrentKeyTime;
 
-        private int mKeyDebounceThreshold;
-        private static final int KEY_DEBOUNCE_FACTOR = 6;
+        private final int mKeyDebounceThresholdSquared;
 
-        KeyDebouncer(int proximityThreshold) {
-            // 1/KEY_DEBOUNCE_FACTOR of distance between adjacent keys
-            mKeyDebounceThreshold = proximityThreshold / KEY_DEBOUNCE_FACTOR;
+        KeyDebouncer(float hysteresisPixel) {
+            mKeyDebounceThresholdSquared = (int)(hysteresisPixel * hysteresisPixel);
         }
 
         public int getLastCodeX() {
@@ -375,15 +373,23 @@
             mLastCodeY = mLastY;
         }
 
-        public boolean isMinorMoveBounce(int x, int y, int keyIndex, int currentKey) {
-            // TODO: Check the coordinate against each key border.  The current
-            // logic is pretty simple.
-            if (keyIndex == currentKey)
+        public boolean isMinorMoveBounce(int x, int y, Key newKey, Key curKey) {
+            if (newKey == curKey)
                 return true;
-            int dx = x - mLastCodeX;
-            int dy = y - mLastCodeY;
-            int delta = dx * dx + dy * dy;
-            return delta < mKeyDebounceThreshold;
+
+            return getSquareDistanceToKeyEdge(x, y, curKey) < mKeyDebounceThresholdSquared;
+        }
+
+        private static int getSquareDistanceToKeyEdge(int x, int y, Key key) {
+            final int left = key.x;
+            final int right = key.x + key.width;
+            final int top = key.y;
+            final int bottom = key.y + key.height;
+            final int edgeX = x < left ? left : (x > right ? right : x);
+            final int edgeY = y < top ? top : (y > bottom ? bottom : y);
+            final int dx = x - edgeX;
+            final int dy = y - edgeY;
+            return dx * dx + dy * dy;
         }
 
         public void startTimeDebouncing(long eventTime) {
@@ -766,7 +772,9 @@
         mProximityThreshold = (int) (dimensionSum * 1.4f / length);
         mProximityThreshold *= mProximityThreshold; // Square it
 
-        mDebouncer = new KeyDebouncer(mProximityThreshold);
+        final float hysteresisPixel = getContext().getResources()
+                .getDimension(R.dimen.key_debounce_hysteresis_distance);
+        mDebouncer = new KeyDebouncer(hysteresisPixel);
     }
 
     @Override
@@ -903,12 +911,12 @@
         mDirtyRect.setEmpty();
     }
 
-    private int getKeyIndices(int x, int y, int[] allKeys) {
+    private int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
         final Key[] keys = mKeys;
         int primaryIndex = NOT_A_KEY;
         int closestKey = NOT_A_KEY;
         int closestKeyDist = mProximityThreshold + 1;
-        java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
+        Arrays.fill(mDistances, Integer.MAX_VALUE);
         int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
         final int keyCount = nearestKeyIndices.length;
         for (int i = 0; i < keyCount; i++) {
@@ -939,10 +947,8 @@
                                 mDistances.length - j - nCodes);
                         System.arraycopy(allKeys, j, allKeys, j + nCodes,
                                 allKeys.length - j - nCodes);
-                        for (int c = 0; c < nCodes; c++) {
-                            allKeys[j + c] = key.codes[c];
-                            mDistances[j + c] = dist;
-                        }
+                        System.arraycopy(key.codes, 0, allKeys, j, nCodes);
+                        Arrays.fill(mDistances, j, j + nCodes, dist);
                         break;
                     }
                 }
@@ -965,7 +971,7 @@
                 //TextEntryState.keyPressedAt(key, x, y);
                 int[] codes = new int[MAX_NEARBY_KEYS];
                 Arrays.fill(codes, NOT_A_KEY);
-                getKeyIndices(x, y, codes);
+                getKeyIndexAndNearbyCodes(x, y, codes);
                 // Multi-tap
                 if (mInMultiTap) {
                     if (mTapCount != -1) {
@@ -975,6 +981,15 @@
                     }
                     code = key.codes[mTapCount];
                 }
+                /*
+                 * Swap the first and second values in the codes array if the primary code is not
+                 * the first value but the second value in the array. This happens when key
+                 * debouncing is in effect.
+                 */
+                if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
+                    codes[1] = codes[0];
+                    codes[0] = code;
+                }
                 mKeyboardActionListener.onKey(code, codes);
                 mKeyboardActionListener.onRelease(code);
             }
@@ -1016,13 +1031,12 @@
         }
         // If key changed and preview is on ...
         if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
-            mHandler.cancelPopupPreview();
-            if (previewPopup.isShowing()) {
-                if (keyIndex == NOT_A_KEY) {
+            if (keyIndex == NOT_A_KEY) {
+                mHandler.cancelPopupPreview();
+                if (previewPopup.isShowing()) {
                     mHandler.dismissPreview(DELAY_AFTER_PREVIEW);
                 }
-            }
-            if (keyIndex != NOT_A_KEY) {
+            } else {
                 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
                     // Show right away, if it's already visible and finger is moving around
                     showKey(keyIndex);
@@ -1295,7 +1309,7 @@
         int touchY = (int) me.getY() + mVerticalCorrection - getPaddingTop();
         final int action = me.getAction();
         final long eventTime = me.getEventTime();
-        int keyIndex = getKeyIndices(touchX, touchY, null);
+        int keyIndex = getKeyIndexAndNearbyCodes(touchX, touchY, null);
         mPossiblePoly = possiblePoly;
 
         // Track the last few movements to look for spurious swipes.
@@ -1354,8 +1368,8 @@
                     if (mCurrentKey == NOT_A_KEY) {
                         mCurrentKey = keyIndex;
                         mDebouncer.updateTimeDebouncing(eventTime);
-                    } else if (mDebouncer.isMinorMoveBounce(touchX, touchY,
-                            keyIndex, mCurrentKey)) {
+                    } else if (mDebouncer.isMinorMoveBounce(touchX, touchY, mKeys[keyIndex],
+                            mKeys[mCurrentKey])) {
                         mDebouncer.updateTimeDebouncing(eventTime);
                         continueLongPress = true;
                     } else if (mRepeatKeyIndex == NOT_A_KEY) {
@@ -1373,12 +1387,19 @@
                         mHandler.startLongPressTimer(me, LONGPRESS_TIMEOUT);
                     }
                 }
-                showPreview(mCurrentKey);
+                /*
+                 * While time debouncing is in effect, mCurrentKey holds the new key and mDebouncer
+                 * holds the last key.  At ACTION_UP event if time debouncing will be in effect
+                 * eventually, the last key should be sent as the result.  In such case mCurrentKey
+                 * should not be showed as popup preview.
+                 */
+                showPreview(mDebouncer.isMinorTimeBounce() ? mDebouncer.getLastKey() : mCurrentKey);
                 break;
 
             case MotionEvent.ACTION_UP:
                 mHandler.cancelKeyTimersAndPopupPreview();
-                if (keyIndex == mCurrentKey) {
+                if (mDebouncer.isMinorMoveBounce(touchX, touchY, mKeys[keyIndex],
+                        mKeys[mCurrentKey])) {
                     mDebouncer.updateTimeDebouncing(eventTime);
                 } else {
                     resetMultiTap();
@@ -1391,7 +1412,6 @@
                     touchY = mDebouncer.getLastCodeY();
                 }
                 showPreview(NOT_A_KEY);
-                Arrays.fill(mKeyIndices, NOT_A_KEY);
                 // If we're not on a repeating key (which sends on a DOWN event)
                 if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
                     detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
@@ -1399,6 +1419,7 @@
                 invalidateKey(keyIndex);
                 mRepeatKeyIndex = NOT_A_KEY;
                 break;
+
             case MotionEvent.ACTION_CANCEL:
                 mHandler.cancelKeyTimersAndPopupPreview();
                 dismissPopupKeyboard();
diff --git a/tests/src/com/android/inputmethod/latin/tests/SuggestHelper.java b/tests/src/com/android/inputmethod/latin/tests/SuggestHelper.java
index e3e995a..107f04c 100644
--- a/tests/src/com/android/inputmethod/latin/tests/SuggestHelper.java
+++ b/tests/src/com/android/inputmethod/latin/tests/SuggestHelper.java
@@ -36,10 +36,11 @@
 
     public SuggestHelper(String tag, Context context, int[] resId) {
         TAG = tag;
+        InputStream[] res = null;
         try {
             // merging separated dictionary into one if dictionary is separated
             int total = 0;
-            InputStream[] res = new InputStream[resId.length];
+            res = new InputStream[resId.length];
             for (int i = 0; i < resId.length; i++) {
                 res[i] = context.getResources().openRawResource(resId[i]);
                 total += res[i].available();
@@ -61,7 +62,7 @@
             Log.w(TAG, "No available memory for binary dictionary");
         } finally {
             try {
-                for (int i = 0;i < is.length; i++) {
+                for (int i = 0;i < res.length; i++) {
                     res[i].close();
                 }
             } catch (IOException e) {