Merge "Change native functions' interface for gesture"
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
index cf3cc78..df88929 100644
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -197,6 +197,7 @@
         Log.d(TAG, "stop called");
         if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
             mLoggingState = LOGGING_STATE_STOPPING;
+            flushEventQueue(true);
             // put this in the Handler queue so pending writes are processed first.
             mLoggingHandler.post(new Runnable() {
                 @Override
@@ -379,11 +380,52 @@
         mCurrentLogUnit.addLogAtom(keys, values, false);
     }
 
+    // Used to track how often words are logged.  Too-frequent logging can leak
+    // semantics, disclosing private data.
+    /* package for test */ static class LoggingFrequencyState {
+        private static final int DEFAULT_WORD_LOG_FREQUENCY = 10;
+        private int mWordsRemainingToSkip;
+        private final int mFrequency;
+
+        /**
+         * Tracks how often words may be uploaded.
+         *
+         * @param frequency 1=Every word, 2=Every other word, etc.
+         */
+        public LoggingFrequencyState(int frequency) {
+            mFrequency = frequency;
+            mWordsRemainingToSkip = mFrequency;
+        }
+
+        public void onWordLogged() {
+            mWordsRemainingToSkip = mFrequency;
+        }
+
+        public void onWordNotLogged() {
+            if (mWordsRemainingToSkip > 1) {
+                mWordsRemainingToSkip--;
+            }
+        }
+
+        public boolean isSafeToLog() {
+            return mWordsRemainingToSkip <= 1;
+        }
+    }
+
+    /* package for test */ LoggingFrequencyState mLoggingFrequencyState =
+            new LoggingFrequencyState(LoggingFrequencyState.DEFAULT_WORD_LOG_FREQUENCY);
+
     /* package for test */ boolean isPrivacyThreat(String word) {
-        // currently: word not in dictionary or contains numbers.
+        // Current checks:
+        // - Word not in dictionary
+        // - Word contains numbers
+        // - Privacy-safe word not logged recently
         if (TextUtils.isEmpty(word)) {
             return false;
         }
+        if (!mLoggingFrequencyState.isSafeToLog()) {
+            return true;
+        }
         final int length = word.length();
         boolean hasLetter = false;
         for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
@@ -410,15 +452,26 @@
         return false;
     }
 
+    private void onWordComplete(String word) {
+        final boolean isPrivacyThreat = isPrivacyThreat(word);
+        flushEventQueue(isPrivacyThreat);
+        if (isPrivacyThreat) {
+            mLoggingFrequencyState.onWordNotLogged();
+        } else {
+            mLoggingFrequencyState.onWordLogged();
+        }
+    }
+
     /**
      * Write out enqueued LogEvents to the log, possibly dropping privacy sensitive events.
      */
-    /* package for test */ synchronized void flushQueue(boolean removePotentiallyPrivateEvents) {
+    /* package for test */ synchronized void flushEventQueue(
+            boolean removePotentiallyPrivateEvents) {
         if (isAllowedToLog()) {
             mCurrentLogUnit.setRemovePotentiallyPrivateEvents(removePotentiallyPrivateEvents);
             mLoggingHandler.post(mCurrentLogUnit);
-            mCurrentLogUnit = new LogUnit();
         }
+        mCurrentLogUnit = new LogUnit();
     }
 
     private synchronized void outputEvent(final String[] keys, final Object[] values) {
@@ -652,7 +705,6 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueuePotentiallyPrivateEvent(
                 EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values);
-        researchLogger.flushQueue(researchLogger.isPrivacyThreat(autoCorrection));
     }
 
     private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = {
@@ -665,7 +717,7 @@
         };
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values);
-        researchLogger.flushQueue(researchLogger.isPrivacyThreat(scrubbedWord));
+        researchLogger.onWordComplete(scrubbedWord);
     }
 
     private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = {
@@ -743,7 +795,7 @@
             }
             final ResearchLogger researchLogger = getInstance();
             researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
-            researchLogger.flushQueue(true); // Play it safe.  Remove privacy-sensitive events.
+            researchLogger.flushEventQueue(true); // Play it safe.  Remove privacy-sensitive events.
         }
     }
 
@@ -824,7 +876,6 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueuePotentiallyPrivateEvent(
                 EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
-        researchLogger.flushQueue(researchLogger.isPrivacyThreat(cs.toString()));
     }
 
     private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
@@ -839,7 +890,6 @@
         final ResearchLogger researchLogger = getInstance();
         researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
                 values);
-        researchLogger.flushQueue(researchLogger.isPrivacyThreat(suggestion.toString()));
     }
 
     private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index a173d79..bde3a84 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -26,6 +26,8 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
@@ -384,6 +386,21 @@
         return suggestionsList;
     }
 
+    private static class SuggestedWordInfoComparator implements Comparator<SuggestedWordInfo> {
+        // This comparator ranks the word info with the higher frequency first. That's because
+        // that's the order we want our elements in.
+        @Override
+        public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) {
+            if (o1.mScore > o2.mScore) return -1;
+            if (o1.mScore < o2.mScore) return 1;
+            if (o1.mCodePointCount < o2.mCodePointCount) return -1;
+            if (o1.mCodePointCount > o2.mCodePointCount) return 1;
+            return o1.mWord.toString().compareTo(o2.mWord.toString());
+        }
+    }
+    private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator =
+            new SuggestedWordInfoComparator();
+
     public boolean addWord(final SuggestedWordInfo wordInfo,
             final int dicTypeId, final int dataType,
             final ArrayList<SuggestedWordInfo> suggestions, final String consideredWord) {
@@ -394,35 +411,11 @@
         final int score = wordInfo.mScore;
         int pos = 0;
 
-        // Check if it's the same word, only caps are different
-        if (StringUtils.equalsIgnoreCase(consideredWord, word)) {
-            // TODO: remove this surrounding if clause and move this logic to
-            // getSuggestedWordBuilder.
-            if (suggestions.size() > 0) {
-                final SuggestedWordInfo currentHighestWord = suggestions.get(0);
-                // If the current highest word is also equal to typed word, we need to compare
-                // frequency to determine the insertion position. This does not ensure strictly
-                // correct ordering, but ensures the top score is on top which is enough for
-                // removing duplicates correctly.
-                if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word)
-                        && score <= currentHighestWord.mScore) {
-                    pos = 1;
-                }
-            }
-        } else {
-            // Check the last one's score and bail
-            if (suggestions.size() >= prefMaxSuggestions
-                    && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
-            final int length = wordInfo.mCodePointCount;
-            while (pos < suggestions.size()) {
-                final int curScore = suggestions.get(pos).mScore;
-                if (curScore < score
-                        || (curScore == score && length < suggestions.get(pos).mCodePointCount)) {
-                    break;
-                }
-                pos++;
-            }
-        }
+        final int index =
+                Collections.binarySearch(suggestions, wordInfo, sSuggestedWordInfoComparator);
+        // binarySearch returns the index of an equal word info if found. If not found
+        // it returns -insertionPoint - 1. We want the insertion point, so:
+        pos = index >= 0 ? index : -index - 1;
         if (pos >= prefMaxSuggestions) {
             return true;
         }