Merge "Remove the dialog to insert words to the dictionary."
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 562e1d0..42f7136 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -422,7 +422,7 @@
 
     private static void reinitializeClientRecordInDictionaryContentProvider(final Context context,
             final ContentProviderClient client, final String clientId) throws RemoteException {
-        final String metadataFileUri = context.getString(R.string.dictionary_pack_metadata_uri);
+        final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
         if (TextUtils.isEmpty(metadataFileUri)) return;
         // Tell the content provider to reset all information about this client id
         final Uri metadataContentUri = getProviderUriBuilder(clientId)
diff --git a/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java
new file mode 100644
index 0000000..792a446
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+import android.view.inputmethod.CompletionInfo;
+
+import java.util.Arrays;
+
+/**
+ * Utilities to do various stuff with CompletionInfo.
+ */
+public class CompletionInfoUtils {
+    private CompletionInfoUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static CompletionInfo[] removeNulls(final CompletionInfo[] src) {
+        int j = 0;
+        final CompletionInfo[] dst = new CompletionInfo[src.length];
+        for (int i = 0; i < src.length; ++i) {
+            if (null != src[i] && !TextUtils.isEmpty(src[i].getText())) {
+                dst[j] = src[i];
+                ++j;
+            }
+        }
+        return Arrays.copyOfRange(dst, 0, j);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 8b5a76a..22d1899 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -173,7 +173,8 @@
                 // capitalization of i.
                 final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
-                    super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
+                    super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
+                            false /* isNotAWord */);
                     if (!TextUtils.isEmpty(prevWord)) {
                         if (mUseFirstLastBigrams) {
                             super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index ff3d83f..9691fa2 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -37,6 +37,8 @@
     public static final String TYPE_USER = "user";
     // User history dictionary internal to LatinIME.
     public static final String TYPE_USER_HISTORY = "history";
+    // Spawned by resuming suggestions. Comes from a span that was in the TextView.
+    public static final String TYPE_RESUMED = "resumed";
     protected final String mDictType;
 
     public Dictionary(final String dictType) {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 97dc6a8..4b1975a 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -176,14 +176,15 @@
      */
     // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
     // considering performance regression.
-    protected void addWord(final String word, final String shortcutTarget, final int frequency) {
+    protected void addWord(final String word, final String shortcutTarget, final int frequency,
+            final boolean isNotAWord) {
         if (shortcutTarget == null) {
-            mFusionDictionary.add(word, frequency, null, false /* isNotAWord */);
+            mFusionDictionary.add(word, frequency, null, isNotAWord);
         } else {
             // TODO: Do this in the subclass, with this class taking an arraylist.
             final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
             shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
-            mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */);
+            mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f08e107..094ccd7 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -44,7 +44,9 @@
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.text.InputType;
+import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
@@ -72,6 +74,7 @@
 import com.android.inputmethod.keyboard.KeyboardId;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.RichInputConnection.Range;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.Utils.Stats;
 import com.android.inputmethod.latin.define.ProductionFlag;
@@ -197,6 +200,7 @@
         private static final int MSG_PENDING_IMS_CALLBACK = 1;
         private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
         private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
+        private static final int MSG_RESUME_SUGGESTIONS = 4;
 
         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
 
@@ -234,6 +238,9 @@
                 latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
                         msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
                 break;
+            case MSG_RESUME_SUGGESTIONS:
+                latinIme.restartSuggestionsOnWordTouchedByCursor();
+                break;
             }
         }
 
@@ -241,6 +248,10 @@
             sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
         }
 
+        public void postResumeSuggestions() {
+            sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
+        }
+
         public void cancelUpdateSuggestionStrip() {
             removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
@@ -910,13 +921,12 @@
                 resetEntireInputState(newSelStart);
             }
 
+            // We moved the cursor. If we are touching a word, we need to resume suggestion.
+            mHandler.postResumeSuggestions();
+
             mKeyboardSwitcher.updateShiftState();
         }
         mExpectingUpdateSelection = false;
-        // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
-        // here. It would probably be too expensive to call directly here but we may want to post a
-        // message to delay it. The point would be to unify behavior between backspace to the
-        // end of a word and manually put the pointer at the end of the word.
 
         // Make a note of the cursor position
         mLastSelectionStart = newSelStart;
@@ -983,7 +993,8 @@
             }
         }
         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
-        mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+        mApplicationSpecifiedCompletions =
+                CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
         if (applicationSpecifiedCompletions == null) {
             clearSuggestionStrip();
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -1723,6 +1734,9 @@
         // during key repeat.
         mHandler.postUpdateShiftState();
 
+        if (mWordComposer.isComposingWord() && !mWordComposer.isCursorAtEndOfComposingWord()) {
+            resetEntireInputState(mLastSelectionStart);
+        }
         if (mWordComposer.isComposingWord()) {
             final int length = mWordComposer.size();
             if (length > 0) {
@@ -1854,6 +1868,10 @@
             promotePhantomSpace();
         }
 
+        if (mWordComposer.isComposingWord() && !mWordComposer.isCursorAtEndOfComposingWord()) {
+            resetEntireInputState(mLastSelectionStart);
+            isComposingWord = false;
+        }
         // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
         // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
         // thread here.
@@ -2327,6 +2345,48 @@
     }
 
     /**
+     * Check if the cursor is touching a word. If so, restart suggestions on this word, else
+     * do nothing.
+     */
+    private void restartSuggestionsOnWordTouchedByCursor() {
+        // If the cursor is not touching a word, or if there is a selection, return right away.
+        if (mLastSelectionStart != mLastSelectionEnd) return;
+        if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
+        final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
+                0 /* additionalPrecedingWordsCount */);
+        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+        if (range.mWord instanceof SpannableString) {
+            final SpannableString spannableString = (SpannableString)range.mWord;
+            final String typedWord = spannableString.toString();
+            int i = 0;
+            for (Object object : spannableString.getSpans(0, spannableString.length(),
+                    SuggestionSpan.class)) {
+                SuggestionSpan span = (SuggestionSpan)object;
+                for (String s : span.getSuggestions()) {
+                    ++i;
+                    if (!TextUtils.equals(s, typedWord)) {
+                        suggestions.add(new SuggestedWordInfo(s,
+                                SuggestionStripView.MAX_SUGGESTIONS - i,
+                                SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED));
+                    }
+                }
+            }
+        }
+        mWordComposer.setComposingWord(range.mWord, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setCursorPositionWithinWord(range.mCharsBefore);
+        mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore,
+                mLastSelectionEnd + range.mCharsAfter);
+        if (suggestions.isEmpty()) {
+            suggestions.add(new SuggestedWordInfo(range.mWord.toString(), 1,
+                    SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_RESUMED));
+        }
+        showSuggestionStrip(new SuggestedWords(suggestions,
+                true /* typedWordValid */, false /* willAutoCorrect */,
+                false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
+                false /* isPrediction */), range.mWord.toString());
+    }
+
+    /**
      * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
      * word, else do nothing.
      */
@@ -2334,17 +2394,18 @@
         final CharSequence word =
                 mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
         if (null != word) {
-            restartSuggestionsOnWordBeforeCursor(word);
+            final String wordString = word.toString();
+            restartSuggestionsOnWordBeforeCursor(wordString);
             // TODO: Handle the case where the user manually moves the cursor and then backs up over
             // a separator.  In that case, the current log unit should not be uncommitted.
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.getInstance().uncommitCurrentLogUnit(word.toString(),
+                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
                         true /* dumpCurrentLogUnit */);
             }
         }
     }
 
-    private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
+    private void restartSuggestionsOnWordBeforeCursor(final String word) {
         mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
         mConnection.deleteSurroundingText(length, 0);
diff --git a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
new file mode 100644
index 0000000..e6dc6db
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+import android.content.Context;
+
+/**
+ * Helper class to get the metadata URI.
+ */
+public class MetadataFileUriGetter {
+    public static String getMetadataUri(Context context) {
+        return context.getString(R.string.dictionary_pack_metadata_uri);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 16744d1..b74ea59 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.latin;
 
 import android.inputmethodservice.InputMethodService;
+import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -392,7 +394,9 @@
     public void commitCompletion(final CompletionInfo completionInfo) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
-        final CharSequence text = completionInfo.getText();
+        CharSequence text = completionInfo.getText();
+        // text should never be null, but just in case, it's better to insert nothing than to crash
+        if (null == text) text = "";
         mCommittedTextBeforeComposingText.append(text);
         mCurrentCursorPosition += text.length() - mComposingText.length();
         mComposingText.setLength(0);
@@ -442,9 +446,9 @@
         public final int mCharsAfter;
 
         /** The actual characters that make up a word */
-        public final String mWord;
+        public final CharSequence mWord;
 
-        public Range(int charsBefore, int charsAfter, String word) {
+        public Range(int charsBefore, int charsAfter, CharSequence word) {
             if (charsBefore < 0 || charsAfter < 0) {
                 throw new IndexOutOfBoundsException();
             }
@@ -498,7 +502,7 @@
      *   separator. For example, if the field contains "he|llo world", where |
      *   represents the cursor, then "hello " will be returned.
      */
-    public String getWordAtCursor(String separators) {
+    public CharSequence getWordAtCursor(String separators) {
         // getWordRangeAtCursor returns null if the connection is null
         Range r = getWordRangeAtCursor(separators, 0);
         return (r == null) ? null : r.mWord;
@@ -517,8 +521,10 @@
         if (mIC == null || sep == null) {
             return null;
         }
-        final CharSequence before = mIC.getTextBeforeCursor(1000, 0);
-        final CharSequence after = mIC.getTextAfterCursor(1000, 0);
+        final CharSequence before = mIC.getTextBeforeCursor(1000,
+                InputConnection.GET_TEXT_WITH_STYLES);
+        final CharSequence after = mIC.getTextAfterCursor(1000,
+                InputConnection.GET_TEXT_WITH_STYLES);
         if (before == null || after == null) {
             return null;
         }
@@ -560,8 +566,9 @@
             }
         }
 
-        final String word = before.toString().substring(startIndexInBefore, before.length())
-                + after.toString().substring(0, endIndexInAfter);
+        final SpannableString word = new SpannableString(TextUtils.concat(
+                before.subSequence(startIndexInBefore, before.length()),
+                after.subSequence(0, endIndexInAfter)));
         return new Range(before.length() - startIndexInBefore, endIndexInAfter, word);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 8fbe843..318d2b2 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -134,6 +134,10 @@
         return mSettingsValues.mIsInternal;
     }
 
+    public String getWordSeparators() {
+        return mSettingsValues.mWordSeparators;
+    }
+
     // Accessed from the settings interface, hence public
     public static boolean readKeypressSoundEnabled(final SharedPreferences prefs,
             final Resources res) {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 3d6fe2d..158cc11 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -131,6 +131,7 @@
         public static final int KIND_APP_DEFINED = 6; // Suggested by the application
         public static final int KIND_SHORTCUT = 7; // A shortcut
         public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
+        public static final int KIND_RESUMED = 9; // A resumed suggestion (comes from a span)
         public final String mWord;
         public final int mScore;
         public final int mKind; // one of the KIND_* constants above
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 8d2e762..90f9297 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -252,10 +252,10 @@
                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
-                    super.addWord(word, null, adjustedFrequency);
+                    super.addWord(word, null, adjustedFrequency, false /* isNotAWord */);
                 }
                 if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
-                    super.addWord(shortcut, word, adjustedFrequency);
+                    super.addWord(shortcut, word, adjustedFrequency, true /* isNotAWord */);
                 }
                 cursor.moveToNext();
             }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index f7cb434..1af1242 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -49,6 +49,7 @@
     private int mCapitalizedMode;
     private int mTrailingSingleQuotesCount;
     private int mCodePointSize;
+    private int mCursorPositionWithinWord;
 
     /**
      * Whether the user chose to capitalize the first char of the word.
@@ -62,6 +63,7 @@
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
         mIsBatchMode = false;
+        mCursorPositionWithinWord = 0;
         refreshSize();
     }
 
@@ -76,6 +78,7 @@
         mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
         mIsResumed = source.mIsResumed;
         mIsBatchMode = source.mIsBatchMode;
+        mCursorPositionWithinWord = source.mCursorPositionWithinWord;
         refreshSize();
     }
 
@@ -91,6 +94,7 @@
         mTrailingSingleQuotesCount = 0;
         mIsResumed = false;
         mIsBatchMode = false;
+        mCursorPositionWithinWord = 0;
         refreshSize();
     }
 
@@ -135,6 +139,7 @@
         final int newIndex = size();
         mTypedWord.appendCodePoint(primaryCode);
         refreshSize();
+        mCursorPositionWithinWord = mCodePointSize;
         if (newIndex < MAX_WORD_LENGTH) {
             mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE
                     ? Character.toLowerCase(primaryCode) : primaryCode;
@@ -158,6 +163,14 @@
         mAutoCorrection = null;
     }
 
+    public void setCursorPositionWithinWord(final int posWithinWord) {
+        mCursorPositionWithinWord = posWithinWord;
+    }
+
+    public boolean isCursorAtEndOfComposingWord() {
+        return mCursorPositionWithinWord == mCodePointSize;
+    }
+
     public void setBatchInputPointers(final InputPointers batchPointers) {
         mInputPointers.set(batchPointers);
         mIsBatchMode = true;
@@ -242,6 +255,7 @@
                 ++mTrailingSingleQuotesCount;
             }
         }
+        mCursorPositionWithinWord = mCodePointSize;
         mAutoCorrection = null;
     }
 
@@ -368,6 +382,7 @@
         mCapitalizedMode = CAPS_MODE_OFF;
         refreshSize();
         mAutoCorrection = null;
+        mCursorPositionWithinWord = 0;
         mIsResumed = false;
         return lastComposedWord;
     }
@@ -380,6 +395,7 @@
         refreshSize();
         mCapitalizedMode = lastComposedWord.mCapitalizedMode;
         mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
+        mCursorPositionWithinWord = mCodePointSize;
         mIsResumed = true;
     }
 
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index c05de09..320db81 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -1283,7 +1283,7 @@
         if (connection != null) {
             Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
             if (range != null) {
-                word = range.mWord;
+                word = range.mWord.toString();
             }
         }
         final ResearchLogger researchLogger = getInstance();
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 136c4e5..becd6c1 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -42,8 +42,9 @@
 class Suggest : public SuggestInterface {
  public:
     AK_FORCE_INLINE Suggest(const SuggestPolicy *const suggestPolicy)
-            : TRAVERSAL(suggestPolicy->getTraversal()),
-              SCORING(suggestPolicy->getScoring()), WEIGHTING(suggestPolicy->getWeighting()) {}
+            : TRAVERSAL(suggestPolicy ? suggestPolicy->getTraversal() : 0),
+              SCORING(suggestPolicy ? suggestPolicy->getScoring() : 0),
+              WEIGHTING(suggestPolicy ? suggestPolicy->getWeighting() : 0) {}
     AK_FORCE_INLINE virtual ~Suggest() {}
     int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
             int *times, int *pointerIds, int *inputCodePoints, int inputSize, int commitPoint,
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 4583eab..9e107a4 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -162,45 +162,22 @@
     // on the same thread that the tests are running on to mimic the actual environment as
     // closely as possible.
     // Now, Looper#loop() never exits in normal operation unless the Looper#quit() method
-    // is called, so we need to do that at the right time so that #loop() returns at some
-    // point and we don't end up in an infinite loop.
-    // After we quit, the looper is still technically ready to process more messages but
-    // the handler will refuse to enqueue any because #quit() has been called and it
-    // explicitly tests for it on message enqueuing, so we'll have to reset it so that
-    // it lets us continue normal operation.
+    // is called, which has a lot of bad side effects. We can however just throw an exception
+    // in the runnable which will unwind the stack and allow us to exit.
+    private final class InterruptRunMessagesException extends RuntimeException {
+        // Empty class
+    }
     protected void runMessages() {
-        // Here begins deep magic.
-        final Looper looper = mLatinIME.mHandler.getLooper();
         mLatinIME.mHandler.post(new Runnable() {
                 @Override
                 public void run() {
-                    looper.quit();
+                    throw new InterruptRunMessagesException();
                 }
             });
-        // The only way to get out of Looper#loop() is to call #quit() on it (or on its queue).
-        // Once #quit() is called remaining messages are not processed, which is why we post
-        // a message that calls it instead of calling it directly.
-        Looper.loop();
-
-        // Once #quit() has been called, the looper is not functional any more (it used to be,
-        // but now it SIGSEGV's if it's used again).
-        // It won't accept creating a new looper for this thread and switching to it...
-        // ...unless we can trick it into throwing out the old looper and believing it hasn't
-        // been initialized before.
-        MessageQueue queue = Looper.myQueue();
         try {
-            // However there is no way of doing it externally, and the static ThreadLocal
-            // field into which it's stored is private.
-            // So... get out the big guns.
-            java.lang.reflect.Field f = Looper.class.getDeclaredField("sThreadLocal");
-            f.setAccessible(true); // private lolwut
-            final ThreadLocal<Looper> a = (ThreadLocal<Looper>) f.get(looper);
-            a.set(null);
-            looper.prepare();
-        } catch (NoSuchFieldException e) {
-            throw new RuntimeException(e);
-        } catch (IllegalAccessException e) {
-            throw new RuntimeException(e);
+            Looper.loop();
+        } catch (InterruptRunMessagesException e) {
+            // Resume normal operation
         }
     }