Merge "Add boundary check for ver2 dict reading."
diff --git a/java/res/values/strings-config-important-notice.xml b/java/res/values/strings-config-important-notice.xml
index 3be95d3..f2229be 100644
--- a/java/res/values/strings-config-important-notice.xml
+++ b/java/res/values/strings-config-important-notice.xml
@@ -20,11 +20,14 @@
 
 <resources>
     <integer name="config_important_notice_version">0</integer>
-    <!-- TODO: Make title and contents resource to string array indexed by version. -->
-    <!-- The text of the important notice displayed on the suggestion strip. -->
-    <string name="important_notice_title"></string>
-    <!-- The contents of the important notice. -->
-    <string name="important_notice_contents"></string>
+    <!-- The array of the text of the important notices displayed on the suggestion strip. -->
+    <string-array name="important_notice_title_array">
+        <!-- empty -->
+    </string-array>
+    <!-- The array of the contents of the important notices. -->
+    <string-array name="important_notice_contents_array">
+        <!-- empty -->
+    </string-array>
     <!-- Description for option enabling the use by the keyboards of sent/received messages, e-mail and typing history to improve suggestion accuracy [CHAR LIMIT=68] -->
     <string name="use_personalized_dicts_summary">Learn from your communications and typed data to improve suggestions</string>
 </resources>
diff --git a/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java
new file mode 100644
index 0000000..9870faa
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.DialogInterface.OnShowListener;
+
+import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
+
+/**
+ * The dialog box that shows the important notice contents.
+ */
+public final class ImportantNoticeDialog extends AlertDialog implements OnShowListener,
+        OnClickListener, OnDismissListener {
+    public interface ImportantNoticeDialogListener {
+        public void onClickSettingsOfImportantNoticeDialog(final int nextVersion);
+        public void onDismissImportantNoticeDialog(final int nextVersion);
+    }
+
+    private final ImportantNoticeDialogListener mListener;
+    private final int mNextImportantNoticeVersion;
+
+    public ImportantNoticeDialog(
+            final Context context, final ImportantNoticeDialogListener listener) {
+        super(context, THEME_HOLO_DARK);
+        mListener = listener;
+        mNextImportantNoticeVersion = ImportantNoticeUtils.getNextImportantNoticeVersion(context);
+        setMessage(ImportantNoticeUtils.getNextImportantNoticeContents(context));
+        // Create buttons and set listeners.
+        setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
+        if (shouldHaveSettingsButton()) {
+            setButton(BUTTON_NEGATIVE, context.getString(R.string.go_to_settings), this);
+        }
+        // Set listeners.
+        setOnShowListener(this);
+        setOnDismissListener(this);
+    }
+
+    private boolean shouldHaveSettingsButton() {
+        return mNextImportantNoticeVersion
+                == ImportantNoticeUtils.VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS;
+    }
+
+    @Override
+    public void onShow(final DialogInterface dialog) {
+        ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext());
+    }
+
+    @Override
+    public void onClick(final DialogInterface dialog, final int which) {
+        if (shouldHaveSettingsButton() && which == BUTTON_NEGATIVE) {
+            mListener.onClickSettingsOfImportantNoticeDialog(mNextImportantNoticeVersion);
+        }
+    }
+
+    @Override
+    public void onDismiss(final DialogInterface dialog) {
+        mListener.onDismissImportantNoticeDialog(mNextImportantNoticeVersion);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 47a3e94..9ded5a8 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -26,8 +26,6 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
-import android.content.DialogInterface.OnDismissListener;
-import android.content.DialogInterface.OnShowListener;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
@@ -100,7 +98,8 @@
  */
 public class LatinIME extends InputMethodService implements KeyboardActionListener,
         SuggestionStripView.Listener, SuggestionStripViewAccessor,
-        DictionaryFacilitatorForSuggest.DictionaryInitializationListener {
+        DictionaryFacilitatorForSuggest.DictionaryInitializationListener,
+        ImportantNoticeDialog.ImportantNoticeDialogListener {
     private static final String TAG = LatinIME.class.getSimpleName();
     private static final boolean TRACE = false;
     private static boolean DEBUG = false;
@@ -799,19 +798,22 @@
             suggest = mInputLogic.mSuggest;
         }
 
-        // Sometimes, while rotating, for some reason the framework tells the app we are not
-        // connected to it and that means we can't refresh the cache. In this case, schedule a
-        // refresh later.
         // TODO[IL]: Can the following be moved to InputLogic#startInput?
         final boolean canReachInputConnection;
         if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
                 editorInfo.initialSelStart, editorInfo.initialSelEnd,
                 false /* shouldFinishComposition */)) {
+            // Sometimes, while rotating, for some reason the framework tells the app we are not
+            // connected to it and that means we can't refresh the cache. In this case, schedule a
+            // refresh later.
             // We try resetting the caches up to 5 times before giving up.
             mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
             // mLastSelection{Start,End} are reset later in this method, don't need to do it here
             canReachInputConnection = false;
         } else {
+            // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best
+            // effort to work around this bug.
+            mInputLogic.mConnection.tryFixLyingCursorPosition();
             if (isDifferentTextField) {
                 mHandler.postResumeSuggestions();
             }
@@ -1177,39 +1179,23 @@
         mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit);
     }
 
-    // TODO: Move this method out of {@link LatinIME}.
     // Callback for the {@link SuggestionStripView}, to call when the important notice strip is
     // pressed.
     @Override
     public void showImportantNoticeContents() {
-        final Context context = this;
-        final AlertDialog.Builder builder =
-                new AlertDialog.Builder(context, AlertDialog.THEME_HOLO_DARK);
-        builder.setMessage(ImportantNoticeUtils.getNextImportantNoticeContents(context));
-        builder.setPositiveButton(android.R.string.ok, null /* listener */);
-        final OnClickListener onClickListener = new OnClickListener() {
-            @Override
-            public void onClick(final DialogInterface dialog, final int position) {
-                if (position == DialogInterface.BUTTON_NEGATIVE) {
-                    launchSettings();
-                }
-            }
-        };
-        builder.setNegativeButton(R.string.go_to_settings, onClickListener);
-        final AlertDialog importantNoticeDialog = builder.create();
-        importantNoticeDialog.setOnShowListener(new OnShowListener() {
-            @Override
-            public void onShow(final DialogInterface dialog) {
-                ImportantNoticeUtils.updateLastImportantNoticeVersion(context);
-            }
-        });
-        importantNoticeDialog.setOnDismissListener(new OnDismissListener() {
-            @Override
-            public void onDismiss(final DialogInterface dialog) {
-                setNeutralSuggestionStrip();
-            }
-        });
-        showOptionDialog(importantNoticeDialog);
+        showOptionDialog(new ImportantNoticeDialog(this /* context */, this /* listener */));
+    }
+
+    // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
+    @Override
+    public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) {
+        launchSettings();
+    }
+
+    // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
+    @Override
+    public void onDismissImportantNoticeDialog(final int nextVersion) {
+        setNeutralSuggestionStrip();
     }
 
     public void displaySettingsDialog() {
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index cc2db4c..0e85b3c 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -172,20 +172,6 @@
             Log.d(TAG, "Will try to retrieve text later.");
             return false;
         }
-        final int lengthOfTextBeforeCursor = mCommittedTextBeforeComposingText.length();
-        if (lengthOfTextBeforeCursor > newSelStart
-                || (newSelStart != lengthOfTextBeforeCursor
-                        && lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
-                        && newSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
-            // newSelStart and newSelEnd may be lying -- when rotating the device (probably a
-            // framework bug). If the values don't agree and we have less chars than we asked
-            // for, then we know how many chars we have. If we got more than newSelStart says, then
-            // we also know it was lying. In both cases the length is more reliable. Note that we
-            // only have to check newSelStart (not newSelEnd) since if newSelEnd is wrong, then
-            // newSelStart will be wrong as well.
-            mExpectedSelStart = lengthOfTextBeforeCursor;
-            mExpectedSelEnd = lengthOfTextBeforeCursor;
-        }
         if (null != mIC && shouldFinishComposition) {
             mIC.finishComposingText();
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 52a6f5f..eeb5bf5 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -330,7 +330,13 @@
             // Another option would be to send suggestions each time we set the composing
             // text, but that is probably too expensive to do, so we decided to leave things
             // as is.
-            resetEntireInputState(settingsValues, newSelStart, newSelEnd);
+            // Also, we're posting a resume suggestions message, and this will update the
+            // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here
+            // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear
+            // it here, which means we'll keep outdated suggestions for a split second but the
+            // visual result is better.
+            resetEntireInputState(settingsValues, newSelStart, newSelEnd,
+                    false /* clearSuggestionStrip */);
         } else {
             // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
             // composition to end. But in all cases where we don't reset the entire input
@@ -498,7 +504,7 @@
                 // If we are in the middle of a recorrection, we need to commit the recorrection
                 // first so that we can insert the batch input at the current cursor position.
                 resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
-                        mConnection.getExpectedSelectionEnd());
+                        mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             } else if (wordComposerSize <= 1) {
                 // We auto-correct the previous (typed, not gestured) string iff it's one character
                 // long. The reason for this is, even in the middle of gesture typing, you'll still
@@ -651,7 +657,7 @@
                     // If we are in the middle of a recorrection, we need to commit the recorrection
                     // first so that we can insert the character at the current cursor position.
                     resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
-                            mConnection.getExpectedSelectionEnd());
+                            mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
                 } else {
                     commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
                 }
@@ -693,7 +699,7 @@
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can insert the character at the current cursor position.
             resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
-                    mConnection.getExpectedSelectionEnd());
+                    mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             isComposingWord = false;
         }
         // We want to find out whether to start composing a new word with this character. If so,
@@ -775,7 +781,7 @@
             // 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.
             resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
-                    mConnection.getExpectedSelectionEnd());
+                    mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
         }
         // isComposingWord() may have changed since we stored wasComposing
         if (mWordComposer.isComposingWord()) {
@@ -881,7 +887,7 @@
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can remove the character at the current cursor position.
             resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
-                    mConnection.getExpectedSelectionEnd());
+                    mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
         }
         if (mWordComposer.isComposingWord()) {
@@ -1252,18 +1258,28 @@
         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
         // recorrection. This is a temporary, stopgap measure that will be removed later.
         // TODO: remove this.
-        if (settingsValues.isBrokenByRecorrection()) return;
+        if (settingsValues.isBrokenByRecorrection()
         // Recorrection is not supported in languages without spaces because we don't know
         // how to segment them yet.
-        if (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) return;
+                || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
         // If no suggestions are requested, don't try restarting suggestions.
-        if (!settingsValues.isSuggestionsRequested()) return;
+                || !settingsValues.isSuggestionsRequested()
         // If the cursor is not touching a word, or if there is a selection, return right away.
-        if (mConnection.hasSelection()) return;
+                || mConnection.hasSelection()
         // If we don't know the cursor location, return.
-        if (mConnection.getExpectedSelectionStart() < 0) return;
+                || mConnection.getExpectedSelectionStart() < 0) {
+            mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
+            return;
+        }
         final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
-        if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) return;
+        if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) {
+            // Show predictions.
+            mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                    WordComposer.CAPS_MODE_OFF,
+                    getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, 1));
+            mLatinIME.mHandler.postUpdateSuggestionStrip();
+            return;
+        }
         final TextRange range = mConnection.getWordRangeAtCursor(
                 settingsValues.mSpacingAndPunctuations.mSortedWordSeparators,
                 0 /* additionalPrecedingWordsCount */);
@@ -1606,14 +1622,17 @@
      * @param settingsValues the current values of the settings.
      * @param newSelStart the new selection start, in java characters.
      * @param newSelEnd the new selection end, in java characters.
+     * @param clearSuggestionStrip whether this method should clear the suggestion strip.
      */
     // TODO: how is this different from startInput ?!
     // TODO: remove all references to this in LatinIME and make this private
     public void resetEntireInputState(final SettingsValues settingsValues,
-            final int newSelStart, final int newSelEnd) {
+            final int newSelStart, final int newSelEnd, final boolean clearSuggestionStrip) {
         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
         resetComposingState(true /* alsoResetLastComposedWord */);
-        mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
+        if (clearSuggestionStrip) {
+            mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
+        }
         mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
                 shouldFinishComposition);
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
index 6b0bb86..ca8bef3 100644
--- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -60,7 +60,7 @@
         return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
     }
 
-    public static int getCurrentImportantNoticeVersion(final Context context) {
+    private static int getCurrentImportantNoticeVersion(final Context context) {
         return context.getResources().getInteger(R.integer.config_important_notice_version);
     }
 
@@ -68,7 +68,7 @@
         return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0);
     }
 
-    private static int getNextImportantNoticeVersion(final Context context) {
+    public static int getNextImportantNoticeVersion(final Context context) {
         return getLastImportantNoticeVersion(context) + 1;
     }
 
@@ -92,23 +92,23 @@
                 .apply();
     }
 
-    // TODO: Make title resource to string array indexed by version.
     public static String getNextImportantNoticeTitle(final Context context) {
-        switch (getNextImportantNoticeVersion(context)) {
-        case VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS:
-            return context.getString(R.string.important_notice_title);
-        default:
-            return null;
+        final int nextVersion = getCurrentImportantNoticeVersion(context);
+        final String[] importantNoticeTitleArray = context.getResources().getStringArray(
+                R.array.important_notice_title_array);
+        if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) {
+            return importantNoticeTitleArray[nextVersion];
         }
+        return null;
     }
 
-    // TODO: Make content resource to string array indexed by version.
     public static String getNextImportantNoticeContents(final Context context) {
-        switch (getNextImportantNoticeVersion(context)) {
-        case VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS:
-            return context.getString(R.string.important_notice_contents);
-        default:
-            return null;
+        final int nextVersion = getNextImportantNoticeVersion(context);
+        final String[] importantNoticeContentsArray = context.getResources().getStringArray(
+                R.array.important_notice_contents_array);
+        if (nextVersion > 0 && nextVersion < importantNoticeContentsArray.length) {
+            return importantNoticeContentsArray[nextVersion];
         }
+        return null;
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index 449030c..b1239f0 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -50,8 +50,7 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mCurrentTime = 0;
-        setCurrentTimeForTestMode(mCurrentTime);
+        resetCurrentTimeForTestMode();
     }
 
     @Override
@@ -60,9 +59,14 @@
         super.tearDown();
     }
 
+    private void resetCurrentTimeForTestMode() {
+        mCurrentTime = 0;
+        setCurrentTimeForTestMode(mCurrentTime);
+    }
+
     private void forcePassingShortTime() {
-        // 4 days.
-        final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(4);
+        // 3 days.
+        final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3);
         mCurrentTime += timeToElapse;
         setCurrentTimeForTestMode(mCurrentTime);
     }
@@ -250,10 +254,12 @@
         final Locale dummyLocale = new Locale("test_decaying" + System.currentTimeMillis());
         final int numberOfWords = 5000;
         final Random random = new Random(123456);
+        resetCurrentTimeForTestMode();
         clearHistory(dummyLocale);
         final List<String> words = generateWords(numberOfWords, random);
         final UserHistoryDictionary dict =
                 PersonalizationHelper.getUserHistoryDictionary(getContext(), dummyLocale);
+        dict.waitAllTasksForTests();
         String prevWord = null;
         for (final String word : words) {
             dict.addToDictionary(prevWord, word, true, mCurrentTime);
@@ -261,10 +267,14 @@
             assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
         }
         forcePassingShortTime();
+        dict.decayIfNeeded();
+        dict.waitAllTasksForTests();
         for (final String word : words) {
             assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
         }
         forcePassingLongTime();
+        dict.decayIfNeeded();
+        dict.waitAllTasksForTests();
         for (final String word : words) {
             assertFalse(dict.isInUnderlyingBinaryDictionaryForTests(word));
         }