diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
29 files changed, 864 insertions, 194 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 562e1d0b7..42f713697 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -422,7 +422,7 @@ public final class BinaryDictionaryFileDumper { 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/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java index 1012cd519..4b8d1ac11 100644 --- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java @@ -41,7 +41,7 @@ public final class CapsModeUtils { if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) { return s.toUpperCase(locale); } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) { - return StringUtils.toTitleCase(s, locale); + return StringUtils.capitalizeFirstCodePoint(s, locale); } else { return s; } 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 000000000..792a446c9 --- /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/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 50e50233e..86bb25562 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -160,6 +160,8 @@ public final class Constants { public static final int CODE_DOUBLE_QUOTE = '"'; public static final int CODE_QUESTION_MARK = '?'; public static final int CODE_EXCLAMATION_MARK = '!'; + public static final int CODE_SLASH = '/'; + public static final int CODE_COMMERCIAL_AT = '@'; // TODO: Check how this should work for right-to-left languages. It seems to stand // that for rtl languages, a closing parenthesis is a left parenthesis. Is this // managed by the font? Or is it a different char? diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 8b5a76a17..75c2cf2c8 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -173,7 +173,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // 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); @@ -251,7 +252,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } private static boolean isValidName(final String name) { - if (name != null && -1 == name.indexOf('@')) { + if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) { return true; } return false; diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index ff3d83fad..9691fa231 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -37,6 +37,8 @@ public abstract class Dictionary { 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 97dc6a8ac..4b1975a00 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -176,14 +176,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ // 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/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java index ecb20144b..46194f6e4 100644 --- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java +++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java @@ -33,7 +33,6 @@ public final class InputTypeUtils implements InputType { private static final int[] SUPPRESSING_AUTO_SPACES_FIELD_VARIATION = { InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, InputType.TYPE_TEXT_VARIATION_PASSWORD, - InputType.TYPE_TEXT_VARIATION_URI, InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD }; public static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1; diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 56b1c786e..efa51569f 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.Message; 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.KeyboardActionListener; 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; @@ -158,6 +161,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); private final RichInputConnection mConnection = new RichInputConnection(this); + private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); // Keep track of the last selection range to decide if we need to show word alternatives private static final int NOT_A_CURSOR_POSITION = -1; @@ -197,6 +201,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction 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 +239,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); break; + case MSG_RESUME_SUGGESTIONS: + latinIme.restartSuggestionsOnWordTouchedByCursor(); + break; } } @@ -241,6 +249,10 @@ public final class LatinIME extends InputMethodService implements KeyboardAction sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); } + public void postResumeSuggestions() { + sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); + } + public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } @@ -427,6 +439,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction mHandler.onCreate(); DEBUG = LatinImeLogger.sDBG; + // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. loadSettings(); initSuggest(); @@ -464,6 +477,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction final InputAttributes inputAttributes = new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); mSettings.loadSettings(locale, inputAttributes); + // May need to reset the contacts dictionary depending on the user settings. resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); } @@ -730,8 +744,14 @@ public final class LatinIME extends InputMethodService implements KeyboardAction resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SPACE_STATE_NONE; + mRecapitalizeStatus.deactivate(); mCurrentlyPressedHardwareKeys.clear(); + // Note: the following does a round-trip IPC on the main thread: be careful + final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + if (null != mSuggest && null != currentLocale && !currentLocale.equals(mSuggest.mLocale)) { + initSuggest(); + } if (mSuggestionStripView != null) { // This will set the punctuation suggestions if next word suggestion is off; // otherwise it will clear the suggestion strip. @@ -784,8 +804,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // to the user dictionary. if (null != mPositionalInfoForUserDictPendingAddition && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( - mConnection, editorInfo, mLastSelectionEnd, - mSubtypeSwitcher.getCurrentSubtypeLocale())) { + mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { mPositionalInfoForUserDictPendingAddition = null; } // If tryReplaceWithActualWord returns false, we don't know what word was @@ -910,13 +929,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction resetEntireInputState(newSelStart); } + // We moved the cursor. If we are touching a word, we need to resume suggestion. + mHandler.postResumeSuggestions(); + // Reset the last recapitalization. + mRecapitalizeStatus.deactivate(); 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 +1002,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; - mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestionStrip(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { @@ -991,6 +1009,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } return; } + mApplicationSpecifiedCompletions = + CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( @@ -1166,6 +1186,15 @@ public final class LatinIME extends InputMethodService implements KeyboardAction SPACE_STATE_PHANTOM == mSpaceState); } + public int getCurrentRecapitalizeState() { + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + // Not recapitalizing at the moment + return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; + } + return mRecapitalizeStatus.getCurrentMode(); + } + // Factor in auto-caps and manual caps and compute the current caps mode. private int getActualCapsMode() { final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); @@ -1243,10 +1272,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } else { wordToEdit = word; } - mPositionalInfoForUserDictPendingAddition = - new PositionalInfoForUserDictPendingAddition( - wordToEdit, mLastSelectionEnd, getCurrentInputEditorInfo(), - mLastComposedWord.mCapitalizedMode); mUserDictionary.addWordToUserDictionary(wordToEdit); } @@ -1380,8 +1405,18 @@ public final class LatinIME extends InputMethodService implements KeyboardAction LatinImeLogger.logOnDelete(x, y); break; case Constants.CODE_SHIFT: + // Note: calling back to the keyboard on Shift key is handled in onPressKey() + // and onReleaseKey(). + final Keyboard currentKeyboard = switcher.getKeyboard(); + if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout. + handleRecapitalize(); + } + break; case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Shift and symbol key is handled in onPressKey() and onReleaseKey(). + // Note: calling back to the keyboard on symbol key is handled in onPressKey() + // and onReleaseKey(). break; case Constants.CODE_SETTINGS: onSettingsKeyPressed(); @@ -1459,7 +1494,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction "", mWordComposer.getTypedWord(), " ", mWordComposer); } } - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } else { + commitTyped(LastComposedWord.NOT_A_SEPARATOR); + } } final int keyX, keyY; final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); @@ -1515,8 +1556,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } final int wordComposerSize = mWordComposer.size(); // Since isComposingWord() is true, the size is at least 1. - final int lastChar = mWordComposer.getCodeAt(wordComposerSize - 1); - if (wordComposerSize <= 1) { + final int lastChar = mWordComposer.getCodeBeforeCursor(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the batch input at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } 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 // tap one-letter words and you want them auto-corrected (typically, "i" in English @@ -1540,7 +1585,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { + if (Character.isLetter(codePointBeforeCursor) + || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { mSpaceState = SPACE_STATE_PHANTOM; } } @@ -1551,7 +1597,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private static final class BatchInputUpdater implements Handler.Callback { private final Handler mHandler; private LatinIME mLatinIme; - private boolean mInBatchInput; // synchronized using "this". + private final Object mLock = new Object(); + private boolean mInBatchInput; // synchronized using {@link #mLock}. private BatchInputUpdater() { final HandlerThread handlerThread = new HandlerThread( @@ -1582,21 +1629,25 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } // Run in the UI thread. - public synchronized void onStartBatchInput(final LatinIME latinIme) { - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mLatinIme = latinIme; - mInBatchInput = true; + public void onStartBatchInput(final LatinIME latinIme) { + synchronized (mLock) { + mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + mLatinIme = latinIme; + mInBatchInput = true; + } } // Run in the Handler thread. - private synchronized void updateBatchInput(final InputPointers batchPointers) { - if (!mInBatchInput) { - // Batch input has ended or canceled while the message was being delivered. - return; + private void updateBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + if (!mInBatchInput) { + // Batch input has ended or canceled while the message was being delivered. + return; + } + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, false /* dismissGestureFloatingPreviewText */); } - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); } // Run in the UI thread. @@ -1609,19 +1660,23 @@ public final class LatinIME extends InputMethodService implements KeyboardAction .sendToTarget(); } - public synchronized void onCancelBatchInput() { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + public void onCancelBatchInput() { + synchronized (mLock) { + mInBatchInput = false; + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + } } // Run in the UI thread. - public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers) { - mInBatchInput = false; - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, true /* dismissGestureFloatingPreviewText */); - return suggestedWords; + public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + mInBatchInput = false; + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, true /* dismissGestureFloatingPreviewText */); + return suggestedWords; + } } // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to @@ -1717,6 +1772,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // during key repeat. mHandler.postUpdateShiftState(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // 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(mLastSelectionStart); + // When we exit this if-clause, mWordComposer.isComposingWord() will return false. + } if (mWordComposer.isComposingWord()) { final int length = mWordComposer.size(); if (length > 0) { @@ -1727,7 +1788,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction ResearchLogger.getInstance().uncommitCurrentLogUnit( word, false /* dumpCurrentLogUnit */); } + final String rejectedSuggestion = mWordComposer.getTypedWord(); mWordComposer.reset(); + mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); } else { mWordComposer.deleteLast(); } @@ -1848,6 +1911,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction promotePhantomSpace(); } + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + 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. @@ -1902,6 +1971,38 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } + private void handleRecapitalize() { + if (mLastSelectionStart == mLastSelectionEnd) return; // No selection + // If we have a recapitalize in progress, use it; otherwise, create a new one. + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, + mConnection.getSelectedText(0 /* flags, 0 for no styles */).toString(), + mSettings.getCurrentLocale(), mSettings.getWordSeparators()); + // We trim leading and trailing whitespace. + mRecapitalizeStatus.trim(); + // Trimming the object may have changed the length of the string, and we need to + // reposition the selection handles accordingly. As this result in an IPC call, + // only do it if it's actually necessary, in other words if the recapitalize status + // is not set at the same place as before. + if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + } + mRecapitalizeStatus.rotate(); + final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; + mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); + // Match the keyboard to the new state. + mKeyboardSwitcher.updateShiftState(); + } + // Returns true if we did an autocorrection, false otherwise. private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { @@ -1909,7 +2010,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } boolean didAutoCorrect = false; - // Handle separator + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the separator at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } if (mWordComposer.isComposingWord()) { if (mSettings.getCurrent().mCorrectionEnabled) { // TODO: maybe cache Strings in an <String> sparse array or something @@ -2321,6 +2426,73 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } /** + * 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 */); + if (null == range) return; // Happens if we don't have an input connection at all + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); + final String typedWord = range.mWord.toString(); + if (range.mWord instanceof SpannableString) { + final SpannableString spannableString = (SpannableString)range.mWord; + 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(typedWord, mKeyboardSwitcher.getKeyboard()); + mWordComposer.setCursorPositionWithinWord(range.mCharsBefore); + mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore, + mLastSelectionEnd + range.mCharsAfter); + final SuggestedWords suggestedWords; + if (suggestions.isEmpty()) { + // We come here if there weren't any suggestion spans on this word. We will try to + // compute suggestions for it instead. + final SuggestedWords suggestedWordsIncludingTypedWord = + getSuggestedWords(Suggest.SESSION_TYPING); + if (suggestedWordsIncludingTypedWord.size() > 1) { + // We were able to compute new suggestions for this word. + // Remove the typed word, since we don't want to display it in this case. + // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. + suggestedWords = + suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); + } else { + // No saved suggestions, and we were unable to compute any good one either. + // Rather than displaying an empty suggestion strip, we'll display the original + // word alone in the middle. + // Since there is only one word, willAutoCorrect is false. + suggestedWords = suggestedWordsIncludingTypedWord; + } + } else { + // We found suggestion spans in the word. We'll create the SuggestedWords out of + // them, and make willAutoCorrect false. + suggestedWords = new SuggestedWords(suggestions, + true /* typedWordValid */, false /* willAutoCorrect */, + false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, + false /* isPrediction */); + } + + // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. + // We never want to auto-correct on a resumed suggestion. Please refer to the three + // places above where suggestedWords is affected. + showSuggestionStrip(suggestedWords, typedWord); + } + + /** * Check if the cursor is actually at the end of a word. If so, restart suggestions on this * word, else do nothing. */ @@ -2328,17 +2500,18 @@ public final class LatinIME extends InputMethodService implements KeyboardAction 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); @@ -2392,7 +2565,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // This essentially inserts a space, and that's it. public void promotePhantomSpace() { - if (mSettings.getCurrent().shouldInsertSpacesAutomatically()) { + if (mSettings.getCurrent().shouldInsertSpacesAutomatically() + && !mConnection.textBeforeCursorLooksLikeURL()) { sendKeyCodePoint(Constants.CODE_SPACE); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_promotePhantomSpace(); @@ -2409,8 +2583,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // Outside LatinIME, only used by the {@link InputTestsBase} test suite. @UsedForTesting void loadKeyboard() { - // When the device locale is changed in SetupWizard etc., this method may get called via - // onConfigurationChanged before SoftInputWindow is shown. + // TODO: Why are we calling {@link #loadSettings()} and {@link #initSuggest()} in a + // different order than in {@link #onStartInputView}? initSuggest(); loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { 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 000000000..e6dc6db8f --- /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/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java new file mode 100644 index 000000000..8a704ab42 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java @@ -0,0 +1,189 @@ +/* + * 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 com.android.inputmethod.latin.StringUtils; + +import java.util.Locale; + +/** + * The status of the current recapitalize process. + */ +public class RecapitalizeStatus { + public static final int NOT_A_RECAPITALIZE_MODE = -1; + public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0; + public static final int CAPS_MODE_ALL_LOWER = 1; + public static final int CAPS_MODE_FIRST_WORD_UPPER = 2; + public static final int CAPS_MODE_ALL_UPPER = 3; + // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant. + public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER; + + private static final int[] ROTATION_STYLE = { + CAPS_MODE_ORIGINAL_MIXED_CASE, + CAPS_MODE_ALL_LOWER, + CAPS_MODE_FIRST_WORD_UPPER, + CAPS_MODE_ALL_UPPER + }; + + private static final int getStringMode(final String string, final String separators) { + if (StringUtils.isIdenticalAfterUpcase(string)) { + return CAPS_MODE_ALL_UPPER; + } else if (StringUtils.isIdenticalAfterDowncase(string)) { + return CAPS_MODE_ALL_LOWER; + } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) { + return CAPS_MODE_FIRST_WORD_UPPER; + } else { + return CAPS_MODE_ORIGINAL_MIXED_CASE; + } + } + + /** + * We store the location of the cursor and the string that was there before the recapitalize + * action was done, and the location of the cursor and the string that was there after. + */ + private int mCursorStartBefore; + private String mStringBefore; + private int mCursorStartAfter; + private int mCursorEndAfter; + private int mRotationStyleCurrentIndex; + private boolean mSkipOriginalMixedCaseMode; + private Locale mLocale; + private String mSeparators; + private String mStringAfter; + private boolean mIsActive; + + public RecapitalizeStatus() { + // By default, initialize with dummy values that won't match any real recapitalize. + initialize(-1, -1, "", Locale.getDefault(), ""); + deactivate(); + } + + public void initialize(final int cursorStart, final int cursorEnd, final String string, + final Locale locale, final String separators) { + mCursorStartBefore = cursorStart; + mStringBefore = string; + mCursorStartAfter = cursorStart; + mCursorEndAfter = cursorEnd; + mStringAfter = string; + final int initialMode = getStringMode(mStringBefore, separators); + mLocale = locale; + mSeparators = separators; + if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) { + mRotationStyleCurrentIndex = 0; + mSkipOriginalMixedCaseMode = false; + } else { + // Find the current mode in the array. + int currentMode; + for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) { + if (ROTATION_STYLE[currentMode] == initialMode) { + break; + } + } + mRotationStyleCurrentIndex = currentMode; + mSkipOriginalMixedCaseMode = true; + } + mIsActive = true; + } + + public void deactivate() { + mIsActive = false; + } + + public boolean isActive() { + return mIsActive; + } + + public boolean isSetAt(final int cursorStart, final int cursorEnd) { + return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter; + } + + /** + * Rotate through the different possible capitalization modes. + */ + public void rotate() { + final String oldResult = mStringAfter; + int count = 0; // Protection against infinite loop. + do { + mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length; + if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex] + && mSkipOriginalMixedCaseMode) { + mRotationStyleCurrentIndex = + (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length; + } + ++count; + switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) { + case CAPS_MODE_ORIGINAL_MIXED_CASE: + mStringAfter = mStringBefore; + break; + case CAPS_MODE_ALL_LOWER: + mStringAfter = mStringBefore.toLowerCase(mLocale); + break; + case CAPS_MODE_FIRST_WORD_UPPER: + mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators, + mLocale); + break; + case CAPS_MODE_ALL_UPPER: + mStringAfter = mStringBefore.toUpperCase(mLocale); + break; + default: + mStringAfter = mStringBefore; + } + } while (mStringAfter.equals(oldResult) && count < ROTATION_STYLE.length + 1); + mCursorEndAfter = mCursorStartAfter + mStringAfter.length(); + } + + /** + * Remove leading/trailing whitespace from the considered string. + */ + public void trim() { + final int len = mStringBefore.length(); + int nonWhitespaceStart = 0; + for (; nonWhitespaceStart < len; + nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) { + final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart); + if (!Character.isWhitespace(codePoint)) break; + } + int nonWhitespaceEnd = len; + for (; nonWhitespaceEnd > 0; + nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) { + final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd); + if (!Character.isWhitespace(codePoint)) break; + } + if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) { + mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd; + mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart; + mStringAfter = mStringBefore = + mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd); + } + } + + public String getRecapitalizedString() { + return mStringAfter; + } + + public int getNewCursorStart() { + return mCursorStartAfter; + } + + public int getNewCursorEnd() { + return mCursorEndAfter; + } + + public int getCurrentMode() { + return ROTATION_STYLE[mRotationStyleCurrentIndex]; + } +} diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 16744d1f0..8ed7ab264 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import android.inputmethodservice.InputMethodService; +import android.text.SpannableString; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; @@ -181,6 +182,11 @@ public final class RichInputConnection { } } + public CharSequence getSelectedText(final int flags) { + if (null == mIC) return null; + return mIC.getSelectedText(flags); + } + /** * Gets the caps modes we should be in after this specific string. * @@ -392,7 +398,9 @@ public final class RichInputConnection { 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 +450,9 @@ public final class RichInputConnection { 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 +506,7 @@ public final class RichInputConnection { * 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 +525,10 @@ public final class RichInputConnection { 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 +570,9 @@ public final class RichInputConnection { } } - 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); } @@ -709,4 +720,15 @@ public final class RichInputConnection { // position and the expected position, then it must be a belated update. return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; } + + /** + * Looks at the text just before the cursor to find out if it looks like a URL. + * + * The weakest point here is, if we don't have enough text bufferized, we may fail to realize + * we are in URL situation, but other places in this class have the same limitation and it + * does not matter too much in the practice. + */ + public boolean textBeforeCursorLooksLikeURL() { + return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); + } } diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 8fbe843cf..72e08700a 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -134,6 +134,14 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } + public String getWordSeparators() { + return mSettingsValues.mWordSeparators; + } + + public Locale getCurrentLocale() { + return mCurrentLocale; + } + // 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/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java index 99b572e06..37ac2e35c 100644 --- a/java/src/com/android/inputmethod/latin/SettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java @@ -25,7 +25,10 @@ public final class SettingsActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent intent = super.getIntent(); - intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + if (fragment == null) { + intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + } intent.putExtra(EXTRA_NO_HEADERS, true); return intent; } diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java index 5405a5eb7..a96c997c8 100644 --- a/java/src/com/android/inputmethod/latin/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java @@ -16,7 +16,6 @@ package com.android.inputmethod.latin; -import android.app.Activity; import android.app.backup.BackupManager; import android.content.Context; import android.content.Intent; @@ -115,12 +114,8 @@ public final class SettingsFragment extends InputMethodSettingsFragment if (FeedbackUtils.isFeedbackFormSupported()) { feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override - public boolean onPreferenceClick(Preference arg0) { - final Activity activity = getActivity(); - FeedbackUtils.showFeedbackForm(activity); - if (!activity.isFinishing()) { - activity.finish(); - } + public boolean onPreferenceClick(final Preference pref) { + FeedbackUtils.showFeedbackForm(getActivity()); return true; } }); @@ -232,10 +227,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment } else if (key.equals(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY)) { setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, Settings.readShowsLanguageSwitchKey(prefs)); - } else if (key.equals(Settings.PREF_GESTURE_INPUT)) { - final boolean gestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); - setPreferenceEnabled(Settings.PREF_GESTURE_PREVIEW_TRAIL, gestureInputEnabled); - setPreferenceEnabled(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, gestureInputEnabled); } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 59ad28fc9..d5ee58a63 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -106,10 +106,19 @@ public final class StringUtils { } } - public static String toTitleCase(final String s, final Locale locale) { + public static String capitalizeFirstCodePoint(final String s, final Locale locale) { if (s.length() <= 1) { - // TODO: is this really correct? Shouldn't this be s.toUpperCase()? - return s; + return s.toUpperCase(locale); + } + // Please refer to the comment below in + // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings + final int cutoff = s.offsetByCodePoints(0, 1); + return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff); + } + + public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) { + if (s.length() <= 1) { + return s.toUpperCase(locale); } // TODO: fix the bugs below // - This does not work for Greek, because it returns upper case instead of title case. @@ -213,4 +222,129 @@ public final class StringUtils { if (1 == capsCount) return CAPITALIZE_FIRST; return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); } + + public static boolean isIdenticalAfterUpcase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterDowncase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterCapitalizeEachWord(final String text, + final String separators) { + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint)) { + if ((needCapsNext && !Character.isUpperCase(codePoint)) + || (!needCapsNext && !Character.isLowerCase(codePoint))) { + return false; + } + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(codePoint)); + } + return true; + } + + // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph + // which should be capitalized together in *some* cases. + public static String capitalizeEachWord(final String text, final String separators, + final Locale locale) { + final StringBuilder builder = new StringBuilder(); + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1)); + if (needCapsNext) { + builder.append(nextChar.toUpperCase(locale)); + } else { + builder.append(nextChar.toLowerCase(locale)); + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0))); + } + return builder.toString(); + } + + /** + * Approximates whether the text before the cursor looks like a URL. + * + * This is not foolproof, but it should work well in the practice. + * Essentially it walks backward from the cursor until it finds something that's not a letter, + * digit, or common URL symbol like underscore. If it hasn't found a period yet, then it + * does not look like a URL. + * If the text: + * - starts with www and contains a period + * - starts with a slash preceded by either a slash, whitespace, or start-of-string + * Then it looks like a URL and we return true. Otherwise, we return false. + * + * Note: this method is called quite often, and should be fast. + * + * TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the + * code complexity, but ideally it should not. It's acceptable for now. + */ + public static boolean lastPartLooksLikeURL(final CharSequence text) { + int i = text.length(); + if (0 == i) return false; + int wCount = 0; + int slashCount = 0; + boolean hasSlash = false; + boolean hasPeriod = false; + int codePoint = 0; + while (i > 0) { + codePoint = Character.codePointBefore(text, i); + if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') { + // Handwavy heuristic to see if that's a URL character. Anything between period + // and z. This includes all lower- and upper-case ascii letters, period, + // underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation + // marks, double quotes... + // Anything that's not a URL-like character causes us to break from here and + // evaluate normally. + break; + } + if (Constants.CODE_PERIOD == codePoint) { + hasPeriod = true; + } + if (Constants.CODE_SLASH == codePoint) { + hasSlash = true; + if (2 == ++slashCount) { + return true; + } + } else { + slashCount = 0; + } + if ('w' == codePoint) { + ++wCount; + } else { + wCount = 0; + } + i = Character.offsetByCodePoints(text, i, -1); + } + // End of the text run. + // If it starts with www and includes a period, then it looks like a URL. + if (wCount >= 3 && hasPeriod) return true; + // If it starts with a slash, and the code point before is whitespace, it looks like an URL. + if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) return true; + // If it has both a period and a slash, it looks like an URL. + if (hasPeriod && hasSlash) return true; + // Otherwise, it doesn't look like an URL. + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java index 5e28cc2d0..4d88ecc0c 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -183,7 +183,7 @@ public final class SubtypeLocale { final Locale locale = LocaleUtils.constructLocaleFromString(localeString); displayName = locale.getDisplayName(displayLocale); } - return StringUtils.toTitleCase(displayName, displayLocale); + return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale); } // InputMethodSubtype's display name in its locale. @@ -243,7 +243,7 @@ public final class SubtypeLocale { } } }; - return StringUtils.toTitleCase( + return StringUtils.capitalizeFirstCodePoint( getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 975664dca..59d0207f6 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -47,6 +47,9 @@ public final class Suggest { // TODO: rename this to CORRECTION_ON public static final int CORRECTION_FULL = 1; + // Close to -2**31 + private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000; + public interface SuggestInitializationListener { public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); } @@ -65,7 +68,7 @@ public final class Suggest { private float mAutoCorrectionThreshold; // Locale used for upper- and title-casing words - private final Locale mLocale; + public final Locale mLocale; public Suggest(final Context context, final Locale locale, final SuggestInitializationListener listener) { @@ -334,7 +337,21 @@ public final class Suggest { } } + if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord, + wordComposer.getRejectedBatchModeSuggestion())) { + final SuggestedWordInfo rejected = suggestionsContainer.remove(0); + suggestionsContainer.add(1, rejected); + } SuggestedWordInfo.removeDups(suggestionsContainer); + + // For some reason some suggestions with MIN_VALUE are making their way here. + // TODO: Find a more robust way to detect distractors. + for (int i = suggestionsContainer.size() - 1; i >= 0; --i) { + if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) { + suggestionsContainer.remove(i); + } + } + // In the batch input mode, the most relevant suggested word should act as a "typed word" // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). return new SuggestedWords(suggestionsContainer, @@ -394,7 +411,7 @@ public final class Suggest { if (isAllUpperCase) { sb.append(wordInfo.mWord.toUpperCase(locale)); } else if (isFirstCharCapitalized) { - sb.append(StringUtils.toTitleCase(wordInfo.mWord, locale)); + sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale)); } else { sb.append(wordInfo.mWord); } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 3d6fe2d22..616e1911b 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -131,6 +131,7 @@ public final class SuggestedWords { 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 @@ -194,4 +195,21 @@ public final class SuggestedWords { } } } + + // SuggestedWords is an immutable object, as much as possible. We must not just remove + // words from the member ArrayList as some other parties may expect the object to never change. + public SuggestedWords getSuggestedWordsExcludingTypedWord() { + final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { + final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); + if (SuggestedWordInfo.KIND_TYPED != info.mKind) { + newSuggestions.add(info); + } + } + // We should never autocorrect, so we say the typed word is valid. Also, in this case, + // no auto-correction should take place hence willAutoCorrect = false. + return new SuggestedWords(newSuggestions, true /* typedWordValid */, + false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions, + mIsPrediction); + } } diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index 0d5bde623..90f92972a 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -20,7 +20,6 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; -import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; @@ -28,7 +27,10 @@ import android.os.Build; import android.provider.UserDictionary.Words; import android.text.TextUtils; +import com.android.inputmethod.compat.UserDictionaryCompatUtils; + import java.util.Arrays; +import java.util.Locale; /** * An expandable dictionary that stores the words in the user dictionary provider into a binary @@ -61,10 +63,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { private static final String NAME = "userunigram"; - // This is not exported by the framework so we pretty much have to write it here verbatim - private static final String ACTION_USER_DICTIONARY_INSERT = - "com.android.settings.USER_DICTIONARY_INSERT"; - private ContentObserver mObserver; final private String mLocale; final private boolean mAlsoUseMoreRestrictiveLocales; @@ -211,23 +209,19 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { /** * Adds a word to the user dictionary and makes it persistent. * - * This will call upon the system interface to do the actual work through the intent readied by - * the system to this effect. - * * @param word the word to add. If the word is capitalized, then the dictionary will * recognize it as a capitalized word when searched. */ public synchronized void addWordToUserDictionary(final String word) { - // TODO: do something for the UI. With the following, any sufficiently long word will - // look like it will go to the user dictionary but it won't. - // Safeguard against adding long words. Can cause stack overflow. - if (word.length() >= MAX_WORD_LENGTH) return; - - Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT); - intent.putExtra(Words.WORD, word); - intent.putExtra(Words.LOCALE, mLocale); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); + // Update the user dictionary provider + final Locale locale; + if (USER_DICTIONARY_ALL_LANGUAGES == mLocale) { + locale = null; + } else { + locale = LocaleUtils.constructLocaleFromString(mLocale); + } + UserDictionaryCompatUtils.addWord(mContext, word, + HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale); } private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) { @@ -258,10 +252,10 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { 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 f7cb4346a..51bd901fb 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -27,6 +27,7 @@ import java.util.Arrays; */ public final class WordComposer { private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; + private static final boolean DBG = LatinImeLogger.sDBG; public static final int CAPS_MODE_OFF = 0; // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits @@ -42,6 +43,13 @@ public final class WordComposer { private String mAutoCorrection; private boolean mIsResumed; private boolean mIsBatchMode; + // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user + // gestures a word, is displeased with the results and hits backspace, then gestures again. + // At the very least we should avoid re-suggesting the same thing, and to do that we memorize + // the rejected suggestion in this variable. + // TODO: this should be done in a comprehensive way by the User History feature instead of + // as an ad-hockery here. + private String mRejectedBatchModeSuggestion; // Cache these values for performance private int mCapsCount; @@ -49,6 +57,7 @@ public final class WordComposer { 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 +71,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; + mCursorPositionWithinWord = 0; + mRejectedBatchModeSuggestion = null; refreshSize(); } @@ -76,6 +87,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; mIsResumed = source.mIsResumed; mIsBatchMode = source.mIsBatchMode; + mCursorPositionWithinWord = source.mCursorPositionWithinWord; + mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion; refreshSize(); } @@ -91,6 +104,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; + mCursorPositionWithinWord = 0; + mRejectedBatchModeSuggestion = null; refreshSize(); } @@ -118,6 +133,13 @@ public final class WordComposer { return mPrimaryKeyCodes[index]; } + public int getCodeBeforeCursor() { + if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) { + return Constants.NOT_A_CODE; + } + return mPrimaryKeyCodes[mCursorPositionWithinWord - 1]; + } + public InputPointers getInputPointers() { return mInputPointers; } @@ -135,6 +157,7 @@ public final class WordComposer { 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 +181,18 @@ public final class WordComposer { mAutoCorrection = null; } + public void setCursorPositionWithinWord(final int posWithinWord) { + mCursorPositionWithinWord = posWithinWord; + } + + public boolean isCursorFrontOrMiddleOfComposingWord() { + if (DBG && mCursorPositionWithinWord > mCodePointSize) { + throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord + + "in a word of size " + mCodePointSize); + } + return mCursorPositionWithinWord != mCodePointSize; + } + public void setBatchInputPointers(final InputPointers batchPointers) { mInputPointers.set(batchPointers); mIsBatchMode = true; @@ -242,6 +277,7 @@ public final class WordComposer { ++mTrailingSingleQuotesCount; } } + mCursorPositionWithinWord = mCodePointSize; mAutoCorrection = null; } @@ -368,7 +404,9 @@ public final class WordComposer { mCapitalizedMode = CAPS_MODE_OFF; refreshSize(); mAutoCorrection = null; + mCursorPositionWithinWord = 0; mIsResumed = false; + mRejectedBatchModeSuggestion = null; return lastComposedWord; } @@ -380,10 +418,20 @@ public final class WordComposer { refreshSize(); mCapitalizedMode = lastComposedWord.mCapitalizedMode; mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. + mCursorPositionWithinWord = mCodePointSize; + mRejectedBatchModeSuggestion = null; mIsResumed = true; } public boolean isBatchMode() { return mIsBatchMode; } + + public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) { + mRejectedBatchModeSuggestion = rejectedSuggestion; + } + + public String getRejectedBatchModeSuggestion() { + return mRejectedBatchModeSuggestion; + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java index 58ec1e83f..467f6a053 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java @@ -1467,8 +1467,8 @@ public final class BinaryDictInputOutput { if (null == last) continue; builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); - groupOffset = last.mChildrenAddress + 1; - i = buffer.readUnsignedByte(); + i = readCharGroupCount(buffer); + groupOffset = last.mChildrenAddress + getGroupCountSize(i); last = null; continue; } @@ -1477,8 +1477,8 @@ public final class BinaryDictInputOutput { if (0 == i && hasChildrenAddress(last.mChildrenAddress)) { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); - groupOffset = last.mChildrenAddress + 1; - i = buffer.readUnsignedByte(); + i = readCharGroupCount(buffer); + groupOffset = last.mChildrenAddress + getGroupCountSize(i); last = null; continue; } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index e7c7e2b8a..17d281518 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -647,7 +647,7 @@ public final class FusionDictionary implements Iterable<Word> { if (index < codePoints.length) return null; if (!currentGroup.isTerminal()) return null; - if (DBG && !codePoints.equals(checker.toString())) return null; + if (DBG && !string.equals(checker.toString())) return null; return currentGroup; } @@ -853,16 +853,19 @@ public final class FusionDictionary implements Iterable<Word> { if (currentPos.pos.hasNext()) { final CharGroup currentGroup = currentPos.pos.next(); currentPos.length = mCurrentString.length(); - for (int i : currentGroup.mChars) + for (int i : currentGroup.mChars) { mCurrentString.append(Character.toChars(i)); + } if (null != currentGroup.mChildren) { currentPos = new Position(currentGroup.mChildren.mData); + currentPos.length = mCurrentString.length(); mPositions.addLast(currentPos); } - if (currentGroup.mFrequency >= 0) + if (currentGroup.mFrequency >= 0) { return new Word(mCurrentString.toString(), currentGroup.mFrequency, currentGroup.mShortcutTargets, currentGroup.mBigrams, currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry); + } } else { mPositions.removeLast(); currentPos = mPositions.getLast(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 8d3b062ff..2d0a89bb3 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -330,7 +330,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { for (int i = 0; i < mSuggestions.size(); ++i) { // Likewise - mSuggestions.set(i, StringUtils.toTitleCase( + mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint( mSuggestions.get(i).toString(), locale)); } } @@ -403,11 +403,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public DictAndProximity createDictAndProximity(final Locale locale) { final int script = getScriptFromLocale(locale); - final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo( - SpellCheckerProximityInfo.getProximityForScript(script), - SpellCheckerProximityInfo.ROW_SIZE, - SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH, - SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT); + final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script); final DictionaryCollection dictionaryCollection = DictionaryFactory.createMainDictionaryFromManager(this, locale, true /* useFullEditDistance */); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index b15063235..da8657201 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -189,10 +189,12 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { int letterCount = 0; for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { final int codePoint = text.codePointAt(i); - // Any word containing a '@' is probably an e-mail address - // Any word containing a '/' is probably either an ad-hoc combination of two + // Any word containing a COMMERCIAL_AT is probably an e-mail address + // Any word containing a SLASH is probably either an ad-hoc combination of two // words or a URI - in either case we don't want to spell check that - if ('@' == codePoint || '/' == codePoint) return true; + if (Constants.CODE_COMMERCIAL_AT == codePoint || Constants.CODE_SLASH == codePoint) { + return true; + } if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; } // Guestimate heuristic: perform spell checking if at least 3/4 of the characters @@ -226,7 +228,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // If the lower case version is not in the dictionary, it's still possible // that we have an all-caps version of a word that needs to be capitalized // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans". - return dict.isValidWord(StringUtils.toTitleCase(lowerCaseText, mLocale)); + return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); } // Note : this must be reentrant diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index 49dca21e6..0c480eaba 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -16,53 +16,40 @@ package com.android.inputmethod.latin.spellcheck; -import com.android.inputmethod.annotations.UsedForTesting; +import android.util.SparseIntArray; + import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.Constants; -import java.util.TreeMap; +public final class SpellCheckerProximityInfo extends ProximityInfo { + public SpellCheckerProximityInfo(final int script) { + super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT); + } -public final class SpellCheckerProximityInfo { - @UsedForTesting - final public static int NUL = Constants.NOT_A_CODE; + private static final int NUL = Constants.NOT_A_CODE; // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside // native code - this value is passed at creation of the binary object and reused // as the size of the passed array afterwards so they can't be different. - final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; + private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; // The number of keys in a row of the grid used by the spell checker. - final public static int PROXIMITY_GRID_WIDTH = 11; + private static final int PROXIMITY_GRID_WIDTH = 11; // The number of rows in the grid used by the spell checker. - final public static int PROXIMITY_GRID_HEIGHT = 3; + private static final int PROXIMITY_GRID_HEIGHT = 3; - final private static int NOT_AN_INDEX = -1; - final public static int NOT_A_COORDINATE_PAIR = -1; + private static final int NOT_AN_INDEX = -1; + public static final int NOT_A_COORDINATE_PAIR = -1; // Helper methods - final protected static void buildProximityIndices(final int[] proximity, - final TreeMap<Integer, Integer> indices) { - for (int i = 0; i < proximity.length; i += ROW_SIZE) { - if (NUL != proximity[i]) indices.put(proximity[i], i / ROW_SIZE); + static void buildProximityIndices(final int[] proximity, final int rowSize, + final SparseIntArray indices) { + for (int i = 0; i < proximity.length; i += rowSize) { + if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize); } } - final protected static int computeIndex(final int characterCode, - final TreeMap<Integer, Integer> indices) { - final Integer result = indices.get(characterCode); - if (null == result) return NOT_AN_INDEX; - return result; - } private static final class Latin { - // This is a map from the code point to the index in the PROXIMITY array. - // At the time the native code to read the binary dictionary needs the proximity info be - // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input - // character. - // Since we need to build such an array, we want to be able to search in our big proximity - // data quickly by character, and a map is probably the best way to do this. - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); - // The proximity here is the union of // - the proximity for a QWERTY keyboard. // - the proximity for an AZERTY keyboard. @@ -79,7 +66,7 @@ public final class SpellCheckerProximityInfo { a s d f g h j k l z x c v b n m */ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -121,16 +108,21 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + // This is a mapping array from the code point to the index in the PROXIMITY array. + // When we check the spelling of a word, we need to pass (x,y) coordinates to the native + // code for each letter of the word. These are most easily computed from the index in the + // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code + // point needs to be as fast as possible, and a map is probably the best way to do this. + // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray. + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Cyrillic { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Russian // speakers on commonly misspelled words/letters. /* @@ -207,7 +199,7 @@ public final class SpellCheckerProximityInfo { private static final int CY_SOFT_SIGN = '\u044C'; // ь private static final int CY_BE = '\u0431'; // б private static final int CY_YU = '\u044E'; // ю - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -280,16 +272,15 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Greek { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Greek // speakers on commonly misspelled words/letters. /* @@ -354,7 +345,7 @@ public final class SpellCheckerProximityInfo { private static final int GR_BETA = '\u03B2'; // β private static final int GR_NU = '\u03BD'; // ν private static final int GR_MU = '\u03BC'; // μ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -419,37 +410,37 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } - public static int[] getProximityForScript(final int script) { + private static int[] getProximityForScript(final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.PROXIMITY; - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.PROXIMITY; + default: + throw new RuntimeException("Wrong script supplied: " + script); } } private static int getIndexOfCodeForScript(final int codePoint, final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.getIndexOf(codePoint); - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.INDICES.get(codePoint, NOT_AN_INDEX); + default: + throw new RuntimeException("Wrong script supplied: " + script); } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index ed408bb3c..3037669c0 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -16,12 +16,14 @@ package com.android.inputmethod.latin.suggestions; +import android.content.Context; import android.content.res.Resources; import android.graphics.Paint; import android.graphics.drawable.Drawable; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.TypefaceUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; @@ -50,16 +52,12 @@ public final class MoreSuggestions extends Keyboard { super(); } - // TODO: Remove {@link MoreSuggestionsView} argument. public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth, - final int minWidth, final int maxRow, final MoreSuggestionsView view) { + final int minWidth, final int maxRow, final Paint paint, final Resources res) { clearKeys(); - final Resources res = view.getResources(); mDivider = res.getDrawable(R.drawable.more_suggestions_divider); mDividerWidth = mDivider.getIntrinsicWidth(); - final int padding = (int) res.getDimension( - R.dimen.more_suggestions_key_horizontal_padding); - final Paint paint = view.newDefaultLabelPaint(); + final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding); int row = 0; int pos = fromPos, rowStartPos = fromPos; @@ -67,7 +65,7 @@ public final class MoreSuggestions extends Keyboard { while (pos < size) { final String word = suggestions.getWord(pos); // TODO: Should take care of text x-scaling. - mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding; + mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding); final int numColumn = pos - rowStartPos + 1; final int columnWidth = (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; @@ -169,8 +167,8 @@ public final class MoreSuggestions extends Keyboard { private int mFromPos; private int mToPos; - public Builder(final MoreSuggestionsView paneView) { - super(paneView.getContext(), new MoreSuggestionsParam()); + public Builder(final Context context, final MoreSuggestionsView paneView) { + super(context, new MoreSuggestionsParam()); mPaneView = paneView; } @@ -183,7 +181,7 @@ public final class MoreSuggestions extends Keyboard { mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow, - mPaneView); + mPaneView.newLabelPaint(null /* key */), mResources); mFromPos = fromPos; mToPos = fromPos + count; mSuggestions = suggestions; diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index 438820d17..6509f394b 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -43,7 +43,13 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { } public void updateKeyboardGeometry(final int keyHeight) { - mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + updateKeyDrawParams(keyHeight); + } + + public void adjustVerticalCorrectionForModalMode() { + // Set vertical correction to zero (Reset more keys keyboard sliding allowance + // {@link R#dimen.more_keys_keyboard_slide_allowance}). + mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); } @Override diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index eeaf828a7..2a21ec2f5 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -596,7 +596,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer .findViewById(R.id.more_suggestions_view); - mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView); + mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); final Resources res = context.getResources(); mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( @@ -755,8 +755,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public boolean dispatchTouchEvent(final MotionEvent me) { - if (!mMoreSuggestionsView.isShowingInParent() - || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) { + if (!mMoreSuggestionsView.isShowingInParent()) { mLastX = (int)me.getX(); mLastY = (int)me.getY(); if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) { @@ -784,6 +783,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { // Decided to be in the modal input mode mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; + mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); } return true; } |