Merge "Fix not intercepting touch event while in modal mode"
diff --git a/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java b/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
index 6543003..a18d582 100644
--- a/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
+++ b/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -18,8 +18,7 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
-
-import com.android.inputmethodcommon.InputMethodSettingsFragment;
+import android.preference.PreferenceFragment;
 
 /**
  * Utility class for managing additional features settings.
@@ -32,7 +31,7 @@
     }
 
     public static void addAdditionalFeaturesPreferences(
-            final Context context, final InputMethodSettingsFragment settingsFragment) {
+            final Context context, final PreferenceFragment settingsFragment) {
         // do nothing.
     }
 
diff --git a/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtilsManager.java b/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtilsManager.java
index 120b105..138f70f 100644
--- a/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtilsManager.java
+++ b/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtilsManager.java
@@ -38,6 +38,12 @@
     public void onLoadSettings(final SettingsValues settingsValues) {
     }
 
+    public void onStartInputView() {
+    }
+
+    public void onFinishInputView() {
+    }
+
     public void onDestroy() {
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java
index df82bec..4f8a105 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java
@@ -49,7 +49,7 @@
             callbacks.dismissKeyPreviewWithoutDelay((Key)msg.obj);
             break;
         case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
-            callbacks.showGestureFloatingPreviewText(SuggestedWords.EMPTY);
+            callbacks.showGestureFloatingPreviewText(SuggestedWords.getEmptyInstance());
             break;
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
index fd84856..37ea0f1 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
@@ -98,7 +98,7 @@
     private final RectF mGesturePreviewRectangle = new RectF();
     private int mPreviewTextX;
     private int mPreviewTextY;
-    private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+    private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
     private final int[] mLastPointerCoords = CoordinateUtils.newInstance();
 
     public GestureFloatingTextDrawingPreview(final TypedArray mainKeyboardViewAttr) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 69fe6de..5aae010 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -793,12 +793,14 @@
     @Override
     public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
         mHandler.onStartInputView(editorInfo, restarting);
+        mStatsUtilsManager.onStartInputView();
     }
 
     @Override
     public void onFinishInputView(final boolean finishingInput) {
         StatsUtils.onFinishInputView();
         mHandler.onFinishInputView(finishingInput);
+        mStatsUtilsManager.onFinishInputView();
     }
 
     @Override
@@ -1491,7 +1493,7 @@
         final boolean isEmptyApplicationSpecifiedCompletions =
                 currentSettingsValues.isApplicationSpecifiedCompletionsOn()
                 && suggestedWords.isEmpty();
-        final boolean noSuggestionsFromDictionaries = (SuggestedWords.EMPTY == suggestedWords)
+        final boolean noSuggestionsFromDictionaries = suggestedWords.isEmpty()
                 || suggestedWords.isPunctuationSuggestions()
                 || isEmptyApplicationSpecifiedCompletions;
         final boolean isBeginningOfSentencePrediction = (suggestedWords.mInputStyle
@@ -1518,7 +1520,7 @@
             final OnGetSuggestedWordsCallback callback) {
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         if (keyboard == null) {
-            callback.onGetSuggestedWords(SuggestedWords.EMPTY);
+            callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance());
             return;
         }
         mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard.getProximityInfo(),
@@ -1526,10 +1528,8 @@
     }
 
     @Override
-    public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) {
-        final SuggestedWords suggestedWords =
-                sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords;
-        if (SuggestedWords.EMPTY == suggestedWords) {
+    public void showSuggestionStrip(final SuggestedWords suggestedWords) {
+        if (suggestedWords.isEmpty()) {
             setNeutralSuggestionStrip();
         } else {
             setSuggestedWords(suggestedWords);
@@ -1537,7 +1537,7 @@
         // Cache the auto-correction in accessibility code so we can speak it if the user
         // touches a key that will insert it.
         AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords,
-                sourceSuggestedWords.mTypedWord);
+                suggestedWords.mTypedWord);
     }
 
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
@@ -1572,7 +1572,8 @@
     public void setNeutralSuggestionStrip() {
         final SettingsValues currentSettings = mSettings.getCurrent();
         final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
-                ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
+                ? SuggestedWords.getEmptyInstance()
+                : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
         setSuggestedWords(neutralSuggestions);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
index 76d4f57..1b7e8f9 100644
--- a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
+++ b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
@@ -126,6 +126,13 @@
         }
     }
 
+    public PrevWordsInfo getTrimmedPrevWordsInfo(final int maxPrevWordCount) {
+        final int newSize = Math.min(maxPrevWordCount, mPrevWordsInfo.length);
+        // TODO: Quit creating a new array.
+        final WordInfo[] prevWordsInfo = Arrays.copyOf(mPrevWordsInfo, newSize);
+        return new PrevWordsInfo(prevWordsInfo);
+    }
+
     public int getPrevWordCount() {
         return mPrevWordsInfo.length;
     }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 3eefafc..e6fd43a 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -45,7 +45,7 @@
     public static final int MAX_SUGGESTIONS = 18;
 
     private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0);
-    public static final SuggestedWords EMPTY = new SuggestedWords(
+    private static final SuggestedWords EMPTY = new SuggestedWords(
             EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */,
             false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE);
 
@@ -196,6 +196,10 @@
         return result;
     }
 
+    public static final SuggestedWords getEmptyInstance() {
+        return SuggestedWords.EMPTY;
+    }
+
     // Should get rid of the first one (what the user typed previously) from suggestions
     // and replace it with what the user currently typed.
     public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 18c740b..46427e5 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -86,7 +86,7 @@
     // Current space state of the input method. This can be any of the above constants.
     private int mSpaceState;
     // Never null
-    public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+    public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
     public final Suggest mSuggest;
     private final DictionaryFacilitator mDictionaryFacilitator;
 
@@ -158,7 +158,7 @@
         mSpaceState = SpaceState.NONE;
         mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
         mCurrentlyPressedHardwareKeys.clear();
-        mSuggestedWords = SuggestedWords.EMPTY;
+        mSuggestedWords = SuggestedWords.getEmptyInstance();
         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
         // so we try using some heuristics to find out about these and fix them.
         mConnection.tryFixLyingCursorPosition();
@@ -332,7 +332,7 @@
         // however need to reset the suggestion strip right away, because we know we can't take
         // the risk of calling commitCompletion twice because we don't know how the app will react.
         if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) {
-            mSuggestedWords = SuggestedWords.EMPTY;
+            mSuggestedWords = SuggestedWords.getEmptyInstance();
             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
             inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
             resetComposingState(true /* alsoResetLastComposedWord */);
@@ -508,7 +508,7 @@
             final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
         mInputLogicHandler.onStartBatchInput();
         handler.showGesturePreviewAndSuggestionStrip(
-                SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
+                SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */);
         handler.cancelUpdateSuggestionStrip();
         ++mAutoCommitSequenceNumber;
         mConnection.beginBatchEdit();
@@ -607,14 +607,14 @@
     public void onCancelBatchInput(final LatinIME.UIHandler handler) {
         mInputLogicHandler.onCancelBatchInput();
         handler.showGesturePreviewAndSuggestionStrip(
-                SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+                SuggestedWords.getEmptyInstance(), true /* dismissGestureFloatingPreviewText */);
     }
 
     // TODO: on the long term, this method should become private, but it will be difficult.
     // Especially, how do we deal with InputMethodService.onDisplayCompletions?
     public void setSuggestedWords(final SuggestedWords suggestedWords,
             final SettingsValues settingsValues, final LatinIME.UIHandler handler) {
-        if (SuggestedWords.EMPTY != suggestedWords) {
+        if (!suggestedWords.isEmpty()) {
             final String autoCorrection;
             final String dictType;
             if (suggestedWords.mWillAutoCorrect) {
@@ -1400,7 +1400,7 @@
                         + "requested!");
             }
             // Clear the suggestions strip.
-            mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY);
+            mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance());
             return;
         }
 
@@ -1892,9 +1892,8 @@
      */
     private SuggestedWords retrieveOlderSuggestions(final String typedWord,
             final SuggestedWords previousSuggestedWords) {
-        final SuggestedWords oldSuggestedWords =
-                previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY
-                        : previousSuggestedWords;
+        final SuggestedWords oldSuggestedWords = previousSuggestedWords.isPunctuationSuggestions()
+                ? SuggestedWords.getEmptyInstance() : previousSuggestedWords;
         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
         return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */,
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index 34d4d4e..d1486f6 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -35,6 +35,7 @@
  */
 public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase {
     /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName();
+    private final static int SUPPORTED_NGRAM = 2; // TODO: 3
 
     // TODO: Make this constructor private
     /* package */ UserHistoryDictionary(final Context context, final Locale locale) {
@@ -61,9 +62,7 @@
     public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary,
             final PrevWordsInfo prevWordsInfo, final String word, final boolean isValid,
             final int timestamp, final DistracterFilter distracterFilter) {
-        final CharSequence prevWord = prevWordsInfo.mPrevWordsInfo[0].mWord;
-        if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH ||
-                (prevWord != null && prevWord.length() > Constants.DICTIONARY_MAX_WORD_LENGTH)) {
+        if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH) {
             return;
         }
         final int frequency = isValid ?
@@ -71,17 +70,29 @@
         userHistoryDictionary.addUnigramEntryWithCheckingDistracter(word, frequency,
                 null /* shortcutTarget */, 0 /* shortcutFreq */, false /* isNotAWord */,
                 false /* isBlacklisted */, timestamp, distracterFilter);
-        // Do not insert a word as a bigram of itself
-        if (TextUtils.equals(word, prevWord)) {
-            return;
-        }
-        if (null != prevWord) {
-            if (prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence) {
-                // Beginning-of-Sentence n-gram entry is treated as a n-gram entry of invalid word.
-                userHistoryDictionary.addNgramEntry(prevWordsInfo, word,
+
+        final boolean isBeginningOfSentenceContext =
+                prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence;
+        final PrevWordsInfo prevWordsInfoToBeSaved =
+                prevWordsInfo.getTrimmedPrevWordsInfo(SUPPORTED_NGRAM - 1);
+        for (int i = 0; i < prevWordsInfoToBeSaved.getPrevWordCount(); i++) {
+            final CharSequence prevWord = prevWordsInfoToBeSaved.mPrevWordsInfo[i].mWord;
+            if (prevWord == null || (prevWord.length() > Constants.DICTIONARY_MAX_WORD_LENGTH)) {
+                return;
+            }
+            // Do not insert a word as a bigram of itself
+            if (i == 0 && TextUtils.equals(word, prevWord)) {
+                return;
+            }
+            if (isBeginningOfSentenceContext) {
+                // Beginning-of-Sentence n-gram entry is added as an n-gram entry of an OOV word.
+                userHistoryDictionary.addNgramEntry(
+                        prevWordsInfoToBeSaved.getTrimmedPrevWordsInfo(i + 1), word,
                         FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp);
             } else {
-                userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp);
+                userHistoryDictionary.addNgramEntry(
+                        prevWordsInfoToBeSaved.getTrimmedPrevWordsInfo(i + 1), word, frequency,
+                        timestamp);
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
index a6cb55d..3303ab0 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
@@ -109,6 +109,8 @@
             removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
         }
 
+        AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
+
         setupKeypressVibrationDurationSettings();
         setupKeypressSoundVolumeSettings();
         refreshEnablingsOfKeypressSoundAndVibrationSettings();
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 04264e6..197a544 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -82,7 +82,7 @@
     private final ArrayList<View> mDividerViews = new ArrayList<>();
 
     Listener mListener;
-    private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+    private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
     private int mStartIndexOfMoreSuggestions;
 
     private final SuggestionStripLayoutHelper mLayoutHelper;
diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
index 8b70778..ea406fa 100644
--- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -23,15 +23,24 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.R;
 
+import java.util.concurrent.TimeUnit;
+
 public final class ImportantNoticeUtils {
     private static final String TAG = ImportantNoticeUtils.class.getSimpleName();
 
     // {@link SharedPreferences} name to save the last important notice version that has been
     // displayed to users.
     private static final String PREFERENCE_NAME = "important_notice_pref";
-    private static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version";
+    @UsedForTesting
+    static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version";
+    @UsedForTesting
+    static final String KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE =
+            "timestamp_of_first_important_notice";
+    @UsedForTesting
+    static final long TIMEOUT_OF_IMPORTANT_NOTICE = TimeUnit.HOURS.toMillis(23);
     public static final int VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS = 1;
 
     // Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key.
@@ -56,15 +65,18 @@
         }
     }
 
-    private static SharedPreferences getImportantNoticePreferences(final Context context) {
+    @UsedForTesting
+    static SharedPreferences getImportantNoticePreferences(final Context context) {
         return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
     }
 
-    private static int getCurrentImportantNoticeVersion(final Context context) {
+    @UsedForTesting
+    static int getCurrentImportantNoticeVersion(final Context context) {
         return context.getResources().getInteger(R.integer.config_important_notice_version);
     }
 
-    private static int getLastImportantNoticeVersion(final Context context) {
+    @UsedForTesting
+    static int getLastImportantNoticeVersion(final Context context) {
         return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0);
     }
 
@@ -77,6 +89,20 @@
         return getCurrentImportantNoticeVersion(context) > lastVersion;
     }
 
+    @UsedForTesting
+    static boolean hasTimeoutPassed(final Context context, final long currentTimeInMillis) {
+        final SharedPreferences prefs = getImportantNoticePreferences(context);
+        if (!prefs.contains(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)) {
+            prefs.edit()
+                    .putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis)
+                    .apply();
+        }
+        final long firstDisplayTimeInMillis = prefs.getLong(
+                KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis);
+        final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis;
+        return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE;
+    }
+
     public static boolean shouldShowImportantNotice(final Context context) {
         if (!hasNewImportantNotice(context)) {
             return false;
@@ -88,6 +114,10 @@
         if (isInSystemSetupWizard(context)) {
             return false;
         }
+        if (hasTimeoutPassed(context, System.currentTimeMillis())) {
+            updateLastImportantNoticeVersion(context);
+            return false;
+        }
         return true;
     }
 
@@ -95,11 +125,12 @@
         getImportantNoticePreferences(context)
                 .edit()
                 .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context))
+                .remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)
                 .apply();
     }
 
     public static String getNextImportantNoticeTitle(final Context context) {
-        final int nextVersion = getCurrentImportantNoticeVersion(context);
+        final int nextVersion = getNextImportantNoticeVersion(context);
         final String[] importantNoticeTitleArray = context.getResources().getStringArray(
                 R.array.important_notice_title_array);
         if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index 8d41356..cca0c29 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -166,6 +166,9 @@
         for (const auto entry : languageModelDictContent->getProbabilityEntries(
                 prevWordIds.limit(i))) {
             const ProbabilityEntry &probabilityEntry = entry.getProbabilityEntry();
+            if (!probabilityEntry.isValid()) {
+                continue;
+            }
             const int probability = probabilityEntry.hasHistoricalInfo() ?
                     ForgettingCurveUtils::decodeProbability(
                             probabilityEntry.getHistoricalInfo(), mHeaderPolicy)
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
index 869c550..563261f 100644
--- a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -147,7 +147,7 @@
         assertNull(wordsWithoutTypedWord.getTypedWordInfoOrNull());
 
         // Make sure getTypedWordInfoOrNull() returns null.
-        assertNull(SuggestedWords.EMPTY.getTypedWordInfoOrNull());
+        assertNull(SuggestedWords.getEmptyInstance().getTypedWordInfoOrNull());
 
         final SuggestedWords emptySuggestedWords = new SuggestedWords(
                 new ArrayList<SuggestedWordInfo>(), null /* rawSuggestions */,
@@ -157,6 +157,6 @@
                 SuggestedWords.INPUT_STYLE_NONE);
         assertNull(emptySuggestedWords.getTypedWordInfoOrNull());
 
-        assertNull(SuggestedWords.EMPTY.getTypedWordInfoOrNull());
+        assertNull(SuggestedWords.getEmptyInstance().getTypedWordInfoOrNull());
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index abb468f..6162096 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -74,9 +74,10 @@
         }
     }
 
-    private void checkExistenceAndRemoveDictFile(final Locale locale, final File dictFile) {
+    private void checkExistenceAndRemoveDictFile(final UserHistoryDictionary dict,
+            final File dictFile) {
         Log.d(TAG, "waiting for writing ...");
-        waitForWriting(locale);
+        dict.waitAllTasksForTests();
         if (!dictFile.exists()) {
             try {
                 Log.d(TAG, dictFile + " is not existing. Wait "
@@ -91,6 +92,10 @@
         FileUtils.deleteRecursively(dictFile);
     }
 
+    private static Locale getDummyLocale(final String name) {
+        return new Locale(TEST_LOCALE_PREFIX + name + System.currentTimeMillis());
+    }
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -168,11 +173,9 @@
      * @param checkContents if true, checks whether written words are actually in the dictionary
      * or not.
      */
-    private void addAndWriteRandomWords(final Locale locale, final int numberOfWords,
-            final Random random, final boolean checkContents) {
+    private void addAndWriteRandomWords(final UserHistoryDictionary dict,
+            final int numberOfWords, final Random random, final boolean checkContents) {
         final List<String> words = generateWords(numberOfWords, random);
-        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                mContext, locale);
         // Add random words to the user history dictionary.
         addToDict(dict, words);
         if (checkContents) {
@@ -188,47 +191,31 @@
 
     /**
      * Clear all entries in the user history dictionary.
-     * @param locale dummy locale for testing.
+     * @param dict the user history dictionary.
      */
-    private void clearHistory(final Locale locale) {
-        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                mContext, locale);
+    private void clearHistory(final UserHistoryDictionary dict) {
         dict.waitAllTasksForTests();
         dict.clear();
         dict.close();
         dict.waitAllTasksForTests();
     }
 
-    /**
-     * Shut down executer and wait until all operations of user history are done.
-     * @param locale dummy locale for testing.
-     */
-    private void waitForWriting(final Locale locale) {
-        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                mContext, locale);
-        dict.waitAllTasksForTests();
-    }
-
     public void testRandomWords() {
         Log.d(TAG, "This test can be used for profiling.");
         Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
-        final Locale dummyLocale =
-                new Locale(TEST_LOCALE_PREFIX + "random_words" + System.currentTimeMillis());
+        final Locale dummyLocale = getDummyLocale("random_words");
         final String dictName = ExpandableBinaryDictionary.getDictName(
                 UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
         final File dictFile = ExpandableBinaryDictionary.getDictFile(
                 mContext, dictName, null /* dictFile */);
+        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
+                getContext(), dummyLocale);
 
         final int numberOfWords = 1000;
         final Random random = new Random(123456);
-
-        try {
-            clearHistory(dummyLocale);
-            addAndWriteRandomWords(dummyLocale, numberOfWords, random,
-                    true /* checksContents */);
-        } finally {
-            checkExistenceAndRemoveDictFile(dummyLocale, dictFile);
-        }
+        clearHistory(dict);
+        addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */);
+        checkExistenceAndRemoveDictFile(dict, dictFile);
     }
 
     public void testStressTestForSwitchingLanguagesAndAddingWords() {
@@ -237,28 +224,30 @@
         final int numberOfWordsInsertedForEachLanguageSwitch = 100;
 
         final File dictFiles[] = new File[numberOfLanguages];
-        final Locale dummyLocales[] = new Locale[numberOfLanguages];
+        final UserHistoryDictionary dicts[] = new UserHistoryDictionary[numberOfLanguages];
+
         try {
             final Random random = new Random(123456);
 
             // Create filename suffixes for this test.
             for (int i = 0; i < numberOfLanguages; i++) {
-                dummyLocales[i] = new Locale(TEST_LOCALE_PREFIX + "switching_languages" + i);
+                final Locale dummyLocale = getDummyLocale("switching_languages" + i);
                 final String dictName = ExpandableBinaryDictionary.getDictName(
-                        UserHistoryDictionary.NAME, dummyLocales[i], null /* dictFile */);
+                        UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
                 dictFiles[i] = ExpandableBinaryDictionary.getDictFile(
                         mContext, dictName, null /* dictFile */);
-                clearHistory(dummyLocales[i]);
+                dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(),
+                        dummyLocale);
+                clearHistory(dicts[i]);
             }
 
             final long start = System.currentTimeMillis();
 
             for (int i = 0; i < numberOfLanguageSwitching; i++) {
                 final int index = i % numberOfLanguages;
-                // Switch languages to testFilenameSuffixes[index].
-                addAndWriteRandomWords(dummyLocales[index],
-                        numberOfWordsInsertedForEachLanguageSwitch, random,
-                        false /* checksContents */);
+                // Switch to dicts[index].
+                addAndWriteRandomWords(dicts[index], numberOfWordsInsertedForEachLanguageSwitch,
+                        random, false /* checksContents */);
             }
 
             final long end = System.currentTimeMillis();
@@ -266,38 +255,38 @@
                     + (end - start) + " ms");
         } finally {
             for (int i = 0; i < numberOfLanguages; i++) {
-                checkExistenceAndRemoveDictFile(dummyLocales[i], dictFiles[i]);
+                checkExistenceAndRemoveDictFile(dicts[i], dictFiles[i]);
             }
         }
     }
 
     public void testAddManyWords() {
-        final Locale dummyLocale =
-                new Locale(TEST_LOCALE_PREFIX + "many_random_words" + System.currentTimeMillis());
+        final Locale dummyLocale = getDummyLocale("many_random_words");
         final String dictName = ExpandableBinaryDictionary.getDictName(
                 UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
         final File dictFile = ExpandableBinaryDictionary.getDictFile(
                 mContext, dictName, null /* dictFile */);
         final int numberOfWords = 10000;
         final Random random = new Random(123456);
-        clearHistory(dummyLocale);
+        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
+                getContext(), dummyLocale);
+        clearHistory(dict);
         try {
-            addAndWriteRandomWords(dummyLocale, numberOfWords, random, true /* checksContents */);
+            addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */);
         } finally {
-            checkExistenceAndRemoveDictFile(dummyLocale, dictFile);
+            checkExistenceAndRemoveDictFile(dict, dictFile);
         }
     }
 
     public void testDecaying() {
-        final Locale dummyLocale =
-                new Locale(TEST_LOCALE_PREFIX + "decaying" + System.currentTimeMillis());
+        final Locale dummyLocale = getDummyLocale("decaying");
+        final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
+                getContext(), dummyLocale);
         final int numberOfWords = 5000;
         final Random random = new Random(123456);
         resetCurrentTimeForTestMode();
-        clearHistory(dummyLocale);
+        clearHistory(dict);
         final List<String> words = generateWords(numberOfWords, random);
-        final UserHistoryDictionary dict =
-                PersonalizationHelper.getUserHistoryDictionary(getContext(), dummyLocale);
         dict.waitAllTasksForTests();
         PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
         for (final String word : words) {
@@ -319,5 +308,6 @@
         for (final String word : words) {
             assertFalse(dict.isInDictionary(word));
         }
+        stopTestModeInNativeCode();
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java
new file mode 100644
index 0000000..819d763
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java
@@ -0,0 +1,222 @@
+/*
+ * 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.utils;
+
+import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE;
+import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_IMPORTANT_NOTICE_VERSION;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+public class ImportantNoticeUtilsTests extends AndroidTestCase {
+    // This should be aligned with R.integer.config_important_notice_version.
+    private static final int CURRENT_IMPORTANT_NOTICE_VERSION = 1;
+
+    private ImportantNoticePreferences mImportantNoticePreferences;
+
+    private static class ImportantNoticePreferences {
+        private final SharedPreferences mPref;
+
+        private Integer mVersion;
+        private Long mLastTime;
+
+        public ImportantNoticePreferences(final Context context) {
+            mPref = ImportantNoticeUtils.getImportantNoticePreferences(context);
+        }
+
+        private Integer getInt(final String key) {
+            if (mPref.contains(key)) {
+                return mPref.getInt(key, 0);
+            }
+            return null;
+        }
+
+        public Long getLong(final String key) {
+            if (mPref.contains(key)) {
+                return mPref.getLong(key, 0);
+            }
+            return null;
+        }
+
+        private void putInt(final String key, final Integer value) {
+            if (value == null) {
+                removePreference(key);
+            } else {
+                mPref.edit().putInt(key, value).apply();
+            }
+        }
+
+        private void putLong(final String key, final Long value) {
+            if (value == null) {
+                removePreference(key);
+            } else {
+                mPref.edit().putLong(key, value).apply();
+            }
+        }
+
+        private void removePreference(final String key) {
+            mPref.edit().remove(key).apply();
+        }
+
+        public void save() {
+            mVersion = getInt(KEY_IMPORTANT_NOTICE_VERSION);
+            mLastTime = getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE);
+        }
+
+        public void restore() {
+            putInt(KEY_IMPORTANT_NOTICE_VERSION, mVersion);
+            putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, mLastTime);
+        }
+
+        public void clear() {
+            removePreference(KEY_IMPORTANT_NOTICE_VERSION);
+            removePreference(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE);
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mImportantNoticePreferences = new ImportantNoticePreferences(getContext());
+        mImportantNoticePreferences.save();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mImportantNoticePreferences.restore();
+    }
+
+    public void testCurrentVersion() {
+        assertEquals("Current version", CURRENT_IMPORTANT_NOTICE_VERSION,
+                ImportantNoticeUtils.getCurrentImportantNoticeVersion(getContext()));
+    }
+
+    public void testUpdateVersion() {
+        mImportantNoticePreferences.clear();
+
+        assertEquals("Current boolean before update", true,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version before update", 0,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version before update ", 1,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Current title before update", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents before update", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+
+        ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext());
+
+        assertEquals("Current boolean after update", false,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version after update", 1,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version after update", 2,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Current title after update", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents after update", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+    }
+
+    private static void sleep(final long millseconds) {
+        try { Thread.sleep(millseconds); } catch (final Exception e) { /* ignore */ }
+    }
+
+    public void testTimeout() {
+        final long lastTime = System.currentTimeMillis()
+                - ImportantNoticeUtils.TIMEOUT_OF_IMPORTANT_NOTICE
+                + TimeUnit.MILLISECONDS.toMillis(1000);
+        mImportantNoticePreferences.clear();
+        assertEquals("Before set last time", null,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+        assertEquals("Set last time", false,
+                ImportantNoticeUtils.hasTimeoutPassed(getContext(), lastTime));
+        assertEquals("After set last time", (Long)lastTime,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+
+        // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} before timeout.
+        assertEquals("Current boolean before timeout 1", true,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version before timeout 1", 0,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version before timeout 1", 1,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Last time before timeout 1", (Long)lastTime,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+        assertEquals("Current title before timeout 1", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents before timeout 1", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+
+        sleep(TimeUnit.MILLISECONDS.toMillis(600));
+
+        // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} before timeout
+        // again.
+        assertEquals("Current boolean before timeout 2", true,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version before timeout 2", 0,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version before timeout 2", 1,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Last time before timeout 2", (Long)lastTime,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+        assertEquals("Current title before timeout 2", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents before timeout 2", false, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+
+        sleep(TimeUnit.MILLISECONDS.toMillis(600));
+
+        // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} after timeout.
+        assertEquals("Current boolean after timeout 1", false,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version after timeout 1", 1,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version after timeout 1", 2,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Last time aflter timeout 1", null,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+        assertEquals("Current title after timeout 1", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents after timeout 1", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+
+        sleep(TimeUnit.MILLISECONDS.toMillis(600));
+
+        // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} after timeout again.
+        assertEquals("Current boolean after timeout 2", false,
+                ImportantNoticeUtils.shouldShowImportantNotice(getContext()));
+        assertEquals("Last version after timeout 2", 1,
+                ImportantNoticeUtils.getLastImportantNoticeVersion(getContext()));
+        assertEquals("Next version after timeout 2", 2,
+                ImportantNoticeUtils.getNextImportantNoticeVersion(getContext()));
+        assertEquals("Last time aflter timeout 2", null,
+                mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE));
+        assertEquals("Current title after timeout 2", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeTitle(getContext())));
+        assertEquals("Current contents after timeout 2", true, TextUtils.isEmpty(
+                ImportantNoticeUtils.getNextImportantNoticeContents(getContext())));
+    }
+}