am b4b3e80f: Merge "Fix empty custom input style entry appears after orientation change" into jb-dev

* commit 'b4b3e80f1124eaefc4218fbd03a2af2dc4fece17':
  Fix empty custom input style entry appears after orientation change
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
index 93106ac..be4cb77 100644
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ b/java/src/com/android/inputmethod/latin/EditingUtils.java
@@ -54,7 +54,7 @@
      */
     public static String getWordAtCursor(InputConnection connection, String separators) {
         // getWordRangeAtCursor returns null if the connection is null
-        Range r = getWordRangeAtCursor(connection, separators);
+        Range r = getWordRangeAtCursor(connection, separators, 0);
         return (r == null) ? null : r.mWord;
     }
 
@@ -84,7 +84,17 @@
         }
     }
 
-    private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
+    /**
+     * Returns the text surrounding the cursor.
+     *
+     * @param connection the InputConnection to the TextView
+     * @param sep a string of characters that split words.
+     * @param additionalPrecedingWordsCount the number of words before the current word that should
+     *   be included in the returned range
+     * @return a range containing the text surrounding the cursor
+     */
+    public static Range getWordRangeAtCursor(InputConnection connection, String sep,
+            int additionalPrecedingWordsCount) {
         if (connection == null || sep == null) {
             return null;
         }
@@ -94,14 +104,40 @@
             return null;
         }
 
-        // Find first word separator before the cursor
+        // Going backward, alternate skipping non-separators and separators until enough words
+        // have been read.
         int start = before.length();
-        while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
+        boolean isStoppingAtWhitespace = true;  // toggles to indicate what to stop at
+        while (true) { // see comments below for why this is guaranteed to halt
+            while (start > 0) {
+                final int codePoint = Character.codePointBefore(before, start);
+                if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+                    break;  // inner loop
+                }
+                --start;
+                if (Character.isSupplementaryCodePoint(codePoint)) {
+                    --start;
+                }
+            }
+            // isStoppingAtWhitespace is true every other time through the loop,
+            // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+            // guarantees outer loop termination
+            if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+                break;  // outer loop
+            }
+            isStoppingAtWhitespace = !isStoppingAtWhitespace;
+        }
 
         // Find last word separator after the cursor
         int end = -1;
-        while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
-            // Nothing to do here.
+        while (++end < after.length()) {
+            final int codePoint = Character.codePointAt(after, end);
+            if (isSeparator(codePoint, sep)) {
+                break;
+            }
+            if (Character.isSupplementaryCodePoint(codePoint)) {
+                ++end;
+            }
         }
 
         int cursor = getCursorPosition(connection);
@@ -114,8 +150,8 @@
         return null;
     }
 
-    private static boolean isWhitespace(int code, String whitespace) {
-        return whitespace.contains(String.valueOf((char) code));
+    private static boolean isSeparator(int code, String sep) {
+        return sep.indexOf(code) != -1;
     }
 
     private static final Pattern spaceRegex = Pattern.compile("\\s+");
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index b59e939..28dcb1e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -711,6 +711,10 @@
 
     @Override
     public void onWindowHidden() {
+        if (ProductionFlag.IS_EXPERIMENTAL) {
+            ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+                    getCurrentInputConnection());
+        }
         super.onWindowHidden();
         KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
         if (inputView != null) inputView.closing();
@@ -741,7 +745,6 @@
             int composingSpanStart, int composingSpanEnd) {
         super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
                 composingSpanStart, composingSpanEnd);
-
         if (DEBUG) {
             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
@@ -753,9 +756,15 @@
                     + ", ce=" + composingSpanEnd);
         }
         if (ProductionFlag.IS_EXPERIMENTAL) {
+            final boolean expectingUpdateSelectionFromLogger =
+                    ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
             ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
-                    composingSpanEnd);
+                    composingSpanEnd, mExpectingUpdateSelection,
+                    expectingUpdateSelectionFromLogger, getCurrentInputConnection());
+            if (expectingUpdateSelectionFromLogger) {
+                return;
+            }
         }
 
         // TODO: refactor the following code to be less contrived.
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
index 566af70..a46ed03 100644
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
 import android.os.Handler;
@@ -29,11 +30,13 @@
 import android.view.MotionEvent;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.internal.KeyboardState;
+import com.android.inputmethod.latin.EditingUtils.Range;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.BufferedWriter;
@@ -47,6 +50,7 @@
 import java.nio.channels.FileChannel;
 import java.nio.charset.Charset;
 import java.util.Map;
+import java.util.UUID;
 
 /**
  * Logs the use of the LatinIME keyboard.
@@ -59,13 +63,20 @@
 public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String TAG = ResearchLogger.class.getSimpleName();
     private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
     private static final boolean DEBUG = false;
+    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
 
     private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
+    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
     public static boolean sIsLogging = false;
     /* package */ final Handler mLoggingHandler;
     private InputMethodService mIms;
 
+    // set when LatinIME should ignore a onUpdateSelection() callback that
+    // arises from operations in this class
+    private static boolean mLatinIMEExpectingUpdateSelection = false;
+
     /**
      * Isolates management of files. This variable should never be null, but can be changed
      * to support testing.
@@ -336,6 +347,7 @@
         private static final boolean LATINIME_DELETESURROUNDINGTEXT_ENABLED = DEFAULT_ENABLED;
         private static final boolean LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED = DEFAULT_ENABLED;
         private static final boolean LATINIME_ONDISPLAYCOMPLETIONS_ENABLED = DEFAULT_ENABLED;
+        private static final boolean LATINIME_ONWINDOWHIDDEN_ENABLED = DEFAULT_ENABLED;
         private static final boolean LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED = DEFAULT_ENABLED;
         private static final boolean LATINIME_ONUPDATESELECTION_ENABLED = DEFAULT_ENABLED;
         private static final boolean LATINIME_PERFORMEDITORACTION_ENABLED = DEFAULT_ENABLED;
@@ -528,11 +540,51 @@
         }
     }
 
+    /* package */ static boolean getAndClearLatinIMEExpectingUpdateSelection() {
+        boolean returnValue = mLatinIMEExpectingUpdateSelection;
+        mLatinIMEExpectingUpdateSelection = false;
+        return returnValue;
+    }
+
+    public static void latinIME_onWindowHidden(final int savedSelectionStart,
+            final int savedSelectionEnd, final InputConnection ic) {
+        if (UnsLogGroup.LATINIME_ONWINDOWHIDDEN_ENABLED) {
+            if (ic != null) {
+                ic.beginBatchEdit();
+                ic.performContextMenuAction(android.R.id.selectAll);
+                CharSequence charSequence = ic.getSelectedText(0);
+                ic.setSelection(savedSelectionStart, savedSelectionEnd);
+                ic.endBatchEdit();
+                mLatinIMEExpectingUpdateSelection = true;
+                if (TextUtils.isEmpty(charSequence)) {
+                    logUnstructured("LatinIME_onWindowHidden", "<no text>");
+                } else {
+                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+                        // do not cut in the middle of a supplementary character
+                        final char c = charSequence.charAt(length-1);
+                        if (Character.isHighSurrogate(c)) {
+                            length--;
+                        }
+                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+                                length);
+                        logUnstructured("LatinIME_onWindowHidden", truncatedCharSequence.toString()
+                                + "<truncated>");
+                    } else {
+                        logUnstructured("LatinIME_onWindowHidden", charSequence.toString());
+                    }
+                }
+            }
+        }
+    }
+
     public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
             final SharedPreferences prefs) {
         if (UnsLogGroup.LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED) {
             final StringBuilder builder = new StringBuilder();
             builder.append("onStartInputView: editorInfo:");
+            builder.append("\tpackageName=");
+            builder.append(editorInfo.packageName);
             builder.append("\tinputType=");
             builder.append(Integer.toHexString(editorInfo.inputType));
             builder.append("\timeOptions=");
@@ -544,14 +596,28 @@
                 Object value = entry.getValue();
                 builder.append("=" + ((value == null) ? "<null>" : value.toString()));
             }
+            builder.append("\tuuid="); builder.append(getUUID(prefs));
             logUnstructured("LatinIME_onStartInputViewInternal", builder.toString());
         }
     }
 
+    private static String getUUID(final SharedPreferences prefs) {
+        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
+        if (null == uuidString) {
+            UUID uuid = UUID.randomUUID();
+            uuidString = uuid.toString();
+            Editor editor = prefs.edit();
+            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
+            editor.apply();
+        }
+        return uuidString;
+    }
+
     public static void latinIME_onUpdateSelection(final int lastSelectionStart,
             final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
             final int newSelStart, final int newSelEnd, final int composingSpanStart,
-            final int composingSpanEnd) {
+            final int composingSpanEnd, final boolean expectingUpdateSelection,
+            final boolean expectingUpdateSelectionFromLogger, final InputConnection connection) {
         if (UnsLogGroup.LATINIME_ONUPDATESELECTION_ENABLED) {
             final String s = "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
@@ -560,7 +626,11 @@
                     + ", nss=" + newSelStart
                     + ", nse=" + newSelEnd
                     + ", cs=" + composingSpanStart
-                    + ", ce=" + composingSpanEnd;
+                    + ", ce=" + composingSpanEnd
+                    + ", eus=" + expectingUpdateSelection
+                    + ", eusfl=" + expectingUpdateSelectionFromLogger
+                    + ", context=\"" + EditingUtils.getWordRangeAtCursor(connection,
+                            WHITESPACE_SEPARATORS, 1).mWord + "\"";
             logUnstructured("LatinIME_onUpdateSelection", s);
         }
     }
@@ -754,4 +824,4 @@
             logUnstructured("SuggestionsView_setSuggestions", mSuggestedWords.toString());
         }
     }
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/inputmethod/latin/EditingUtilsTests.java b/tests/src/com/android/inputmethod/latin/EditingUtilsTests.java
new file mode 100644
index 0000000..c73f889
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/EditingUtilsTests.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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 android.test.AndroidTestCase;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputConnectionWrapper;
+
+import com.android.inputmethod.latin.EditingUtils.Range;
+
+public class EditingUtilsTests extends AndroidTestCase {
+
+    // The following is meant to be a reasonable default for
+    // the "word_separators" resource.
+    private static final String sSeparators = ".,:;!?-";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    private class MockConnection extends InputConnectionWrapper {
+        final String mTextBefore;
+        final String mTextAfter;
+        final ExtractedText mExtractedText;
+
+        public MockConnection(String textBefore, String textAfter, ExtractedText extractedText) {
+            super(null, false);
+            mTextBefore = textBefore;
+            mTextAfter = textAfter;
+            mExtractedText = extractedText;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextBeforeCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextBeforeCursor(int n, int flags) {
+            return mTextBefore;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getTextAfterCursor(int, int)
+         */
+        @Override
+        public CharSequence getTextAfterCursor(int n, int flags) {
+            return mTextAfter;
+        }
+
+        /* (non-Javadoc)
+         * @see android.view.inputmethod.InputConnectionWrapper#getExtractedText(ExtractedTextRequest, int)
+         */
+        @Override
+        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+            return mExtractedText;
+        }
+    }
+
+    /************************** Tests ************************/
+
+    /**
+     * Test for getting previous word (for bigram suggestions)
+     */
+    public void testGetPreviousWord() {
+        // If one of the following cases breaks, the bigram suggestions won't work.
+        assertEquals(EditingUtils.getPreviousWord("abc def", sSeparators), "abc");
+        assertNull(EditingUtils.getPreviousWord("abc", sSeparators));
+        assertNull(EditingUtils.getPreviousWord("abc. def", sSeparators));
+
+        // The following tests reflect the current behavior of the function
+        // EditingUtils#getPreviousWord.
+        // TODO: However at this time, the code does never go
+        // into such a path, so it should be safe to change the behavior of
+        // this function if needed - especially since it does not seem very
+        // logical. These tests are just there to catch any unintentional
+        // changes in the behavior of the EditingUtils#getPreviousWord method.
+        assertEquals(EditingUtils.getPreviousWord("abc def ", sSeparators), "abc");
+        assertEquals(EditingUtils.getPreviousWord("abc def.", sSeparators), "abc");
+        assertEquals(EditingUtils.getPreviousWord("abc def .", sSeparators), "def");
+        assertNull(EditingUtils.getPreviousWord("abc ", sSeparators));
+    }
+
+    /**
+     * Test for getting the word before the cursor (for bigram)
+     */
+    public void testGetThisWord() {
+        assertEquals(EditingUtils.getThisWord("abc def", sSeparators), "def");
+        assertEquals(EditingUtils.getThisWord("abc def ", sSeparators), "def");
+        assertNull(EditingUtils.getThisWord("abc def.", sSeparators));
+        assertNull(EditingUtils.getThisWord("abc def .", sSeparators));
+    }
+
+    /**
+     * Test logic in getting the word range at the cursor.
+     */
+    public void testGetWordRangeAtCursor() {
+        ExtractedText et = new ExtractedText();
+        InputConnection mockConnection;
+        mockConnection = new MockConnection("word wo", "rd", et);
+        et.startOffset = 0;
+        et.selectionStart = 7;
+        Range r;
+
+        // basic case
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, " ", 0);
+        assertEquals("word", r.mWord);
+        r = null;
+
+        // more than one word
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, " ", 1);
+        assertEquals("word word", r.mWord);
+        r = null;
+
+        // tab character instead of space
+        mockConnection = new MockConnection("one\tword\two", "rd", et);
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, "\t", 1);
+        assertEquals("word\tword", r.mWord);
+        r = null;
+
+        // only one word doesn't go too far
+        mockConnection = new MockConnection("one\tword\two", "rd", et);
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, "\t", 1);
+        assertEquals("word\tword", r.mWord);
+        r = null;
+
+        // tab or space
+        mockConnection = new MockConnection("one word\two", "rd", et);
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, " \t", 1);
+        assertEquals("word\tword", r.mWord);
+        r = null;
+
+        // tab or space multiword
+        mockConnection = new MockConnection("one word\two", "rd", et);
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, " \t", 2);
+        assertEquals("one word\tword", r.mWord);
+        r = null;
+
+        // splitting on supplementary character
+        final String supplementaryChar = "\uD840\uDC8A";
+        mockConnection = new MockConnection("one word" + supplementaryChar + "wo", "rd", et);
+        r = EditingUtils.getWordRangeAtCursor(mockConnection, supplementaryChar, 0);
+        assertEquals("word", r.mWord);
+        r = null;
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/UtilsTests.java b/tests/src/com/android/inputmethod/latin/UtilsTests.java
deleted file mode 100644
index 2ef4e2f..0000000
--- a/tests/src/com/android/inputmethod/latin/UtilsTests.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2010,2011 The Android Open Source Project
- *
- * 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 android.test.AndroidTestCase;
-
-public class UtilsTests extends AndroidTestCase {
-
-    // The following is meant to be a reasonable default for
-    // the "word_separators" resource.
-    private static final String sSeparators = ".,:;!?-";
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-    }
-
-    /************************** Tests ************************/
-
-    /**
-     * Test for getting previous word (for bigram suggestions)
-     */
-    public void testGetPreviousWord() {
-        // If one of the following cases breaks, the bigram suggestions won't work.
-        assertEquals(EditingUtils.getPreviousWord("abc def", sSeparators), "abc");
-        assertNull(EditingUtils.getPreviousWord("abc", sSeparators));
-        assertNull(EditingUtils.getPreviousWord("abc. def", sSeparators));
-
-        // The following tests reflect the current behavior of the function
-        // EditingUtils#getPreviousWord.
-        // TODO: However at this time, the code does never go
-        // into such a path, so it should be safe to change the behavior of
-        // this function if needed - especially since it does not seem very
-        // logical. These tests are just there to catch any unintentional
-        // changes in the behavior of the EditingUtils#getPreviousWord method.
-        assertEquals(EditingUtils.getPreviousWord("abc def ", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def.", sSeparators), "abc");
-        assertEquals(EditingUtils.getPreviousWord("abc def .", sSeparators), "def");
-        assertNull(EditingUtils.getPreviousWord("abc ", sSeparators));
-    }
-
-    /**
-     * Test for getting the word before the cursor (for bigram)
-     */
-    public void testGetThisWord() {
-        assertEquals(EditingUtils.getThisWord("abc def", sSeparators), "def");
-        assertEquals(EditingUtils.getThisWord("abc def ", sSeparators), "def");
-        assertNull(EditingUtils.getThisWord("abc def.", sSeparators));
-        assertNull(EditingUtils.getThisWord("abc def .", sSeparators));
-    }
-}