Language switching with slide gesture on spacebar. Bug: 2331173

Shows the language on the spacebar and in the preview bubble. Allows
dragging of the spacebar from side to side to switch to previous or
next languages.
diff --git a/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/src/com/android/inputmethod/latin/KeyboardSwitcher.java
index ea473f2..0ebfe4a 100644
--- a/src/com/android/inputmethod/latin/KeyboardSwitcher.java
+++ b/src/com/android/inputmethod/latin/KeyboardSwitcher.java
@@ -77,6 +77,7 @@
     private int mSymbolsModeState = SYMBOLS_MODE_STATE_NONE;
 
     private int mLastDisplayWidth;
+    private LanguageSwitcher mLanguageSwitcher;
     private Locale mInputLocale;
     private boolean mEnableMultipleLanguages;
 
@@ -94,9 +95,10 @@
      * @param locale the current input locale, or null for default locale with no locale 
      * button.
      */
-    void setInputLocale(Locale locale, boolean enableMultipleLanguages) {
-        mInputLocale = locale;
-        mEnableMultipleLanguages = enableMultipleLanguages;
+    void setLanguageSwitcher(LanguageSwitcher languageSwitcher) {
+        mLanguageSwitcher = languageSwitcher;
+        mInputLocale = mLanguageSwitcher.getInputLocale();
+        mEnableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 1;
     }
 
     void setInputView(LatinKeyboardView inputView) {
@@ -195,11 +197,6 @@
         }
 
         mCurrentId = id;
-        if (mEnableMultipleLanguages) {
-            keyboard.setLanguage(mInputLocale);
-        } else {
-            keyboard.setLanguage(null);
-        }
         mInputView.setKeyboard(keyboard);
         keyboard.setShifted(false);
         keyboard.setShiftLocked(keyboard.isShiftLocked());
@@ -215,6 +212,7 @@
             orig.updateConfiguration(conf, null);
             LatinKeyboard keyboard = new LatinKeyboard(
                 mContext, id.mXml, id.mKeyboardMode, id.mHasVoice);
+            keyboard.setLanguageSwitcher(mLanguageSwitcher);
             if (id.mKeyboardMode == KEYBOARDMODE_NORMAL
                     || id.mKeyboardMode == KEYBOARDMODE_URL
                     || id.mKeyboardMode == KEYBOARDMODE_IM
diff --git a/src/com/android/inputmethod/latin/LanguageSwitcher.java b/src/com/android/inputmethod/latin/LanguageSwitcher.java
new file mode 100644
index 0000000..9717353
--- /dev/null
+++ b/src/com/android/inputmethod/latin/LanguageSwitcher.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ * 
+ * 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 java.util.Locale;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+
+/**
+ * Keeps track of list of selected input languages and the current
+ * input language that the user has selected.
+ */
+public class LanguageSwitcher {
+
+    private Locale[] mLocales;
+    private LatinIME mIme;
+    private String[] mSelectedLanguageArray;
+    private String   mSelectedLanguages;
+    private int      mCurrentIndex = 0;
+    private String   mDefaultInputLanguage;
+    private Locale   mDefaultInputLocale;
+
+    public LanguageSwitcher(LatinIME ime) {
+        mIme = ime;
+        mLocales = new Locale[0];
+    }
+
+    public Locale[]  getLocales() {
+        return mLocales;
+    }
+
+    public int getLocaleCount() {
+        return mLocales.length;
+    }
+
+    /**
+     * Loads the currently selected input languages from shared preferences.
+     * @param sp
+     * @return whether there was any change
+     */
+    public boolean loadLocales(SharedPreferences sp) {
+        String selectedLanguages = sp.getString(LatinIME.PREF_SELECTED_LANGUAGES, null);
+        String currentLanguage   = sp.getString(LatinIME.PREF_INPUT_LANGUAGE, null);
+        if (selectedLanguages == null || selectedLanguages.length() < 1) {
+            loadDefaults();
+            if (mLocales.length == 0) {
+                return false;
+            }
+            mLocales = new Locale[0];
+            return true;
+        }
+        if (selectedLanguages.equals(mSelectedLanguages)) {
+            return false;
+        }
+        mSelectedLanguageArray = selectedLanguages.split(",");
+        mSelectedLanguages = selectedLanguages; // Cache it for comparison later
+        constructLocales();
+        mCurrentIndex = 0;
+        if (currentLanguage != null) {
+            // Find the index
+            mCurrentIndex = 0;
+            for (int i = 0; i < mLocales.length; i++) {
+                if (mSelectedLanguageArray[i].equals(currentLanguage)) {
+                    mCurrentIndex = i;
+                    break;
+                }
+            }
+            // If we didn't find the index, use the first one
+        }
+        return true;
+    }
+
+    private void loadDefaults() {
+        mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
+        mDefaultInputLanguage = mDefaultInputLocale.getLanguage() + "_"
+                + mDefaultInputLocale.getCountry();
+    }
+
+    private void constructLocales() {
+        mLocales = new Locale[mSelectedLanguageArray.length];
+        for (int i = 0; i < mLocales.length; i++) {
+            mLocales[i] = new Locale(mSelectedLanguageArray[i]);
+        }
+    }
+
+    /**
+     * Returns the currently selected input language code, or the display language code if
+     * no specific locale was selected for input.
+     */
+    public String getInputLanguage() {
+        if (getLocaleCount() == 0) return mDefaultInputLanguage;
+
+        return mSelectedLanguageArray[mCurrentIndex];
+    }
+
+    /**
+     * Returns the currently selected input locale, or the display locale if no specific
+     * locale was selected for input.
+     * @return
+     */
+    public Locale getInputLocale() {
+        if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+        return mLocales[mCurrentIndex];
+    }
+
+    /**
+     * Returns the next input locale in the list. Wraps around to the beginning of the
+     * list if we're at the end of the list.
+     * @return
+     */
+    public Locale getNextInputLocale() {
+        if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+        return mLocales[(mCurrentIndex + 1) % mLocales.length];
+    }
+
+    /**
+     * Returns the previous input locale in the list. Wraps around to the end of the
+     * list if we're at the beginning of the list.
+     * @return
+     */
+    public Locale getPrevInputLocale() {
+        if (getLocaleCount() == 0) return mDefaultInputLocale;
+
+        return mLocales[(mCurrentIndex - 1 + mLocales.length) % mLocales.length];
+    }
+
+    public void reset() {
+        mCurrentIndex = 0;
+    }
+
+    public void next() {
+        mCurrentIndex++;
+        if (mCurrentIndex >= mLocales.length) mCurrentIndex = 0; // Wrap around
+    }
+
+    public void prev() {
+        mCurrentIndex--;
+        if (mCurrentIndex < 0) mCurrentIndex = mLocales.length - 1; // Wrap around
+    }
+
+    public void persist() {
+        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mIme);
+        Editor editor = sp.edit();
+        editor.putString(LatinIME.PREF_INPUT_LANGUAGE, getInputLanguage());
+        editor.commit();
+    }
+}
diff --git a/src/com/android/inputmethod/latin/LatinIME.java b/src/com/android/inputmethod/latin/LatinIME.java
index 8056030..ee525b4 100644
--- a/src/com/android/inputmethod/latin/LatinIME.java
+++ b/src/com/android/inputmethod/latin/LatinIME.java
@@ -178,6 +178,7 @@
     Resources mResources;
 
     private String mLocale;
+    private LanguageSwitcher mLanguageSwitcher;
 
     private StringBuilder mComposing = new StringBuilder();
     private WordComposer mWord = new WordComposer();
@@ -244,10 +245,6 @@
         List<String> candidates;
         Map<String, List<CharSequence>> alternatives;
     }
-    private int mCurrentInputLocale = 0;
-    private String mInputLanguage;
-    private String[] mSelectedLanguageArray;
-    private String mSelectedLanguagesList;
     private boolean mRefreshKeyboardRequired;
 
     Handler mHandler = new Handler() {
@@ -285,18 +282,19 @@
     @Override public void onCreate() {
         super.onCreate();
         //setStatusIcon(R.drawable.ime_qwerty);
-        mKeyboardSwitcher = new KeyboardSwitcher(this, this);
         mResources = getResources();
         final Configuration conf = mResources.getConfiguration();
-        mInputLanguage = getPersistedInputLanguage();
-        mSelectedLanguagesList = getSelectedInputLanguages();
-        boolean enableMultipleLanguages = mSelectedLanguagesList != null
-                && mSelectedLanguagesList.split(",").length > 1;
-        if (mInputLanguage == null) {
-            mInputLanguage = conf.locale.toString();
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mLanguageSwitcher = new LanguageSwitcher(this);
+        mLanguageSwitcher.loadLocales(prefs);
+        mKeyboardSwitcher = new KeyboardSwitcher(this, this);
+        mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
+        boolean enableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 0;
+        String inputLanguage = mLanguageSwitcher.getInputLanguage();
+        if (inputLanguage == null) {
+            inputLanguage = conf.locale.toString();
         }
-        initSuggest(mInputLanguage);
-        mKeyboardSwitcher.setInputLocale(conf.locale, enableMultipleLanguages);
+        initSuggest(inputLanguage);
         mOrientation = conf.orientation;
 
         mVibrateDuration = mResources.getInteger(R.integer.vibrate_duration_ms);
@@ -317,8 +315,7 @@
                 }
               });
         }
-        PreferenceManager.getDefaultSharedPreferences(this)
-                .registerOnSharedPreferenceChangeListener(this);
+        prefs.registerOnSharedPreferenceChangeListener(this);
     }
 
     private void initSuggest(String locale) {
@@ -429,7 +426,7 @@
 
         if (mRefreshKeyboardRequired) {
             mRefreshKeyboardRequired = false;
-            toggleLanguage(true);
+            toggleLanguage(true, true);
         }
 
         mKeyboardSwitcher.makeKeyboards(false);
@@ -791,8 +788,7 @@
         if (mKeyboardSwitcher == null) {
             mKeyboardSwitcher = new KeyboardSwitcher(this, this);
         }
-        mKeyboardSwitcher.setInputLocale(new Locale(mInputLanguage),
-                getSelectedInputLanguages() != null);
+        mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher);
         if (mInputView != null) {
             mKeyboardSwitcher.setVoiceMode(mEnableVoice, mVoiceOnPrimary);
         }
@@ -919,7 +915,10 @@
                 showOptionsMenu();
                 break;
             case LatinKeyboardView.KEYCODE_NEXT_LANGUAGE:
-                toggleLanguage(false);
+                toggleLanguage(false, true);
+                break;
+            case LatinKeyboardView.KEYCODE_PREV_LANGUAGE:
+                toggleLanguage(false, false);
                 break;
             case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS:
                 if (mCapsLock) {
@@ -1514,27 +1513,30 @@
         }
     }
 
-    private void toggleLanguage(boolean reset) {
-        final String [] languages = mSelectedLanguageArray;
-        if (reset) mCurrentInputLocale = -1;
-        mCurrentInputLocale = (mCurrentInputLocale + 1)
-                % (languages != null ? languages.length : 1);
-        mInputLanguage = languages != null ? languages[mCurrentInputLocale] :
-                getResources().getConfiguration().locale.getLanguage();
+    private void toggleLanguage(boolean reset, boolean next) {
+        if (reset) {
+            mLanguageSwitcher.reset();
+        } else {
+            if (next) {
+                mLanguageSwitcher.next();
+            } else {
+                mLanguageSwitcher.prev();
+            }
+        }
         int currentKeyboardMode = mKeyboardSwitcher.getKeyboardMode();
         reloadKeyboards();
         mKeyboardSwitcher.makeKeyboards(true);
         mKeyboardSwitcher.setKeyboardMode(currentKeyboardMode, 0,
                 mEnableVoiceButton && mEnableVoice);
-        initSuggest(mInputLanguage);
-        persistInputLanguage(mInputLanguage);
+        initSuggest(mLanguageSwitcher.getInputLanguage());
+        mLanguageSwitcher.persist();
         updateShiftKeyState(getCurrentInputEditorInfo());
     }
 
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
             String key) {
         if (PREF_SELECTED_LANGUAGES.equals(key)) {
-            updateSelectedLanguages(sharedPreferences.getString(key, null));
+            mLanguageSwitcher.loadLocales(sharedPreferences);
             mRefreshKeyboardRequired = true;
         }
     }
@@ -1556,6 +1558,8 @@
     }
 
     public void onRelease(int primaryCode) {
+        // Reset any drag flags in the keyboard
+        ((LatinKeyboard) mInputView.getKeyboard()).keyReleased();
         //vibrate();
     }
 
@@ -1750,16 +1754,7 @@
         mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE,
                 mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions;
         updateCorrectionMode();
-        String languageList = sp.getString(PREF_SELECTED_LANGUAGES, null);
-        updateSelectedLanguages(languageList);
-    }
-
-    private void updateSelectedLanguages(String languageList) {
-        if (languageList != null && languageList.length() > 1) {
-            mSelectedLanguageArray = languageList.split(",");
-        } else {
-            mSelectedLanguageArray = null;
-        }
+        mLanguageSwitcher.loadLocales(sp);
     }
 
     private String getPersistedInputLanguage() {
@@ -1767,13 +1762,6 @@
         return sp.getString(PREF_INPUT_LANGUAGE, null);
     }
 
-    private void persistInputLanguage(String inputLanguage) {
-        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
-        Editor editor = sp.edit();
-        editor.putString(PREF_INPUT_LANGUAGE, inputLanguage);
-        editor.commit();
-    }
-
     private String getSelectedInputLanguages() {
         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
         return sp.getString(PREF_SELECTED_LANGUAGES, null);
diff --git a/src/com/android/inputmethod/latin/LatinKeyboard.java b/src/com/android/inputmethod/latin/LatinKeyboard.java
index f876af7..92f93b3 100644
--- a/src/com/android/inputmethod/latin/LatinKeyboard.java
+++ b/src/com/android/inputmethod/latin/LatinKeyboard.java
@@ -16,19 +16,26 @@
 
 package com.android.inputmethod.latin;
 
+import java.util.List;
 import java.util.Locale;
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.ColorFilter;
 import android.graphics.Paint;
+import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
+import android.graphics.Rect;
 import android.graphics.Paint.Align;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.inputmethodservice.Keyboard;
+import android.text.TextPaint;
+import android.view.ViewConfiguration;
 import android.view.inputmethod.EditorInfo;
 
 public class LatinKeyboard extends Keyboard {
@@ -38,18 +45,29 @@
     private Drawable mOldShiftIcon;
     private Drawable mOldShiftPreviewIcon;
     private Drawable mSpaceIcon;
+    private Drawable mSpacePreviewIcon;
     private Drawable mMicIcon;
     private Drawable mMicPreviewIcon;
     private Drawable m123MicIcon;
     private Drawable m123MicPreviewIcon;
+    private Drawable mButtonArrowLeftIcon;
+    private Drawable mButtonArrowRightIcon;
     private Key mShiftKey;
     private Key mEnterKey;
     private Key mF1Key;
     private Key mSpaceKey;
+    private int mSpaceKeyIndex = -1;
+    private int mSpaceDragStartX;
+    private int mSpaceDragLastDiff;
     /* package */ Locale mLocale;
+    private LanguageSwitcher mLanguageSwitcher;
     private Resources mRes;
+    private Context mContext;
     private int mMode;
     private boolean mHasVoice;
+    private boolean mCurrentlyInSpace;
+    private SlidingLocaleDrawable mSlidingLocaleIcon;
+    private Rect mBounds = new Rect();
 
     private int mExtensionResId; 
     
@@ -59,6 +77,8 @@
     
     private int mShiftState = SHIFT_OFF;
 
+    private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f;
+
     static int sSpacebarVerticalCorrection;
 
     public LatinKeyboard(Context context, int xmlLayoutResId) {
@@ -68,6 +88,7 @@
     public LatinKeyboard(Context context, int xmlLayoutResId, int mode, boolean hasVoice) {
         super(context, xmlLayoutResId, mode);
         final Resources res = context.getResources();
+        mContext = context;
         mMode = mode;
         mRes = res;
         mHasVoice = hasVoice;
@@ -77,11 +98,15 @@
                 mShiftLockPreviewIcon.getIntrinsicWidth(),
                 mShiftLockPreviewIcon.getIntrinsicHeight());
         mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space);
+        mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space);
         mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic);
         mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic);
+        mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left);
+        mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right);
         sSpacebarVerticalCorrection = res.getDimensionPixelOffset(
                 R.dimen.spacebar_vertical_correction);
         setF1Key();
+        mSpaceKeyIndex = indexOf((int) ' ');
     }
 
     public LatinKeyboard(Context context, int layoutTemplateResId, 
@@ -237,7 +262,6 @@
 
     private void setF1Key() {
         if (mF1Key == null) return;
-        System.err.println("Setting F1 key");
         if (!mHasVoice) {
             mF1Key.label = ",";
             mF1Key.codes = new int[] { ',' };
@@ -260,34 +284,179 @@
             canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
             Paint paint = new Paint();
             paint.setAntiAlias(true);
-            // TODO: Make the text size a customizable attribute
-            paint.setTextSize(18);
+            // Get the text size from the theme
+            paint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14));
             paint.setTextAlign(Align.CENTER);
             // Draw a drop shadow for the text
-            paint.setShadowLayer(1f, 0, 0, 0xFF000000);
+            paint.setShadowLayer(2f, 0, 0, 0xFF000000);
             paint.setColor(0xFF808080);
-            canvas.drawText(mLocale.getDisplayLanguage(mLocale),
-                    buffer.getWidth() / 2, - paint.ascent() + 2, paint);
+            final String language = getInputLanguage(mSpaceKey.width, paint);
+            final int ascent = (int) -paint.ascent();
+            canvas.drawText(language,
+                    buffer.getWidth() / 2, ascent, paint);
+            // Put arrows on either side of the text
+            if (mLanguageSwitcher.getLocaleCount() > 1) {
+                Rect bounds = new Rect();
+                paint.getTextBounds(language, 0, language.length(), bounds);
+                drawButtonArrow(mButtonArrowLeftIcon, canvas,
+                        (mSpaceKey.width - bounds.right) / 2
+                        - mButtonArrowLeftIcon.getIntrinsicWidth(),
+                        (int) paint.getTextSize());
+                drawButtonArrow(mButtonArrowRightIcon, canvas,
+                        (mSpaceKey.width + bounds.right) / 2, (int) paint.getTextSize());
+            }
+            // Draw the spacebar icon at the bottom
             int x = (buffer.getWidth() - mSpaceIcon.getIntrinsicWidth()) / 2;
             int y = buffer.getHeight() - mSpaceIcon.getIntrinsicHeight();
             mSpaceIcon.setBounds(x, y, 
                     x + mSpaceIcon.getIntrinsicWidth(), y + mSpaceIcon.getIntrinsicHeight());
             mSpaceIcon.draw(canvas);
             mSpaceKey.icon = new BitmapDrawable(mRes, buffer);
-            mSpaceKey.repeatable = false;
+            mSpaceKey.repeatable = mLanguageSwitcher.getLocaleCount() < 2;
         } else {
             mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space);
             mSpaceKey.repeatable = true;
         }
     }
 
-    public void setLanguage(Locale locale) {
+    private void drawButtonArrow(Drawable arrow, Canvas canvas, int x, int bottomY) {
+        arrow.setBounds(x, bottomY - arrow.getIntrinsicHeight(), x + arrow.getIntrinsicWidth(),
+                bottomY);
+        arrow.draw(canvas);
+    }
+
+    private String getInputLanguage(int widthAvail, Paint paint) {
+        return chooseDisplayName(mLanguageSwitcher.getInputLocale(), widthAvail, paint);
+    }
+
+    private String getNextInputLanguage(int widthAvail, Paint paint) {
+        return chooseDisplayName(mLanguageSwitcher.getNextInputLocale(), widthAvail, paint);
+    }
+
+    private String getPrevInputLanguage(int widthAvail, Paint paint) {
+        return chooseDisplayName(mLanguageSwitcher.getPrevInputLocale(), widthAvail, paint);
+    }
+
+    private String chooseDisplayName(Locale locale, int widthAvail, Paint paint) {
+        if (widthAvail < (int) (.35 * getMinWidth())) {
+            return locale.getLanguage().substring(0, 2).toUpperCase(locale);
+        } else {
+            return locale.getDisplayLanguage(locale);
+        }
+    }
+
+    private void updateLocaleDrag(int diff) {
+        if (mSlidingLocaleIcon == null) {
+            mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, mSpaceKey.width,
+                    mSpacePreviewIcon.getIntrinsicHeight());
+            mSlidingLocaleIcon.setBounds(0, 0, mSpaceKey.width,
+                    mSpacePreviewIcon.getIntrinsicHeight());
+            mSpaceKey.iconPreview = mSlidingLocaleIcon;
+        }
+        mSlidingLocaleIcon.setDiff(diff);
+        if (Math.abs(diff) == Integer.MAX_VALUE) {
+            mSpaceKey.iconPreview = mSpacePreviewIcon;
+        } else {
+            mSpaceKey.iconPreview = mSlidingLocaleIcon;
+        }
+        mSpaceKey.iconPreview.invalidateSelf();
+    }
+
+    public int getLanguageChangeDirection() {
+        if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2
+                || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) {
+            return 0; // No change
+        }
+        return mSpaceDragLastDiff > 0 ? 1 : -1;
+    }
+
+    public void setLanguageSwitcher(LanguageSwitcher switcher) {
+        mLanguageSwitcher = switcher;
+        Locale locale = mLanguageSwitcher.getLocaleCount() > 0
+                ? mLanguageSwitcher.getInputLocale()
+                : null;
         if (mLocale != null && mLocale.equals(locale)) return;
         mLocale = locale;
         updateSpaceBarForLocale();
     }
 
-    static class LatinKey extends Keyboard.Key {
+    boolean isCurrentlyInSpace() {
+        return mCurrentlyInSpace;
+    }
+
+    void keyReleased() {
+        mCurrentlyInSpace = false;
+        mSpaceDragLastDiff = 0;
+        if (mSpaceKey != null) {
+            updateLocaleDrag(Integer.MAX_VALUE);
+        }
+    }
+
+    /**
+     * Does the magic of locking the touch gesture into the spacebar when
+     * switching input languages.
+     */
+    boolean isInside(LatinKey key, int x, int y) {
+        final int code = key.codes[0];
+        if (code == KEYCODE_SHIFT ||
+                code == KEYCODE_DELETE) {
+            y -= key.height / 10;
+            if (code == KEYCODE_SHIFT) x += key.width / 6;
+            if (code == KEYCODE_DELETE) x -= key.width / 6;
+        } else if (code == LatinIME.KEYCODE_SPACE) {
+            y += LatinKeyboard.sSpacebarVerticalCorrection;
+            if (mLanguageSwitcher.getLocaleCount() > 1) {
+                if (mCurrentlyInSpace) {
+                    int diff = x - mSpaceDragStartX;
+                    if (Math.abs(diff - mSpaceDragLastDiff) > 0) {
+                        updateLocaleDrag(diff);
+                    }
+                    mSpaceDragLastDiff = diff;
+                    return true;
+                } else {
+                    boolean insideSpace = key.isInsideSuper(x, y);
+                    if (insideSpace) {
+                        mCurrentlyInSpace = true;
+                        mSpaceDragStartX = x;
+                        updateLocaleDrag(0);
+                    }
+                    return insideSpace;
+                }
+            }
+        }
+
+        // Lock into the spacebar
+        if (mCurrentlyInSpace) return false;
+
+        return key.isInsideSuper(x, y);
+    }
+
+    @Override
+    public int[] getNearestKeys(int x, int y) {
+        if (mCurrentlyInSpace) {
+            return new int[] { mSpaceKeyIndex };
+        } else {
+            return super.getNearestKeys(x, y);
+        }
+    }
+
+    private int indexOf(int code) {
+        List<Key> keys = getKeys();
+        int count = keys.size();
+        for (int i = 0; i < count; i++) {
+            if (keys.get(i).codes[0] == code) return i;
+        }
+        return -1;
+    }
+
+    private int getTextSizeFromTheme(int style, int defValue) {
+        TypedArray array = mContext.getTheme().obtainStyledAttributes(
+                style, new int[] { android.R.attr.textSize });
+        int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
+        return textSize;
+    }
+
+    class LatinKey extends Keyboard.Key {
         
         private boolean mShiftLockEnabled;
         
@@ -318,16 +487,130 @@
          */
         @Override
         public boolean isInside(int x, int y) {
-            final int code = codes[0];
-            if (code == KEYCODE_SHIFT ||
-                    code == KEYCODE_DELETE) {
-                y -= height / 10;
-                if (code == KEYCODE_SHIFT) x += width / 6;
-                if (code == KEYCODE_DELETE) x -= width / 6;
-            } else if (code == LatinIME.KEYCODE_SPACE) {
-                y += LatinKeyboard.sSpacebarVerticalCorrection;
-            }
+            return LatinKeyboard.this.isInside(this, x, y);
+        }
+
+        boolean isInsideSuper(int x, int y) {
             return super.isInside(x, y);
         }
     }
+
+    /**
+     * Animation to be displayed on the spacebar preview popup when switching 
+     * languages by swiping the spacebar. It draws the current, previous and
+     * next languages and moves them by the delta of touch movement on the spacebar.
+     */
+    class SlidingLocaleDrawable extends Drawable {
+
+        private int mWidth;
+        private int mHeight;
+        private Drawable mBackground;
+        private int mDiff;
+        private TextPaint mTextPaint;
+        private int mMiddleX;
+        private int mAscent;
+        private Drawable mLeftDrawable;
+        private Drawable mRightDrawable;
+        private boolean mHitThreshold;
+        private int     mThreshold;
+        private String mCurrentLanguage;
+        private String mNextLanguage;
+        private String mPrevLanguage;
+
+        public SlidingLocaleDrawable(Drawable background, int width, int height) {
+            mBackground = background;
+            mBackground.setBounds(0, 0,
+                    mBackground.getIntrinsicWidth(), mBackground.getIntrinsicHeight());
+            mWidth = width;
+            mHeight = height;
+            mTextPaint = new TextPaint();
+            int textSize = getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18);
+            mTextPaint.setTextSize(textSize);
+            mTextPaint.setColor(0);
+            mTextPaint.setTextAlign(Align.CENTER);
+            mTextPaint.setAlpha(255);
+            mTextPaint.setAntiAlias(true);
+            mAscent = (int) mTextPaint.ascent();
+            mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2;
+            mLeftDrawable =
+                    mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left);
+            mRightDrawable =
+                    mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right);
+            mLeftDrawable.setBounds(0, 0,
+                    mLeftDrawable.getIntrinsicWidth(), mLeftDrawable.getIntrinsicHeight());
+            mRightDrawable.setBounds(mWidth - mRightDrawable.getIntrinsicWidth(), 0,
+                    mWidth, mRightDrawable.getIntrinsicHeight());
+            mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop();
+        }
+
+        void setDiff(int diff) {
+            if (diff == Integer.MAX_VALUE) {
+                mHitThreshold = false;
+                mCurrentLanguage = null;
+                return;
+            }
+            mDiff = diff;
+            if (mDiff > mWidth) mDiff = mWidth;
+            if (mDiff < -mWidth) mDiff = -mWidth;
+            if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
+            invalidateSelf();
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            canvas.save();
+            if (mHitThreshold) {
+                mTextPaint.setColor(0);
+                canvas.clipRect(0, 0, mWidth, mHeight);
+                int alpha = (255 * Math.max(0, mWidth / 2 - Math.abs(mDiff))) / (mWidth / 2);
+                mTextPaint.setAlpha(alpha);
+
+                if (mCurrentLanguage == null) {
+                    mCurrentLanguage = getInputLanguage(mWidth, mTextPaint);
+                    mNextLanguage = getNextInputLanguage(mWidth, mTextPaint);
+                    mPrevLanguage = getPrevInputLanguage(mWidth, mTextPaint);
+                }
+
+                canvas.drawText(mCurrentLanguage,
+                        mWidth / 2 + mDiff, -mAscent + 4, mTextPaint);
+                mTextPaint.setAlpha(255 - alpha);
+                canvas.drawText(mNextLanguage,
+                        mDiff - mWidth / 2, -mAscent + 4, mTextPaint);
+                canvas.drawText(mPrevLanguage,
+                        mDiff + mWidth + mWidth / 2, -mAscent + 4, mTextPaint);
+                mLeftDrawable.draw(canvas);
+                mRightDrawable.draw(canvas);
+            }
+            if (mBackground != null) {
+                canvas.translate(mMiddleX, 0);
+                mBackground.draw(canvas);
+            }
+            canvas.restore();
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+
+        @Override
+        public void setAlpha(int alpha) {
+            // Ignore
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter cf) {
+            // Ignore
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return mWidth;
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return mHeight;
+        }
+    }
 }
diff --git a/src/com/android/inputmethod/latin/LatinKeyboardView.java b/src/com/android/inputmethod/latin/LatinKeyboardView.java
index a88c181..05f8aff 100644
--- a/src/com/android/inputmethod/latin/LatinKeyboardView.java
+++ b/src/com/android/inputmethod/latin/LatinKeyboardView.java
@@ -38,9 +38,15 @@
     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;
 
     private Keyboard mPhoneKeyboard;
 
+    private boolean mExtensionVisible;
+    private LatinKeyboardView mExtension;
+    private PopupWindow mExtensionPopup;
+    private boolean mFirstEvent;
+
     public LatinKeyboardView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -66,22 +72,33 @@
             // Long pressing on 0 in phone number keypad gives you a '+'.
             getOnKeyboardActionListener().onKey('+', null);
             return true;
-        } else if (key.codes[0] == ' ' && ((LatinKeyboard)getKeyboard()).mLocale != null) {
-            getOnKeyboardActionListener().onKey(KEYCODE_NEXT_LANGUAGE, null);
-            return true;
         } else {
             return super.onLongPress(key);
         }
     }
 
-    private boolean mExtensionVisible;
-    private LatinKeyboardView mExtension;
-    private PopupWindow mExtensionPopup;
-    private boolean mFirstEvent;
-
     @Override
     public boolean onTouchEvent(MotionEvent me) {
-        if (((LatinKeyboard) getKeyboard()).getExtension() == 0) {
+        LatinKeyboard keyboard = (LatinKeyboard) getKeyboard();
+        // Reset any bounding box controls in the keyboard
+        if (me.getAction() == MotionEvent.ACTION_DOWN) {
+            keyboard.keyReleased();
+        }
+
+        if (me.getAction() == MotionEvent.ACTION_UP) {
+            int languageDirection = keyboard.getLanguageChangeDirection();
+            if (languageDirection != 0) {
+                getOnKeyboardActionListener().onKey(
+                        languageDirection == 1 ? KEYCODE_NEXT_LANGUAGE : KEYCODE_PREV_LANGUAGE,
+                        null);
+                me.setAction(MotionEvent.ACTION_CANCEL);
+                keyboard.keyReleased();
+                return super.onTouchEvent(me);
+            }
+        }
+
+        // If we don't have an extension keyboard, don't go any further.
+        if (keyboard.getExtension() == 0) {
             return super.onTouchEvent(me);
         }
         if (me.getY() < 0) {
diff --git a/src/com/android/inputmethod/latin/TextEntryState.java b/src/com/android/inputmethod/latin/TextEntryState.java
index 90c364a..c5e8ad9 100644
--- a/src/com/android/inputmethod/latin/TextEntryState.java
+++ b/src/com/android/inputmethod/latin/TextEntryState.java
@@ -123,6 +123,7 @@
     }
     
     public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord) {
+        if (typedWord == null) return;
         if (!typedWord.equals(actualWord)) {
             sAutoSuggestCount++;
         }