Merge "Special case quotes at start and end of words"
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 73a8f1f..655c46e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1548,6 +1548,9 @@
                 mComposingStateManager.onFinishComposingText();
             }
         }
+        if (code == Keyboard.CODE_SINGLE_QUOTE && !isCursorTouchingWord()) {
+            mHasUncommittedTypedChars = false;
+        }
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         if (switcher.isShiftedOrShiftLocked()) {
             if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
@@ -1820,7 +1823,15 @@
         // a boolean flag. Right now this is handled with a slight hack in
         // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
         final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
-                mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
+                mSuggest.getUnigramDictionaries(),
+                // If the typed string ends with a single quote, for dictionary lookup purposes
+                // we behave as if the single quote was not here. Here, we are looking up the
+                // typed string in the dictionary (to avoid autocorrecting from an existing
+                // word, so for consistency this lookup should be made WITHOUT the trailing
+                // single quote.
+                wordComposer.isLastCharASingleQuote()
+                        ? typedWord.subSequence(0, typedWord.length() - 1) : typedWord,
+                preferCapitalization());
         if (mCorrectionMode == Suggest.CORRECTION_FULL
                 || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
             autoCorrectionAvailable |= (!allowsToBeAutoCorrected);
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 97e9174..5a3c348 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -20,6 +20,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.ProximityInfo;
 
 import java.io.File;
@@ -81,6 +82,8 @@
     public static final String DICT_KEY_USER_BIGRAM = "user_bigram";
     public static final String DICT_KEY_WHITELIST ="whitelist";
 
+    private static String SINGLE_QUOTE_AS_STRING = String.valueOf((char)Keyboard.CODE_SINGLE_QUOTE);
+
     private static final boolean DBG = LatinImeLogger.sDBG;
 
     private AutoCorrection mAutoCorrection;
@@ -101,11 +104,12 @@
 
     private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
     ArrayList<CharSequence> mBigramSuggestions  = new ArrayList<CharSequence>();
-    private CharSequence mTypedWord;
+    private CharSequence mConsideredWord;
 
     // TODO: Remove these member variables by passing more context to addWord() callback method
     private boolean mIsFirstCharCapitalized;
     private boolean mIsAllUpperCase;
+    private boolean mIsLastCharASingleQuote;
 
     private int mCorrectionMode = CORRECTION_BASIC;
 
@@ -295,17 +299,19 @@
         mAutoCorrection.init();
         mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
         mIsAllUpperCase = wordComposer.isAllUpperCase();
+        mIsLastCharASingleQuote = wordComposer.isLastCharASingleQuote();
         collectGarbage(mSuggestions, mPrefMaxSuggestions);
         Arrays.fill(mScores, 0);
 
-        // Save a lowercase version of the original word
-        String typedWord = wordComposer.getTypedWord();
+        final String typedWord = wordComposer.getTypedWord();
+        final String consideredWord = mIsLastCharASingleQuote
+                ? typedWord.substring(0, typedWord.length() - 1) : typedWord;
         if (typedWord != null) {
             // Treating USER_TYPED as UNIGRAM suggestion for logging now.
             LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED,
                     Dictionary.DataType.UNIGRAM);
         }
-        mTypedWord = typedWord;
+        mConsideredWord = consideredWord;
 
         if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
                 || mCorrectionMode == CORRECTION_BASIC)) {
@@ -321,7 +327,7 @@
                 for (final Dictionary dictionary : mBigramDictionaries.values()) {
                     dictionary.getBigrams(wordComposer, prevWordForBigram, this);
                 }
-                if (TextUtils.isEmpty(typedWord)) {
+                if (TextUtils.isEmpty(consideredWord)) {
                     // Nothing entered: return all bigrams for the previous word
                     int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
                     for (int i = 0; i < insertCount; ++i) {
@@ -330,7 +336,7 @@
                 } else {
                     // Word entered: return only bigrams that match the first char of the typed word
                     @SuppressWarnings("null")
-                    final char currentChar = typedWord.charAt(0);
+                    final char currentChar = consideredWord.charAt(0);
                     // TODO: Must pay attention to locale when changing case.
                     final char currentCharUpper = Character.toUpperCase(currentChar);
                     int count = 0;
@@ -354,24 +360,32 @@
                 if (key.equals(DICT_KEY_USER_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
                     continue;
                 final Dictionary dictionary = mUnigramDictionaries.get(key);
-                dictionary.getWords(wordComposer, this, proximityInfo);
+                if (mIsLastCharASingleQuote) {
+                    final WordComposer tmpWordComposer = new WordComposer(wordComposer);
+                    tmpWordComposer.deleteLast();
+                    dictionary.getWords(tmpWordComposer, this, proximityInfo);
+                } else {
+                    dictionary.getWords(wordComposer, this, proximityInfo);
+                }
             }
         }
-        final String typedWordString = typedWord == null ? null : typedWord.toString();
+        final String consideredWordString =
+                consideredWord == null ? null : consideredWord.toString();
 
         CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized,
-                mWhiteListDictionary.getWhitelistedWord(typedWordString));
+                mWhiteListDictionary.getWhitelistedWord(consideredWordString));
 
         mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer,
-                mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode,
+                mSuggestions, mScores, consideredWord, mAutoCorrectionThreshold, mCorrectionMode,
                 whitelistedWord);
 
         if (whitelistedWord != null) {
-            mSuggestions.add(0, whitelistedWord);
+            mSuggestions.add(0, mIsLastCharASingleQuote
+                    ? whitelistedWord + SINGLE_QUOTE_AS_STRING : whitelistedWord);
         }
 
         if (typedWord != null) {
-            mSuggestions.add(0, typedWordString);
+            mSuggestions.add(0, typedWord.toString());
         }
         Utils.removeDupes(mSuggestions);
 
@@ -424,7 +438,7 @@
         int pos = 0;
 
         // Check if it's the same word, only caps are different
-        if (Utils.equalsIgnoreCase(mTypedWord, word, offset, length)) {
+        if (Utils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
             // TODO: remove this surrounding if clause and move this logic to
             // getSuggestedWordBuilder.
             if (suggestions.size() > 0) {
@@ -486,6 +500,9 @@
         } else {
             sb.append(word, offset, length);
         }
+        if (mIsLastCharASingleQuote) {
+            sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
+        }
         suggestions.add(pos, sb);
         if (suggestions.size() > prefMaxSuggestions) {
             final CharSequence garbage = suggestions.remove(prefMaxSuggestions);
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 7f3a542..612b160 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -16,9 +16,11 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyDetector;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * A place to store the currently composing word with information such as adjacent key codes as well
@@ -41,7 +43,9 @@
     private int mCapsCount;
 
     private boolean mAutoCapitalized;
-    
+    // Cache this value for performance
+    private boolean mIsLastCharASingleQuote;
+
     /**
      * Whether the user chose to capitalize the first char of the word.
      */
@@ -53,6 +57,7 @@
         mTypedWord = new StringBuilder(N);
         mXCoordinates = new int[N];
         mYCoordinates = new int[N];
+        mIsLastCharASingleQuote = false;
     }
 
     public WordComposer(WordComposer source) {
@@ -62,11 +67,12 @@
     public void init(WordComposer source) {
         mCodes = new ArrayList<int[]>(source.mCodes);
         mTypedWord = new StringBuilder(source.mTypedWord);
-        mXCoordinates = source.mXCoordinates;
-        mYCoordinates = source.mYCoordinates;
+        mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
+        mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
         mCapsCount = source.mCapsCount;
         mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
         mAutoCapitalized = source.mAutoCapitalized;
+        mIsLastCharASingleQuote = source.mIsLastCharASingleQuote;
     }
 
     /**
@@ -77,6 +83,7 @@
         mTypedWord.setLength(0);
         mCapsCount = 0;
         mIsFirstCharCapitalized = false;
+        mIsLastCharASingleQuote = false;
     }
 
     /**
@@ -126,6 +133,7 @@
         mIsFirstCharCapitalized = isFirstCharCapitalized(
                 newIndex, primaryCode, mIsFirstCharCapitalized);
         if (Character.isUpperCase(primaryCode)) mCapsCount++;
+        mIsLastCharASingleQuote = Keyboard.CODE_SINGLE_QUOTE == primaryCode;
     }
 
     /**
@@ -157,6 +165,10 @@
         }
         if (size() == 0) {
             mIsFirstCharCapitalized = false;
+            mIsLastCharASingleQuote = false;
+        } else {
+            mIsLastCharASingleQuote =
+                    Keyboard.CODE_SINGLE_QUOTE == mTypedWord.codePointAt(mTypedWord.length() - 1);
         }
     }
 
@@ -179,6 +191,10 @@
         return mIsFirstCharCapitalized;
     }
 
+    public boolean isLastCharASingleQuote() {
+        return mIsLastCharASingleQuote;
+    }
+
     /**
      * Whether or not all of the user typed chars are upper case
      * @return true if all user typed chars are upper case, false otherwise