Merge "[IL90] Small refactoring"
diff --git a/java/res/layout/suggestions_strip.xml b/java/res/layout/suggestions_strip.xml
index 908e305..2ffac17 100644
--- a/java/res/layout/suggestions_strip.xml
+++ b/java/res/layout/suggestions_strip.xml
@@ -25,4 +25,19 @@
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
+    <LinearLayout
+        android:id="@+id/add_to_dictionary_strip"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible">
+        <include
+            layout="@layout/suggestion_word"
+            android:id="@+id/word_to_save" />
+        <include
+            layout="@layout/suggestion_divider" />
+        <include
+            layout="@layout/hint_add_to_dictionary"
+            android:id="@+id/hint_add_to_dictionary" />
+    </LinearLayout>
 </merge>
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 325a0d9..0d0b7a1 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -815,6 +815,17 @@
     }
 
     /**
+     * Looks at the text just before the cursor to find out if we are inside a double quote.
+     *
+     * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached.
+     * However this won't be a concrete problem in most situations, as the cache is almost always
+     * long enough for this use.
+     */
+    public boolean isInsideDoubleQuoteOrAfterDigit() {
+        return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText);
+    }
+
+    /**
      * Try to get the text from the editor to expose lies the framework may have been
      * telling us. Concretely, when the device rotates, the frameworks tells us about where the
      * cursor used to be initially in the editor at the time it first received the focus; this
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 3fb7037..43d7533 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -661,8 +661,21 @@
         final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
                 isFromSuggestionStrip);
 
-        if (SpaceState.PHANTOM == spaceState &&
-                settingsValues.isUsuallyPrecededBySpace(codePoint)) {
+        final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
+                && mConnection.isInsideDoubleQuoteOrAfterDigit();
+
+        final boolean needsPrecedingSpace;
+        if (SpaceState.PHANTOM != spaceState) {
+            needsPrecedingSpace = false;
+        } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
+            // Double quotes behave like they are usually preceded by space iff we are
+            // not inside a double quote or after a digit.
+            needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit;
+        } else {
+            needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint);
+        }
+
+        if (needsPrecedingSpace) {
             promotePhantomSpace(settingsValues);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -689,14 +702,17 @@
             if (swapWeakSpace) {
                 swapSwapperAndSpace(keyboardSwitcher);
                 mSpaceState = SpaceState.SWAP_PUNCTUATION;
-            } else if (SpaceState.PHANTOM == spaceState
-                    && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
+            } else if ((SpaceState.PHANTOM == spaceState
+                    && settingsValues.isUsuallyFollowedBySpace(codePoint))
+                    || (Constants.CODE_DOUBLE_QUOTE == codePoint
+                            && isInsideDoubleQuoteOrAfterDigit)) {
                 // If we are in phantom space state, and the user presses a separator, we want to
                 // stay in phantom space state so that the next keypress has a chance to add the
                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
                 // then insert a comma and go on to typing the next word, I want the space to be
                 // inserted automatically before the next word, the same way it is when I don't
-                // input the comma.
+                // input the comma. A double quote behaves like it's usually followed by space if
+                // we're inside a double quote.
                 // The case is a little different if the separator is a space stripper. Such a
                 // separator does not normally need a space on the right (that's the difference
                 // between swappers and strippers), so we should not stay in phantom space state if
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index f836e61..af04de4 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -38,7 +38,6 @@
 import android.text.style.UnderlineSpan;
 import android.util.AttributeSet;
 import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
@@ -100,10 +99,6 @@
     private static final int AUTO_CORRECT_UNDERLINE = 0x02;
     private static final int VALID_TYPED_WORD_BOLD = 0x04;
 
-    private final TextView mWordToSaveView;
-    private final TextView mLeftwardsArrowView;
-    private final TextView mHintToSaveView;
-
     public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs,
             final int defStyle, final ArrayList<TextView> wordViews,
             final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) {
@@ -157,11 +152,6 @@
                 R.dimen.config_more_suggestions_bottom_gap);
         mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
                 R.dimen.config_more_suggestions_row_height);
-
-        final LayoutInflater inflater = LayoutInflater.from(context);
-        mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
-        mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
-        mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
     }
 
     public int getMaxMoreSuggestionsRow() {
@@ -466,54 +456,30 @@
         mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
     }
 
-    public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView,
-            final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
+    public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip,
+            final int stripWidth, final CharSequence hintText) {
         final int width = stripWidth - mDividerWidth - mPadding * 2;
 
-        final TextView wordView = mWordToSaveView;
+        final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save);
         wordView.setTextColor(mColorTypedWord);
         final int wordWidth = (int)(width * mCenterSuggestionWeight);
-        final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
+        final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint());
         final float wordScaleX = wordView.getTextScaleX();
-        // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
-        // will be extracted at {@link #getAddToDictionaryWord()}.
-        wordView.setTag(word);
-        wordView.setText(text);
+        wordView.setText(wordToSave);
         wordView.setTextScaleX(wordScaleX);
-        stripView.addView(wordView);
         setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
 
-        stripView.addView(mDividerViews.get(0));
-
-        final TextView leftArrowView = mLeftwardsArrowView;
-        leftArrowView.setTextColor(mColorAutoCorrect);
-        leftArrowView.setText(LEFTWARDS_ARROW);
-        stripView.addView(leftArrowView);
-
-        final TextView hintView = mHintToSaveView;
+        final TextView hintView = (TextView)addToDictionaryStrip.findViewById(
+                R.id.hint_add_to_dictionary);
         hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
         hintView.setTextColor(mColorAutoCorrect);
-        final int hintWidth = width - wordWidth - leftArrowView.getWidth();
-        final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
-        hintView.setText(hintText);
+        final int hintWidth = width - wordWidth;
+        final String hintWithArrow = LEFTWARDS_ARROW + hintText;
+        final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint());
+        hintView.setText(hintWithArrow);
         hintView.setTextScaleX(hintScaleX);
-        stripView.addView(hintView);
         setLayoutWeight(
                 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
-
-        wordView.setOnClickListener(listener);
-        leftArrowView.setOnClickListener(listener);
-        hintView.setOnClickListener(listener);
-    }
-
-    public String getAddToDictionaryWord() {
-        // String tag is set at
-        // {@link #layoutAddToDictionaryHint(String,ViewGroup,int,CharSequence,OnClickListener}.
-        return (String)mWordToSaveView.getTag();
-    }
-
-    public boolean isAddToDictionaryShowing(final View v) {
-        return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
     }
 
     private static void setLayoutWeight(final View v, final float weight, final int height) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 88b57f9..32552eb 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -56,6 +56,7 @@
     static final boolean DBG = LatinImeLogger.sDBG;
 
     private final ViewGroup mSuggestionsStrip;
+    private final ViewGroup mAddToDictionaryStrip;
     MainKeyboardView mMainKeyboardView;
 
     private final View mMoreSuggestionsContainer;
@@ -70,6 +71,32 @@
     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
 
     private final SuggestionStripLayoutHelper mLayoutHelper;
+    private final StripVisibilityGroup mStripVisibilityGroup;
+
+    private static class StripVisibilityGroup {
+        private final View mSuggestionsStrip;
+        private final View mAddToDictionaryStrip;
+
+        public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip) {
+            mSuggestionsStrip = suggestionsStrip;
+            mAddToDictionaryStrip = addToDictionaryStrip;
+            showSuggestionsStrip();
+        }
+
+        public void showSuggestionsStrip() {
+            mSuggestionsStrip.setVisibility(VISIBLE);
+            mAddToDictionaryStrip.setVisibility(INVISIBLE);
+        }
+
+        public void showAddToDictionaryStrip() {
+            mSuggestionsStrip.setVisibility(INVISIBLE);
+            mAddToDictionaryStrip.setVisibility(VISIBLE);
+        }
+
+        public boolean isShowingAddToDictionaryStrip() {
+            return mAddToDictionaryStrip.getVisibility() == VISIBLE;
+        }
+    }
 
     /**
      * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
@@ -88,6 +115,9 @@
         inflater.inflate(R.layout.suggestions_strip, this);
 
         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
+        mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip);
+        mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip);
+
         for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) {
             final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
             word.setOnClickListener(this);
@@ -137,14 +167,16 @@
     }
 
     public boolean isShowingAddToDictionaryHint() {
-        return mSuggestionsStrip.getChildCount() > 0
-                && mLayoutHelper.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
+        return mStripVisibilityGroup.isShowingAddToDictionaryStrip();
     }
 
     public void showAddToDictionaryHint(final String word, final CharSequence hintText) {
-        clear();
-        mLayoutHelper.layoutAddToDictionaryHint(
-                word, mSuggestionsStrip, getWidth(), hintText, this);
+        mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, getWidth(), hintText);
+        // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
+        // will be extracted at {@link #onClick(View)}.
+        mAddToDictionaryStrip.setTag(word);
+        mAddToDictionaryStrip.setOnClickListener(this);
+        mStripVisibilityGroup.showAddToDictionaryStrip();
     }
 
     public boolean dismissAddToDictionaryHint() {
@@ -157,8 +189,7 @@
 
     public void clear() {
         mSuggestionsStrip.removeAllViews();
-        removeAllViews();
-        addView(mSuggestionsStrip);
+        mStripVisibilityGroup.showSuggestionsStrip();
         dismissMoreSuggestionsPanel();
     }
 
@@ -302,26 +333,26 @@
 
     @Override
     public void onClick(final View view) {
-        if (mLayoutHelper.isAddToDictionaryShowing(view)) {
-            mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord());
+        final Object tag = view.getTag();
+        // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}.
+        if (tag instanceof String) {
+            final String wordToSave = (String)tag;
+            mListener.addWordToUserDictionary(wordToSave);
             clear();
             return;
         }
 
-        final Object tag = view.getTag();
-        // Integer tag is set at
+        // {@link Integer} tag is set at
         // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
         // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
-        if (!(tag instanceof Integer)) {
-            return;
+        if (tag instanceof Integer) {
+            final int index = (Integer) tag;
+            if (index >= mSuggestedWords.size()) {
+                return;
+            }
+            final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
+            mListener.pickSuggestionManually(index, wordInfo);
         }
-        final int index = (Integer) tag;
-        if (index >= mSuggestedWords.size()) {
-            return;
-        }
-
-        final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
-        mListener.pickSuggestionManually(index, wordInfo);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 6f15b11..b154623 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -348,7 +348,7 @@
         boolean hasPeriod = false;
         int codePoint = 0;
         while (i > 0) {
-            codePoint =  Character.codePointBefore(text, i);
+            codePoint = Character.codePointBefore(text, i);
             if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
                 // Handwavy heuristic to see if that's a URL character. Anything between period
                 // and z. This includes all lower- and upper-case ascii letters, period,
@@ -387,6 +387,48 @@
         return false;
     }
 
+    /**
+     * Examines the string and returns whether we're inside a double quote.
+     *
+     * This is used to decide whether we should put an automatic space before or after a double
+     * quote character. If we're inside a quotation, then we want to close it, so we want a space
+     * after and not before. Otherwise, we want to open the quotation, so we want a space before
+     * and not after. Exception: after a digit, we never want a space because the "inch" or
+     * "minutes" use cases is dominant after digits.
+     * In the practice, we determine whether we are in a quotation or not by finding the previous
+     * double quote character, and looking at whether it's followed by whitespace. If so, that
+     * was a closing quotation mark, so we're not inside a double quote. If it's not followed
+     * by whitespace, then it was an opening quotation mark, and we're inside a quotation.
+     *
+     * @param text the text to examine.
+     * @return whether we're inside a double quote.
+     */
+    public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) {
+        int i = text.length();
+        if (0 == i) return false;
+        int codePoint = Character.codePointBefore(text, i);
+        if (Character.isDigit(codePoint)) return true;
+        int prevCodePoint = 0;
+        while (i > 0) {
+            codePoint = Character.codePointBefore(text, i);
+            if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
+                // If we see a double quote followed by whitespace, then that
+                // was a closing quote.
+                if (Character.isWhitespace(prevCodePoint)) return false;
+            }
+            if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) {
+                // If we see a double quote preceded by whitespace, then that
+                // was an opening quote. No need to continue seeking.
+                return true;
+            }
+            i -= Character.charCount(codePoint);
+            prevCodePoint = codePoint;
+        }
+        // We reached the start of text. If the first char is a double quote, then we're inside
+        // a double quote. Otherwise we're not.
+        return Constants.CODE_DOUBLE_QUOTE == codePoint;
+    }
+
     public static boolean isEmptyStringOrWhiteSpaces(final String s) {
         final int N = codePointCount(s);
         for (int i = 0; i < N; ++i) {
diff --git a/tests/src/com/android/inputmethod/latin/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index d5c06e2..556af09 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -169,4 +169,32 @@
                 + " ; Suggestions = " + mLatinIME.getSuggestedWords(),
                 EXPECTED_RESULT, mEditText.getText().toString());
     }
+
+    public void testAutoSpaceWithDoubleQuotes() {
+        final String STRING_TO_TYPE = "He said\"hello\"to me. I replied,\"hi\"."
+                + "Then, 5\"passed. He said\"bye\"and left.";
+        final String EXPECTED_RESULT = "He said \"hello\" to me. I replied, \"hi\". "
+                + "Then, 5\" passed. He said \"bye\" and left. \"";
+        // Split by double quote, so that we can type the double quotes individually.
+        for (final String partToType : STRING_TO_TYPE.split("\"")) {
+            // Split at word boundaries. This regexp means "anywhere that is preceded
+            // by a word character but not followed by a word character, OR that is not
+            // preceded by a word character but followed by a word character".
+            // We need to input word by word because auto-spaces are only active when
+            // manually picking or gesturing (which we can't simulate yet), but only words
+            // can be picked.
+            final String[] wordsToType = partToType.split("(?<=\\w)(?!\\w)|(?<!\\w)(?=\\w)");
+            for (final String wordToType : wordsToType) {
+                type(wordToType);
+                if (wordToType.matches("^\\w+$")) {
+                    // Only pick selection if that was a word, because if that was not a word,
+                    // then we don't have a composition.
+                    pickSuggestionManually(0, wordToType);
+                }
+            }
+            type("\"");
+        }
+        assertEquals("auto-space with double quotes",
+                EXPECTED_RESULT, mEditText.getText().toString());
+    }
 }