Add aggressive cancellation for auto suggestion

- Add ring buffer
- Count separator for auto suggestion
- Add a test for ring buffer

Change-Id: Id4a0aa00ceb1b055b8fc96c45e100d318cceb2ab
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 10be866..b4a5ab6 100755
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -1,7 +1,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.google.android.inputmethod.latin"
         android:versionCode="9"
-        android:versionName="0.12">
+        android:versionName="0.14">
 
     <original-package android:name="com.android.inputmethod.latin" />
 
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index b15de6b..35edb8a 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -219,7 +219,7 @@
     private final float FX_VOLUME = -1.0f;
     private boolean mSilentMode;
 
-    private String mWordSeparators;
+    /* package */ String mWordSeparators;
     private String mSentenceSeparators;
     private VoiceInput mVoiceInput;
     private VoiceResults mVoiceResults = new VoiceResults();
@@ -955,7 +955,7 @@
             case Keyboard.KEYCODE_DELETE:
                 handleBackspace();
                 mDeleteCount++;
-                LatinImeLogger.logOnDelete(1);
+                LatinImeLogger.logOnDelete();
                 break;
             case Keyboard.KEYCODE_SHIFT:
                 handleShift();
@@ -996,12 +996,12 @@
                 if (primaryCode != KEYCODE_ENTER) {
                     mJustAddedAutoSpace = false;
                 }
+                LatinImeLogger.logOnInputChar((char)primaryCode);
                 if (isWordSeparator(primaryCode)) {
                     handleSeparator(primaryCode);
                 } else {
                     handleCharacter(primaryCode, keyCodes);
                 }
-                LatinImeLogger.logOnInputChar(1);
                 // Cancel the just reverted state
                 mJustRevertedSeparator = null;
         }
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index b497c0c..736b0af 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -33,7 +33,8 @@
 
 public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
     private static final String TAG = "LatinIMELogs";
-    private static boolean sDBG = false;
+    private static final boolean DBG = true;
+    private static boolean sLOGPRINT = false;
     // SUPPRESS_EXCEPTION should be true when released to public.
     private static final boolean SUPPRESS_EXCEPTION = false;
     // DEFAULT_LOG_ENABLED should be false when released to public.
@@ -43,6 +44,7 @@
     private static final long MINIMUMCOUNTINTERVAL = 20 * DateUtils.SECOND_IN_MILLIS; // 20 sec
     private static final long MINIMUMSENDSIZE = 40;
     private static final char SEPARATER = ';';
+    private static final char NULL_CHAR = '\uFFFC';
     private static final int ID_CLICKSUGGESTION = 0;
     private static final int ID_AUTOSUGGESTIONCANCELLED = 1;
     private static final int ID_AUTOSUGGESTION = 2;
@@ -60,14 +62,17 @@
     private static final String PREF_AUTO_COMPLETE = "auto_complete";
 
     public static boolean sLogEnabled = true;
-    private static LatinImeLogger sLatinImeLogger = new LatinImeLogger();
+    /* package */ static LatinImeLogger sLatinImeLogger = new LatinImeLogger();
     // Store the last auto suggested word.
     // This is required for a cancellation log of auto suggestion of that word.
-    private static String sLastAutoSuggestBefore;
-    private static String sLastAutoSuggestAfter;
+    /* package */ static String sLastAutoSuggestBefore;
+    /* package */ static String sLastAutoSuggestAfter;
+    /* package */ static String sLastAutoSuggestSeparator;
 
     private ArrayList<LogEntry> mLogBuffer = null;
     private ArrayList<LogEntry> mPrivacyLogBuffer = null;
+    /* package */ RingCharBuffer mRingCharBuffer = null;
+
     private Context mContext = null;
     private DropBoxManager mDropBox = null;
     private long mLastTimeActive;
@@ -116,11 +121,12 @@
         mActualCharCount = 0;
         mLogBuffer = new ArrayList<LogEntry>();
         mPrivacyLogBuffer = new ArrayList<LogEntry>();
+        mRingCharBuffer = new RingCharBuffer(context);
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
         sLogEnabled = prefs.getBoolean(PREF_ENABLE_LOG, DEFAULT_LOG_ENABLED);
         mThemeId = prefs.getString(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT,
                 KeyboardSwitcher.DEFAULT_LAYOUT_ID);
-        sDBG = prefs.getBoolean(PREF_DEBUG_MODE, sDBG);
+        sLOGPRINT = prefs.getBoolean(PREF_DEBUG_MODE, sLOGPRINT);
         prefs.registerOnSharedPreferenceChangeListener(this);
     }
 
@@ -134,13 +140,14 @@
         mActualCharCount = 0;
         mLogBuffer.clear();
         mPrivacyLogBuffer.clear();
+        mRingCharBuffer.reset();
     }
 
     /**
      * Check if the input string is safe as an entry or not.
      */
     private static boolean checkStringDataSafe(String s) {
-        if (sDBG) {
+        if (DBG) {
             Log.d(TAG, "Check String safety: " + s);
         }
         for (int i = 0; i < s.length(); ++i) {
@@ -152,7 +159,7 @@
     }
 
     private void addCountEntry(long time) {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Log counts. (4)");
         }
         mLogBuffer.add(new LogEntry (time, ID_DELETE_COUNT,
@@ -171,7 +178,7 @@
     }
 
     private void addThemeIdEntry(long time) {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Log theme Id. (1)");
         }
         mLogBuffer.add(new LogEntry (time, ID_THEME_ID,
@@ -179,7 +186,7 @@
     }
 
     private void addSettingsEntry(long time) {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Log settings. (1)");
         }
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
@@ -189,7 +196,7 @@
     }
 
     private void addVersionNameEntry(long time) {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Log Version. (1)");
         }
         try {
@@ -203,15 +210,15 @@
     }
 
     private void addExceptionEntry(long time, String[] data) {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Log Exception. (1)");
         }
         mLogBuffer.add(new LogEntry(time, ID_EXCEPTION, data));
     }
 
     private void flushPrivacyLogSafely() {
-        if (sDBG) {
-            Log.d(TAG, "Log theme Id. (" + mPrivacyLogBuffer.size() + ")");
+        if (sLOGPRINT) {
+            Log.d(TAG, "Log obfuscated data. (" + mPrivacyLogBuffer.size() + ")");
         }
         long now = System.currentTimeMillis();
         Collections.sort(mPrivacyLogBuffer);
@@ -248,7 +255,7 @@
                 ++mWordCount;
                 String[] dataStrings = (String[]) data;
                 if (dataStrings.length < 2) {
-                    if (sDBG) {
+                    if (DBG) {
                         Log.e(TAG, "The length of logged string array is invalid.");
                     }
                     break;
@@ -258,7 +265,7 @@
                     mPrivacyLogBuffer.add(
                             new LogEntry (System.currentTimeMillis(), tag, dataStrings));
                 } else {
-                    if (sDBG) {
+                    if (DBG) {
                         Log.d(TAG, "Skipped to add an entry because data is unsafe.");
                     }
                 }
@@ -267,7 +274,7 @@
                 --mWordCount;
                 dataStrings = (String[]) data;
                 if (dataStrings.length < 2) {
-                    if (sDBG) {
+                    if (DBG) {
                         Log.e(TAG, "The length of logged string array is invalid.");
                     }
                     break;
@@ -277,7 +284,7 @@
                     mPrivacyLogBuffer.add(
                             new LogEntry (System.currentTimeMillis(), tag, dataStrings));
                 } else {
-                    if (sDBG) {
+                    if (DBG) {
                         Log.d(TAG, "Skipped to add an entry because data is unsafe.");
                     }
                 }
@@ -285,7 +292,7 @@
             case ID_EXCEPTION:
                 dataStrings = (String[]) data;
                 if (dataStrings.length < 2) {
-                    if (sDBG) {
+                    if (DBG) {
                         Log.e(TAG, "The length of logged string array is invalid.");
                     }
                     break;
@@ -293,7 +300,7 @@
                 addExceptionEntry(System.currentTimeMillis(), dataStrings);
                 break;
             default:
-                if (sDBG) {
+                if (DBG) {
                     Log.e(TAG, "Log Tag is not entried.");
                 }
                 break;
@@ -301,7 +308,7 @@
     }
 
     private void commitInternal() {
-        if (sDBG) {
+        if (sLOGPRINT) {
             Log.d(TAG, "Commit (" + mLogBuffer.size() + ")");
         }
         flushPrivacyLogSafely();
@@ -312,7 +319,7 @@
         addVersionNameEntry(now);
         String s = LogSerializer.createStringFromEntries(mLogBuffer);
         if (!TextUtils.isEmpty(s)) {
-            if (sDBG) {
+            if (sLOGPRINT) {
                 Log.d(TAG, "Commit log: " + s);
             }
             mDropBox.addText(TAG, s);
@@ -329,7 +336,7 @@
 
     private synchronized void sendLogToDropBox(int tag, Object s) {
         long now = System.currentTimeMillis();
-        if (sDBG) {
+        if (DBG) {
             String out = "";
             if (s instanceof String[]) {
                 for (String str: ((String[]) s)) {
@@ -367,7 +374,7 @@
                     KeyboardSwitcher.DEFAULT_LAYOUT_ID);
             addThemeIdEntry(mLastTimeActive);
         } else if (PREF_DEBUG_MODE.equals(key)) {
-            sDBG = sharedPreferences.getBoolean(PREF_DEBUG_MODE, sDBG);
+            sLOGPRINT = sharedPreferences.getBoolean(PREF_DEBUG_MODE, sLOGPRINT);
         }
     }
 
@@ -403,7 +410,9 @@
                 before = "";
                 after = "";
             }
-            String[] strings = new String[] {before, after};
+            sLastAutoSuggestSeparator =
+                String.valueOf(sLatinImeLogger.mRingCharBuffer.getLastChar());
+            String[] strings = new String[] {before, after, sLastAutoSuggestSeparator};
             synchronized (LatinImeLogger.class) {
                 sLastAutoSuggestBefore = before;
                 sLastAutoSuggestAfter = after;
@@ -415,21 +424,33 @@
     public static void logOnAutoSuggestionCanceled() {
         if (sLogEnabled) {
             if (sLastAutoSuggestBefore != null && sLastAutoSuggestAfter != null) {
-                String[] strings = new String[] {sLastAutoSuggestBefore, sLastAutoSuggestAfter};
+                String[] strings = new String[] {
+                        sLastAutoSuggestBefore, sLastAutoSuggestAfter, sLastAutoSuggestSeparator};
                 sLatinImeLogger.sendLogToDropBox(ID_AUTOSUGGESTIONCANCELLED, strings);
             }
+            synchronized (LatinImeLogger.class) {
+                sLastAutoSuggestBefore = "";
+                sLastAutoSuggestAfter = "";
+            }
         }
     }
 
-    public static void logOnDelete(int length) {
+    public static void logOnDelete() {
         if (sLogEnabled) {
-            sLatinImeLogger.sendLogToDropBox(ID_DELETE_COUNT, length);
+            String mLastWord = sLatinImeLogger.mRingCharBuffer.getLastString();
+            if (!TextUtils.isEmpty(mLastWord)
+                    && mLastWord.equalsIgnoreCase(sLastAutoSuggestBefore)) {
+                logOnAutoSuggestionCanceled();
+            }
+            sLatinImeLogger.mRingCharBuffer.pop();
+            sLatinImeLogger.sendLogToDropBox(ID_DELETE_COUNT, 1);
         }
     }
 
-    public static void logOnInputChar(int length) {
+    public static void logOnInputChar(char c) {
         if (sLogEnabled) {
-            sLatinImeLogger.sendLogToDropBox(ID_INPUT_COUNT, length);
+            sLatinImeLogger.mRingCharBuffer.push(c);
+            sLatinImeLogger.sendLogToDropBox(ID_INPUT_COUNT, 1);
         }
     }
 
@@ -478,4 +499,59 @@
             return sb.toString();
         }
     }
+
+    /* package */ static class RingCharBuffer {
+        final int BUFSIZE = 20;
+        private Context mContext;
+        private int mEnd = 0;
+        /* package */ int length = 0;
+        private char[] mCharBuf = new char[BUFSIZE];
+
+        public RingCharBuffer(Context context) {
+            mContext = context;
+        }
+
+        private int normalize(int in) {
+            int ret = in % BUFSIZE;
+            return ret < 0 ? ret + BUFSIZE : ret;
+        }
+        public void push(char c) {
+            mCharBuf[mEnd] = c;
+            mEnd = normalize(mEnd + 1);
+            if (length < BUFSIZE) {
+                ++length;
+            }
+        }
+        public char pop() {
+            if (length < 1) {
+                return NULL_CHAR;
+            } else {
+                mEnd = normalize(mEnd - 1);
+                --length;
+                return mCharBuf[mEnd];
+            }
+        }
+        public char getLastChar() {
+            if (length < 1) {
+                return NULL_CHAR;
+            } else {
+                return mCharBuf[normalize(mEnd - 1)];
+            }
+        }
+        public String getLastString() {
+            StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < length; ++i) {
+                char c = mCharBuf[normalize(mEnd - 1 - i)];
+                if (!((LatinIME)mContext).isWordSeparator(c)) {
+                    sb.append(c);
+                } else {
+                    break;
+                }
+            }
+            return sb.reverse().toString();
+        }
+        public void reset() {
+            length = 0;
+        }
+    }
 }
diff --git a/tests/Android.mk b/tests/Android.mk
index e72587d..60e82d5 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -10,7 +10,7 @@
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_PACKAGE_NAME := LatinIMETests
+LOCAL_PACKAGE_NAME := LatinIME2Tests
 
 LOCAL_INSTRUMENTATION_FOR := LatinIme2Google
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 210e814..66cecee 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -27,7 +27,7 @@
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
-        android:targetPackage="com.android.inputmethod.latin"
+        android:targetPackage="com.google.android.inputmethod.latin"
         android:label="LatinIME tests">
     </instrumentation>
 </manifest>
diff --git a/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java b/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java
new file mode 100644
index 0000000..234559b
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java
@@ -0,0 +1,59 @@
+package com.android.inputmethod.latin;
+
+import android.test.ServiceTestCase;
+
+public class ImeLoggerTests extends ServiceTestCase<LatinIME> {
+
+    private static final String WORD_SEPARATORS
+            = ".\u0009\u0020,;:!?\n()[]*&@{}<>;_+=|\\u0022";
+
+    public ImeLoggerTests() {
+        super(LatinIME.class);
+    }
+    static LatinImeLogger sLogger;
+    @Override
+    protected void setUp() {
+        try {
+            super.setUp();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        setupService();
+        // startService(null); // can't be started because VoiceInput can't be found.
+        final LatinIME context = getService();
+        context.mWordSeparators = WORD_SEPARATORS;
+        LatinImeLogger.init(context);
+        sLogger = LatinImeLogger.sLatinImeLogger;
+    }
+    /*********************** Tests *********************/
+    public void testRingBuffer() {
+        for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) {
+            LatinImeLogger.logOnDelete();
+        }
+        assertEquals("", sLogger.mRingCharBuffer.getLastString());
+        LatinImeLogger.logOnInputChar('t');
+        LatinImeLogger.logOnInputChar('g');
+        LatinImeLogger.logOnInputChar('i');
+        LatinImeLogger.logOnInputChar('s');
+        LatinImeLogger.logOnInputChar(' ');
+        LatinImeLogger.logOnAutoSuggestion("tgis", "this");
+        LatinImeLogger.logOnInputChar(' ');
+        LatinImeLogger.logOnDelete();
+        assertEquals("", sLogger.mRingCharBuffer.getLastString());
+        LatinImeLogger.logOnDelete();
+        assertEquals("tgis", sLogger.mRingCharBuffer.getLastString());
+        assertEquals("tgis", LatinImeLogger.sLastAutoSuggestBefore);
+        LatinImeLogger.logOnAutoSuggestionCanceled();
+        assertEquals("", LatinImeLogger.sLastAutoSuggestBefore);
+        LatinImeLogger.logOnDelete();
+        assertEquals("tgi", sLogger.mRingCharBuffer.getLastString());
+        for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) {
+            LatinImeLogger.logOnDelete();
+        }
+        assertEquals("", sLogger.mRingCharBuffer.getLastString());
+        for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) {
+            LatinImeLogger.logOnInputChar('a');
+        }
+        assertEquals(sLogger.mRingCharBuffer.BUFSIZE, sLogger.mRingCharBuffer.length);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/tests/SuggestTests.java b/tests/src/com/android/inputmethod/latin/tests/SuggestTests.java
index 9401d92..0d3babf 100644
--- a/tests/src/com/android/inputmethod/latin/tests/SuggestTests.java
+++ b/tests/src/com/android/inputmethod/latin/tests/SuggestTests.java
@@ -241,8 +241,11 @@
      * Are accented forms of words suggested as corrections?
      */
     public void testAccents() {
-        assertTrue(isDefaultCorrection("nino", "ni\u00F1o")); // ni–o
-        assertTrue(isDefaultCorrection("nimo", "ni\u00F1o")); // ni–o
-        assertTrue(isDefaultCorrection("maria", "Mar\u00EDa")); // Mar’a
+        // ni<LATIN SMALL LETTER N WITH TILDE>o
+        assertTrue(isDefaultCorrection("nino", "ni\u00F1o"));
+        // ni<LATIN SMALL LETTER N WITH TILDE>o
+        assertTrue(isDefaultCorrection("nimo", "ni\u00F1o"));
+        // Mar<LATIN SMALL LETTER I WITH ACUTE>a
+        assertTrue(isDefaultCorrection("maria", "Mar\u00EDa"));
     }
 }