Merge "Remove LatinImeLogger and UsabilityStudyLogUtils"
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index dc2e1af..4fac5ed 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -757,10 +757,11 @@
         final int codePoint = inputTransaction.mEvent.mCodePoint;
         final SettingsValues settingsValues = inputTransaction.mSettingsValues;
         boolean didAutoCorrect = false;
+        final boolean wasComposingWord = mWordComposer.isComposingWord();
         // We avoid sending spaces in languages without spaces if we were composing.
         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
                 && !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
-                && mWordComposer.isComposingWord();
+                && wasComposingWord;
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can insert the separator at the current cursor position.
@@ -811,13 +812,16 @@
         if (Constants.CODE_SPACE == codePoint) {
             if (maybeDoubleSpacePeriod(inputTransaction)) {
                 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
+                inputTransaction.setRequiresUpdateSuggestions();
                 mSpaceState = SpaceState.DOUBLE;
             } else if (!mSuggestedWords.isPunctuationSuggestions()) {
                 mSpaceState = SpaceState.WEAK;
             }
 
             startDoubleSpacePeriodCountdown(inputTransaction);
-            inputTransaction.setRequiresUpdateSuggestions();
+            if (wasComposingWord) {
+                inputTransaction.setRequiresUpdateSuggestions();
+            }
         } else {
             if (swapWeakSpace) {
                 swapSwapperAndSpace(inputTransaction);
@@ -911,6 +915,11 @@
                 if (mConnection.revertDoubleSpacePeriod()) {
                     // No need to reset mSpaceState, it has already be done (that's why we
                     // receive it as a parameter)
+                    inputTransaction.setRequiresUpdateSuggestions();
+                    mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                            WordComposer.CAPS_MODE_OFF,
+                            getPrevWordsInfoFromNthPreviousWordForSuggestion(
+                                    inputTransaction.mSettingsValues.mSpacingAndPunctuations, 1));
                     return;
                 }
             } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
index c08697c..61da1b7 100644
--- a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
@@ -19,23 +19,42 @@
 import com.android.inputmethod.annotations.UsedForTesting;
 
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 
 /**
  * Utilities to manage executors.
  */
 public class ExecutorUtils {
-    private static final ConcurrentHashMap<String, PrioritizedSerialExecutor> sExecutorMap =
+    private static final ConcurrentHashMap<String, ExecutorService> sExecutorMap =
             new ConcurrentHashMap<>();
 
+    private static class ThreadFactoryWithId implements ThreadFactory {
+        private final String mId;
+
+        public ThreadFactoryWithId(final String id) {
+            mId = id;
+        }
+
+        @Override
+        public Thread newThread(final Runnable r) {
+            return new Thread(r, "Executor - " + mId);
+        }
+    }
+
     /**
      * Gets the executor for the given id.
      */
-    public static PrioritizedSerialExecutor getExecutor(final String id) {
-        PrioritizedSerialExecutor executor = sExecutorMap.get(id);
+    public static ExecutorService getExecutor(final String id) {
+        ExecutorService executor = sExecutorMap.get(id);
         if (executor == null) {
             synchronized(sExecutorMap) {
-                executor = new PrioritizedSerialExecutor(id);
-                sExecutorMap.put(id, executor);
+                executor = sExecutorMap.get(id);
+                if (executor == null) {
+                    executor = Executors.newSingleThreadExecutor(new ThreadFactoryWithId(id));
+                    sExecutorMap.put(id, executor);
+                }
             }
         }
         return executor;
@@ -47,7 +66,7 @@
     @UsedForTesting
     public static void shutdownAllExecutors() {
         synchronized(sExecutorMap) {
-            for (final PrioritizedSerialExecutor executor : sExecutorMap.values()) {
+            for (final ExecutorService executor : sExecutorMap.values()) {
                 executor.execute(new Runnable() {
                     @Override
                     public void run() {
diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
deleted file mode 100644
index 21949ff..0000000
--- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2013 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.utils;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-
-import java.util.Queue;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * An object that executes submitted tasks using a thread.
- */
-public class PrioritizedSerialExecutor {
-    public static final String TAG = PrioritizedSerialExecutor.class.getSimpleName();
-
-    private final Object mLock = new Object();
-
-    private final Queue<Runnable> mTasks;
-    private final Queue<Runnable> mPrioritizedTasks;
-    private boolean mIsShutdown;
-    private final ThreadPoolExecutor mThreadPoolExecutor;
-
-    // The task which is running now.
-    private Runnable mActive;
-
-    private static class ThreadFactoryWithId implements ThreadFactory {
-        private final String mId;
-
-        public ThreadFactoryWithId(final String id) {
-            mId = id;
-        }
-
-        @Override
-        public Thread newThread(final Runnable r) {
-            return new Thread(r, TAG + " - " + mId);
-        }
-    }
-
-    public PrioritizedSerialExecutor(final String id) {
-        mTasks = new ConcurrentLinkedQueue<>();
-        mPrioritizedTasks = new ConcurrentLinkedQueue<>();
-        mIsShutdown = false;
-        mThreadPoolExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
-                0 /* keepAliveTime */, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(1),
-                new ThreadFactoryWithId(id));
-    }
-
-    /**
-     * Enqueues the given task into the task queue.
-     * @param r the enqueued task
-     */
-    public void execute(final Runnable r) {
-        synchronized(mLock) {
-            if (!mIsShutdown) {
-                mTasks.offer(new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            r.run();
-                        } finally {
-                            scheduleNext();
-                        }
-                    }
-                });
-                if (mActive == null) {
-                    scheduleNext();
-                }
-            }
-        }
-    }
-
-    /**
-     * Enqueues the given task into the prioritized task queue.
-     * @param r the enqueued task
-     */
-    @UsedForTesting
-    public void executePrioritized(final Runnable r) {
-        synchronized(mLock) {
-            if (!mIsShutdown) {
-                mPrioritizedTasks.offer(new Runnable() {
-                    @Override
-                    public void run() {
-                        try {
-                            r.run();
-                        } finally {
-                            scheduleNext();
-                        }
-                    }
-                });
-                if (mActive == null) {
-                    scheduleNext();
-                }
-            }
-        }
-    }
-
-    private boolean fetchNextTasksLocked() {
-        mActive = mPrioritizedTasks.poll();
-        if (mActive == null) {
-            mActive = mTasks.poll();
-        }
-        return mActive != null;
-    }
-
-    private void scheduleNext() {
-        synchronized(mLock) {
-            if (fetchNextTasksLocked()) {
-                mThreadPoolExecutor.execute(mActive);
-            }
-        }
-    }
-
-    public void shutdown() {
-        synchronized(mLock) {
-            mIsShutdown = true;
-            mThreadPoolExecutor.shutdown();
-        }
-    }
-}
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index a944416..460f600 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -481,6 +481,27 @@
                 suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
     }
 
+    public void testPredictionsWithDoubleSpaceToPeriod() {
+        final String WORD_TO_TYPE = "Barack ";
+        type(WORD_TO_TYPE);
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        // No need to test here, testPredictionsAfterSpace is testing it already
+        type(" ");
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        // Test the predictions have been cleared
+        SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest();
+        assertEquals("predictions cleared after double-space-to-period", suggestedWords.size(), 0);
+        type(Constants.CODE_DELETE);
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        // Test the first prediction is displayed
+        suggestedWords = mLatinIME.getSuggestedWordsForTest();
+        assertEquals("predictions after cancel double-space-to-period", "Obama",
+                suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
+    }
+
     public void testPredictionsAfterManualPick() {
         final String WORD_TO_TYPE = "Barack";
         type(WORD_TO_TYPE);
diff --git a/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java
new file mode 100644
index 0000000..0f2f981
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014 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.personalization;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.DictionaryFacilitator;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback;
+import com.android.inputmethod.latin.makedict.CodePointUtils;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+/**
+ * Unit tests for personalization dictionary
+ */
+@LargeTest
+public class PersonalizationDictionaryTests extends AndroidTestCase {
+    private static final String TAG = PersonalizationDictionaryTests.class.getSimpleName();
+
+    private static final Locale LOCALE_EN_US = new Locale("en", "US");
+    private static final String DUMMY_PACKAGE_NAME = "test.package.name";
+    private static final long TIMEOUT_TO_WAIT_DICTIONARY_OPERATIONS_IN_SECONDS = 120;
+
+    private DictionaryFacilitator getDictionaryFacilitator() {
+        final ArrayList<String> dictTypes = new ArrayList<>();
+        dictTypes.add(Dictionary.TYPE_MAIN);
+        dictTypes.add(Dictionary.TYPE_PERSONALIZATION);
+        final DictionaryFacilitator dictionaryFacilitator = new DictionaryFacilitator();
+        dictionaryFacilitator.resetDictionariesForTesting(getContext(), LOCALE_EN_US, dictTypes,
+                new HashMap<String, File>(), new HashMap<String, Map<String, String>>());
+        return dictionaryFacilitator;
+    }
+
+    public void testAddManyTokens() {
+        final DictionaryFacilitator dictionaryFacilitator = getDictionaryFacilitator();
+        dictionaryFacilitator.clearPersonalizationDictionary();
+        final int dataChunkCount = 20;
+        final int wordCountInOneChunk = 2000;
+        final Random random = new Random(System.currentTimeMillis());
+        final int[] codePointSet = CodePointUtils.LATIN_ALPHABETS_LOWER;
+
+        final SpacingAndPunctuations spacingAndPunctuations =
+                new SpacingAndPunctuations(getContext().getResources());
+
+        final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
+                System.currentTimeMillis());
+
+        for (int i = 0; i < dataChunkCount; i++) {
+            final ArrayList<String> tokens = new ArrayList<>();
+            for (int j = 0; j < wordCountInOneChunk; j++) {
+                tokens.add(CodePointUtils.generateWord(random, codePointSet));
+            }
+            final PersonalizationDataChunk personalizationDataChunk = new PersonalizationDataChunk(
+                    true /* inputByUser */, tokens, timeStampInSeconds, DUMMY_PACKAGE_NAME);
+            final CountDownLatch countDownLatch = new CountDownLatch(1);
+            final AddMultipleDictionaryEntriesCallback callback =
+                    new AddMultipleDictionaryEntriesCallback() {
+                        @Override
+                        public void onFinished() {
+                            countDownLatch.countDown();
+                        }
+                    };
+            dictionaryFacilitator.addEntriesToPersonalizationDictionary(personalizationDataChunk,
+                    spacingAndPunctuations, callback);
+            try {
+                countDownLatch.await(TIMEOUT_TO_WAIT_DICTIONARY_OPERATIONS_IN_SECONDS,
+                        TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e);
+            }
+        }
+        dictionaryFacilitator.flushPersonalizationDictionary();
+        try {
+            dictionaryFacilitator.waitForLoadingDictionariesForTesting(
+                    TIMEOUT_TO_WAIT_DICTIONARY_OPERATIONS_IN_SECONDS, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e);
+        }
+        final String dictName = ExpandableBinaryDictionary.getDictName(
+                PersonalizationDictionary.NAME, LOCALE_EN_US, null /* dictFile */);
+        final File dictFile = ExpandableBinaryDictionary.getDictFile(
+                getContext(), dictName, null /* dictFile */);
+
+        final BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, 0 /* size */,
+                true /* useFullEditDistance */, LOCALE_EN_US, Dictionary.TYPE_PERSONALIZATION,
+                true /* isUpdatable */);
+        assertTrue(binaryDictionary.isValidDictionary());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java
new file mode 100644
index 0000000..ae2623d
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/ExecutorUtilsTests.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 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.utils;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Unit tests for ExecutorUtils.
+ */
+@MediumTest
+public class ExecutorUtilsTests extends AndroidTestCase {
+    private static final String TAG = ExecutorUtilsTests.class.getSimpleName();
+
+    private static final String TEST_EXECUTOR_ID = "test";
+    private static final int NUM_OF_TASKS = 10;
+    private static final int DELAY_FOR_WAITING_TASKS_MILLISECONDS = 500;
+
+    public void testExecute() {
+        final ExecutorService executor = ExecutorUtils.getExecutor(TEST_EXECUTOR_ID);
+        final AtomicInteger v = new AtomicInteger(0);
+        for (int i = 0; i < NUM_OF_TASKS; ++i) {
+            executor.execute(new Runnable() {
+                @Override
+                public void run() {
+                    v.incrementAndGet();
+                }
+            });
+        }
+        try {
+            executor.awaitTermination(DELAY_FOR_WAITING_TASKS_MILLISECONDS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Log.d(TAG, "Exception while sleeping.", e);
+        }
+
+        assertEquals(NUM_OF_TASKS, v.get());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutorTests.java b/tests/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutorTests.java
deleted file mode 100644
index 8b78816..0000000
--- a/tests/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutorTests.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright (C) 2013 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.utils;
-
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.util.Log;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Unit tests for PrioritizedSerialExecutor.
- * TODO: Add more detailed tests to make use of priorities, etc.
- */
-@MediumTest
-public class PrioritizedSerialExecutorTests extends AndroidTestCase {
-    private static final String TAG = PrioritizedSerialExecutorTests.class.getSimpleName();
-
-    private static final String TEST_EXECUTOR_ID = "test";
-    private static final int NUM_OF_TASKS = 10;
-    private static final int DELAY_FOR_WAITING_TASKS_MILLISECONDS = 500;
-
-    public void testExecute() {
-        final PrioritizedSerialExecutor executor = new PrioritizedSerialExecutor(TEST_EXECUTOR_ID);
-        final AtomicInteger v = new AtomicInteger(0);
-        for (int i = 0; i < NUM_OF_TASKS; ++i) {
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    v.incrementAndGet();
-                }
-            });
-        }
-        try {
-            Thread.sleep(DELAY_FOR_WAITING_TASKS_MILLISECONDS);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Exception while sleeping.", e);
-        }
-
-        assertEquals(NUM_OF_TASKS, v.get());
-    }
-
-    public void testExecutePrioritized() {
-        final PrioritizedSerialExecutor executor = new PrioritizedSerialExecutor(TEST_EXECUTOR_ID);
-        final AtomicInteger v = new AtomicInteger(0);
-        for (int i = 0; i < NUM_OF_TASKS; ++i) {
-            executor.executePrioritized(new Runnable() {
-                @Override
-                public void run() {
-                    v.incrementAndGet();
-                }
-            });
-        }
-        try {
-            Thread.sleep(DELAY_FOR_WAITING_TASKS_MILLISECONDS);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Exception while sleeping.", e);
-        }
-
-        assertEquals(NUM_OF_TASKS, v.get());
-    }
-
-    public void testExecuteCombined() {
-        final PrioritizedSerialExecutor executor = new PrioritizedSerialExecutor(TEST_EXECUTOR_ID);
-        final AtomicInteger v = new AtomicInteger(0);
-        for (int i = 0; i < NUM_OF_TASKS; ++i) {
-            executor.execute(new Runnable() {
-                @Override
-                public void run() {
-                    v.incrementAndGet();
-                }
-            });
-        }
-
-        for (int i = 0; i < NUM_OF_TASKS; ++i) {
-            executor.executePrioritized(new Runnable() {
-                @Override
-                public void run() {
-                    v.incrementAndGet();
-                }
-            });
-        }
-
-        try {
-            Thread.sleep(DELAY_FOR_WAITING_TASKS_MILLISECONDS);
-        } catch (InterruptedException e) {
-            Log.d(TAG, "Exception while sleeping.", e);
-        }
-
-        assertEquals(2 * NUM_OF_TASKS, v.get());
-    }
-}