diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
8 files changed, 303 insertions, 73 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 4b242c5f5..1fe0a4cce 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -29,6 +29,7 @@ import android.util.Log; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.dictionarypack.MD5Calculator; +import com.android.inputmethod.dictionarypack.UpdateHandler; import com.android.inputmethod.latin.common.FileUtils; import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; @@ -323,7 +324,10 @@ public final class BinaryDictionaryFileDumper { // move the output file to the final staging file. final File finalFile = new File(finalFileName); - FileUtils.renameTo(outputFile, finalFile); + if (!FileUtils.renameTo(outputFile, finalFile)) { + Log.e(TAG, String.format("Failed to rename from %s to %s.", + outputFile.getAbsoluteFile(), finalFile.getAbsoluteFile())); + } wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_SUCCESS); @@ -445,7 +449,6 @@ public final class BinaryDictionaryFileDumper { */ public static void downloadDictIfNeverRequested(final Locale locale, final Context context, final boolean hasDefaultWordList) { - Log.d("inamul_tag", "BinaryDictionaryFileDumper.downloadDictIfNeverRequested()"); getWordListWordListInfos(locale, context, hasDefaultWordList); } @@ -488,6 +491,8 @@ public final class BinaryDictionaryFileDumper { private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, final ContentProviderClient client, final String clientId) throws RemoteException { final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context); + Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : MetadataFileUri = " + + metadataFileUri); final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context); // Tell the content provider to reset all information about this client id final Uri metadataContentUri = getProviderUriBuilder(clientId) @@ -512,9 +517,34 @@ public final class BinaryDictionaryFileDumper { final int length = dictionaryList.size(); for (int i = 0; i < length; ++i) { final DictionaryInfo info = dictionaryList.get(i); + Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : Insert " + info); client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId), info.toContentValues()); } + + // Read from metadata file in resources to get the baseline dictionary info. + // This ensures we start with a sane list of available dictionaries. + final int metadataResourceId = context.getResources().getIdentifier("metadata", + "raw", DictionaryInfoUtils.RESOURCE_PACKAGE_NAME); + if (metadataResourceId == 0) { + Log.w(TAG, "Missing metadata.json resource"); + return; + } + InputStream inputStream = null; + try { + inputStream = context.getResources().openRawResource(metadataResourceId); + UpdateHandler.handleMetadata(context, inputStream, clientId); + } catch (Exception e) { + Log.w(TAG, "Failed to read metadata.json from resources", e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close metadata.json", e); + } + } + } } /** diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java index 63064ba3e..c7115c9d9 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -27,6 +27,7 @@ import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.common.ComposedData; import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.utils.ExecutorUtils; @@ -509,10 +510,27 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { } } - private void putWordIntoValidSpellingWordCache(final String caller, final String word) { - final String spellingWord = word.toLowerCase(getLocale()); - final boolean isValid = isValidSpellingWord(spellingWord); - mValidSpellingWordWriteCache.put(spellingWord, isValid); + private void putWordIntoValidSpellingWordCache( + @Nonnull final String caller, + @Nonnull final String originalWord) { + if (mValidSpellingWordWriteCache == null) { + return; + } + + final String lowerCaseWord = originalWord.toLowerCase(getLocale()); + final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord); + mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid); + + final String capitalWord = + StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale()); + final boolean capitalValid; + if (lowerCaseValid) { + // The lower case form of the word is valid, so the upper case must be valid. + capitalValid = true; + } else { + capitalValid = isValidSpellingWord(capitalWord); + } + mValidSpellingWordWriteCache.put(capitalWord, capitalValid); } private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, @@ -620,8 +638,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { public boolean isValidSpellingWord(final String word) { if (mValidSpellingWordReadCache != null) { - final String spellingWord = word.toLowerCase(getLocale()); - final Boolean cachedValue = mValidSpellingWordReadCache.get(spellingWord); + final Boolean cachedValue = mValidSpellingWordReadCache.get(word); if (cachedValue != null) { return cachedValue; } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index a123d282b..a10f2bdb0 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -16,11 +16,10 @@ package com.android.inputmethod.latin; -import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH; - import android.inputmethodservice.InputMethodService; import android.os.Build; import android.os.Bundle; +import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.CharacterStyle; @@ -37,7 +36,6 @@ import com.android.inputmethod.compat.InputConnectionCompatUtils; import com.android.inputmethod.latin.common.Constants; import com.android.inputmethod.latin.common.UnicodeSurrogate; import com.android.inputmethod.latin.common.StringUtils; -import com.android.inputmethod.latin.define.DecoderSpecificConstants; import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; @@ -45,8 +43,11 @@ import com.android.inputmethod.latin.utils.DebugLogUtils; import com.android.inputmethod.latin.utils.NgramContextUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SpannableStringUtils; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.TextRange; +import java.util.concurrent.TimeUnit; + import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -59,17 +60,42 @@ import javax.annotation.Nullable; * for example. */ public final class RichInputConnection implements PrivateCommandPerformer { - private static final String TAG = RichInputConnection.class.getSimpleName(); + private static final String TAG = "RichInputConnection"; private static final boolean DBG = false; private static final boolean DEBUG_PREVIOUS_TEXT = false; private static final boolean DEBUG_BATCH_NESTING = false; - // Provision for realistic N-grams like "Hello, how are you?" and "I'm running 5 late". - // Technically, this will not handle 5-grams composed of long words, but in practice, - // our language models don't include that much data. - private static final int LOOKBACK_CHARACTER_NUM = 80; + private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40; + private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40; private static final int INVALID_CURSOR_POSITION = -1; /** + * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter + * the {@link #hasSlowInputConnection} state. + */ + private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000; + /** + * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs + * to take for the keyboard to enter the {@link #hasSlowInputConnection} state. + */ + private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200; + + private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0; + private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1; + private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2; + private static final int OPERATION_RELOAD_TEXT_CACHE = 3; + private static final String[] OPERATION_NAMES = new String[] { + "GET_TEXT_BEFORE_CURSOR", + "GET_TEXT_AFTER_CURSOR", + "GET_WORD_RANGE_AT_CURSOR", + "RELOAD_TEXT_CACHE"}; + + /** + * The amount of time the keyboard will persist in the {@link #hasSlowInputConnection} state + * after observing a slow InputConnection event. + */ + private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10); + + /** * This variable contains an expected value for the selection start position. This is where the * cursor or selection start may end up after all the keyboard-triggered updates have passed. We * keep this to compare it to the actual selection start to guess whether the move was caused by @@ -85,7 +111,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points /** * This contains the committed text immediately preceding the cursor and the composing - * text if any. It is refreshed when the cursor moves by calling upon the TextView. + * text, if any. It is refreshed when the cursor moves by calling upon the TextView. */ private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); /** @@ -100,8 +126,13 @@ public final class RichInputConnection implements PrivateCommandPerformer { private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); private final InputMethodService mParent; - InputConnection mIC; - int mNestLevel; + private InputConnection mIC; + private int mNestLevel; + + /** + * The timestamp of the last slow InputConnection operation + */ + private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; public RichInputConnection(final InputMethodService parent) { mParent = parent; @@ -113,6 +144,19 @@ public final class RichInputConnection implements PrivateCommandPerformer { return mIC != null; } + /** + * Returns whether or not the underlying InputConnection is slow. When true, we want to avoid + * calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor). + */ + public boolean hasSlowInputConnection() { + return (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime) + <= SLOW_INPUTCONNECTION_PERSIST_MS; + } + + public void onStartInput() { + mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; + } + private void checkConsistencyForDebug() { final ExtractedTextRequest r = new ExtractedTextRequest(); r.hintMaxChars = 0; @@ -211,9 +255,11 @@ public final class RichInputConnection implements PrivateCommandPerformer { mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and // we want to refresh it. - final CharSequence textBeforeCursor = isConnected() - ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0) - : null; + final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_RELOAD_TEXT_CACHE, + SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS, + Constants.EDITOR_CONTENTS_CACHE_SIZE, + 0 /* flags */); if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. @@ -377,16 +423,54 @@ public final class RichInputConnection implements PrivateCommandPerformer { } return s; } + return getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_GET_TEXT_BEFORE_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + n, flags); + } + + private CharSequence getTextBeforeCursorAndDetectLaggyConnection( + final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null; + if (!isConnected()) { + return null; + } + final long startTime = SystemClock.uptimeMillis(); + final CharSequence result = mIC.getTextBeforeCursor(n, flags); + detectLaggyConnection(operation, timeout, startTime); + return result; } public CharSequence getTextAfterCursor(final int n, final int flags) { + return getTextAfterCursorAndDetectLaggyConnection( + OPERATION_GET_TEXT_AFTER_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + n, flags); + } + + private CharSequence getTextAfterCursorAndDetectLaggyConnection( + final int operation, final long timeout, final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - return isConnected() ? mIC.getTextAfterCursor(n, flags) : null; + if (!isConnected()) { + return null; + } + final long startTime = SystemClock.uptimeMillis(); + final CharSequence result = mIC.getTextAfterCursor(n, flags); + detectLaggyConnection(operation, timeout, startTime); + return result; + } + + private void detectLaggyConnection(final int operation, final long timeout, final long startTime) { + final long duration = SystemClock.uptimeMillis() - startTime; + if (duration >= timeout) { + final String operationName = OPERATION_NAMES[operation]; + Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); + StatsUtils.onInputConnectionLaggy(operation, duration); + mLastSlowInputConnectionTime = SystemClock.uptimeMillis(); + } } - public void deleteSurroundingText(final int beforeLength, final int afterLength) { + public void deleteTextBeforeCursor(final int beforeLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // TODO: the following is incorrect if the cursor is not immediately after the composition. // Right now we never come here in this case because we reset the composing state before we @@ -411,7 +495,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { mExpectedSelStart = 0; } if (isConnected()) { - mIC.deleteSurroundingText(beforeLength, afterLength); + mIC.deleteSurroundingText(beforeLength, 0); } if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } @@ -576,9 +660,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return NgramContext.EMPTY_PREV_WORDS_INFO; } - final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { - final int checkLength = LOOKBACK_CHARACTER_NUM - 1; + final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1; final String reference = prev.length() <= checkLength ? prev.toString() : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); // TODO: right now the following works because mComposingText holds the part of the @@ -621,9 +705,15 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return null; } - final CharSequence before = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, + final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection( + OPERATION_GET_WORD_RANGE_AT_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + NUM_CHARS_TO_GET_BEFORE_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); - final CharSequence after = mIC.getTextAfterCursor(LOOKBACK_CHARACTER_NUM, + final CharSequence after = getTextAfterCursorAndDetectLaggyConnection( + OPERATION_GET_WORD_RANGE_AT_CURSOR, + SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, + NUM_CHARS_TO_GET_AFTER_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); if (before == null || after == null) { return null; @@ -666,8 +756,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { hasUrlSpans); } - public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { - if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations, + boolean checkTextAfter) { + if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) { // If what's after the cursor is a word character, then we're touching a word. return true; } @@ -704,7 +795,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (DEBUG_BATCH_NESTING) checkBatchEdit(); final int codePointBeforeCursor = getCodePointBeforeCursor(); if (Constants.CODE_SPACE == codePointBeforeCursor) { - deleteSurroundingText(1, 0); + deleteTextBeforeCursor(1); } } @@ -730,7 +821,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { } // Double-space results in ". ". A backspace to cancel this should result in a single // space in the text field, so we replace ". " with a single space. - deleteSurroundingText(2, 0); + deleteTextBeforeCursor(2); final String singleSpace = " "; commitText(singleSpace, 1); return true; @@ -752,7 +843,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { + "find a space just before the cursor."); return false; } - deleteSurroundingText(2, 0); + deleteTextBeforeCursor(2); final String text = " " + textBeforeCursor.subSequence(0, 1); commitText(text, 1); return true; diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java index 26c5e0034..90221512f 100644 --- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import android.app.DownloadManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -23,6 +24,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.database.Cursor; import android.os.Process; import android.preference.PreferenceManager; import android.util.Log; @@ -30,13 +32,12 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; +import com.android.inputmethod.dictionarypack.DownloadManagerWrapper; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.setup.SetupActivity; import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; -import java.util.Locale; - /** * This class detects the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME * package has been replaced by a newer version of the same package. This class also detects @@ -76,23 +77,12 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes(); richImm.setAdditionalInputMethodSubtypes(additionalSubtypes); toggleAppIcon(context); - downloadLatestDictionaries(context); - // TODO: This is only for dogfood builds. Remove this from the final release. - DictionaryFacilitator keyboardFacilitator = - DictionaryFacilitatorProvider.getDictionaryFacilitator(false); - boolean historyCleared = keyboardFacilitator.clearUserHistoryDictionary(context); - Locale keyboardLocale = keyboardFacilitator.getLocale(); - if (historyCleared && keyboardLocale != null) { - keyboardFacilitator.resetDictionaries( - context, - keyboardLocale, - keyboardFacilitator.usesContacts(), - true /* history */, - true /* force */, - keyboardFacilitator.getAccount(), - "" /* prefix */, - null /* listener */); - } + + // Remove all the previously scheduled downloads. This will also makes sure + // that any erroneously stuck downloads will get cleared. (b/21797386) + removeOldDownloads(context); + // b/21797386 + // downloadLatestDictionaries(context); } else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { Log.i(TAG, "Boot has been completed"); toggleAppIcon(context); @@ -120,6 +110,32 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver { } } + private void removeOldDownloads(Context context) { + try { + Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version."); + final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper( + context); + final DownloadManager.Query q = new DownloadManager.Query(); + // Query all the download statuses except the succeeded ones. + q.setFilterByStatus(DownloadManager.STATUS_FAILED + | DownloadManager.STATUS_PAUSED + | DownloadManager.STATUS_PENDING + | DownloadManager.STATUS_RUNNING); + final Cursor c = downloadManagerWrapper.query(q); + if (c != null) { + for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) { + final long downloadId = c + .getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)); + downloadManagerWrapper.remove(downloadId); + Log.i(TAG, "Removed the download with Id: " + downloadId); + } + c.close(); + } + } catch (Exception e) { + Log.e(TAG, "Exception while removing old downloads."); + } + } + private void downloadLatestDictionaries(Context context) { final Intent updateIntent = new Intent( DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION); diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 975ed7c01..1dd5850f8 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -139,6 +139,7 @@ public final class InputLogic { public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordBeingCorrectedByCursor = null; + mConnection.onStartInput(); if (!mWordComposer.getTypedWord().isEmpty()) { // For messaging apps that offer send button, the IME does not get the opportunity // to capture the last word. This block should capture those uncommitted words. @@ -472,7 +473,7 @@ public final class InputLogic { } // Try to record the word being corrected when the user enters a word character or // the backspace key. - if (!mWordComposer.isComposingWord() + if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord() && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || processedEvent.mKeyCode == Constants.CODE_DELETE)) { mWordBeingCorrectedByCursor = getWordAtCursor( @@ -832,8 +833,14 @@ public final class InputLogic { && settingsValues.needsToLookupSuggestions() && // In languages with spaces, we only start composing a word when we are not already // touching a word. In languages without spaces, the above conditions are sufficient. - (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) - || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { + // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it + // can incur a very expensive getTextAfterCursor() lookup, potentially making the + // keyboard UI slow and non-responsive. + // TODO: Cache the text after the cursor so we don't need to go to the InputConnection + // each time. We are already doing this for getTextBeforeCursor(). + (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, + !mConnection.hasSlowInputConnection() /* checkTextAfter */))) { // Reset entirely the composing state anyway, then start composing a new word unless // the character is a word connector. The idea here is, word connectors are not // separators and they should be treated as normal characters, except in the first @@ -1053,7 +1060,7 @@ public final class InputLogic { // Cancel multi-character input: remove the text we just entered. // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. - mConnection.deleteSurroundingText(mEnteredText.length(), 0); + mConnection.deleteTextBeforeCursor(mEnteredText.length()); StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); mEnteredText = null; // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. @@ -1098,7 +1105,7 @@ public final class InputLogic { - mConnection.getExpectedSelectionStart(); mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); - mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.deleteTextBeforeCursor(numCharsDeleted); StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. @@ -1138,13 +1145,13 @@ public final class InputLogic { // broken apps expect something to happen in this case so that they can // catch it and have their broken interface react. If you need the keyboard // to do this, you're doing it wrong -- please fix your app. - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDelete, 0); + mConnection.deleteTextBeforeCursor(lengthToDelete); int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { // If this is an accelerated (i.e., double) deletion, then we need to @@ -1157,7 +1164,7 @@ public final class InputLogic { if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); totalDeletedLength += lengthToDeleteAgain; } } @@ -1169,7 +1176,9 @@ public final class InputLogic { unlearnWordBeingDeleted( inputTransaction.mSettingsValues, currentKeyboardScriptId); } - if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + if (mConnection.hasSlowInputConnection()) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( @@ -1196,6 +1205,13 @@ public final class InputLogic { boolean unlearnWordBeingDeleted( final SettingsValues settingsValues, final int currentKeyboardScriptId) { + if (mConnection.hasSlowInputConnection()) { + // TODO: Refactor unlearning so that it does not incur any extra calls + // to the InputConnection. That way it can still be performed on a slow + // InputConnection. + Log.w(TAG, "Skipping unlearning due to slow InputConnection."); + return false; + } // If we just started backspacing to delete a previous word (but have not // entered the composing state yet), unlearn the word. // TODO: Consider tracking whether or not this word was typed by the user. @@ -1241,7 +1257,7 @@ public final class InputLogic { if (Constants.CODE_SPACE != codePointBeforeCursor) { return false; } - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); final String text = event.getTextToCommit() + " "; mConnection.commitText(text, 1); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); @@ -1331,7 +1347,7 @@ public final class InputLogic { Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2); if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { cancelDoubleSpacePeriodCountdown(); - mConnection.deleteSurroundingText(1, 0); + mConnection.deleteTextBeforeCursor(1); final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations .mSentenceSeparatorAndSpace; mConnection.commitText(textToInsert, 1); @@ -1399,7 +1415,7 @@ public final class InputLogic { mConnection.finishComposingText(); mRecapitalizeStatus.rotate(); mConnection.setSelection(selectionEnd, selectionEnd); - mConnection.deleteSurroundingText(numCharsSelected, 0); + mConnection.deleteTextBeforeCursor(numCharsSelected); mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), mRecapitalizeStatus.getNewCursorEnd()); @@ -1411,6 +1427,12 @@ public final class InputLogic { // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return; + if (mConnection.hasSlowInputConnection()) { + // Since we don't unlearn when the user backspaces on a slow InputConnection, + // turn off learning to guard against adding typos that the user later deletes. + Log.w(TAG, "Skipping learning due to slow InputConnection."); + return; + } if (TextUtils.isEmpty(suggestion)) return; final boolean wasAutoCapitalized = @@ -1514,7 +1536,8 @@ public final class InputLogic { return; } final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); - if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, + true /* checkTextAfter */)) { // Show predictions. mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); @@ -1637,7 +1660,7 @@ public final class InputLogic { + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); } } - mConnection.deleteSurroundingText(deleteLength, 0); + mConnection.deleteTextBeforeCursor(deleteLength); if (!TextUtils.isEmpty(committedWord)) { unlearnWord(committedWordString, inputTransaction.mSettingsValues, Constants.EVENT_REVERT); @@ -2135,9 +2158,10 @@ public final class InputLogic { final SuggestedWords suggestedWords = mSuggestedWords; // TODO: Locale should be determined based on context and the text given. final Locale locale = getDictionaryFacilitatorLocale(); - final CharSequence chosenWordWithSuggestions = - SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, - suggestedWords, locale); + final CharSequence chosenWordWithSuggestions = chosenWord; + // b/21926256 + // SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, + // suggestedWords, locale); if (DebugFlags.DEBUG_ENABLED) { long runTimeMillis = System.currentTimeMillis() - startTimeMillis; Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " diff --git a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java index aa73a9a83..d28e703fe 100644 --- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -16,20 +16,27 @@ package com.android.inputmethod.latin.settings; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.os.Build; import android.os.Bundle; import android.preference.Preference; import com.android.inputmethod.dictionarypack.DictionarySettingsActivity; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.userdictionary.UserDictionaryList; +import com.android.inputmethod.latin.userdictionary.UserDictionarySettings; + +import java.util.TreeSet; /** * "Text correction" settings sub screen. * * This settings sub screen handles the following text correction preferences. + * - Personal dictionary * - Add-on dictionaries * - Block offensive words * - Auto-correction @@ -59,5 +66,39 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { if (0 >= number) { removePreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY); } + + final Preference editPersonalDictionary = + findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY); + final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent(); + final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null + : pm.resolveActivity( + editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (ri == null) { + overwriteUserDictionaryPreference(editPersonalDictionary); + } + } + + private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { + final Activity activity = getActivity(); + final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity); + if (null == localeList) { + // The locale list is null if and only if the user dictionary service is + // not present or disabled. In this case we need to remove the preference. + getPreferenceScreen().removePreference(userDictionaryPreference); + } else if (localeList.size() <= 1) { + userDictionaryPreference.setFragment(UserDictionarySettings.class.getName()); + // If the size of localeList is 0, we don't set the locale parameter in the + // extras. This will be interpreted by the UserDictionarySettings class as + // meaning "the current locale". + // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet() + // the locale list always has at least one element, since it always includes the current + // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet(). + if (localeList.size() == 1) { + final String locale = (String)localeList.toArray()[0]; + userDictionaryPreference.getExtras().putString("locale", locale); + } + } else { + userDictionaryPreference.setFragment(UserDictionaryList.class.getName()); + } } } diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 694f43d3f..940f1bdfc 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -56,6 +56,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead. public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode"; public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key"; + public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary"; public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key"; // PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead. public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE = diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 096a545e5..cea2e13b1 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -54,7 +54,7 @@ import javax.annotation.Nullable; */ public class DictionaryInfoUtils { private static final String TAG = DictionaryInfoUtils.class.getSimpleName(); - private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); + public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; private static final String MAIN_DICT_PREFIX = "main_"; private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX; @@ -103,6 +103,13 @@ public class DictionaryInfoUtils { values.put(VERSION_COLUMN, mVersion); return values; } + + @Override + public String toString() { + return "DictionaryInfo : Id = '" + mId + + "' : Locale=" + mLocale + + " : Version=" + mVersion; + } } private DictionaryInfoUtils() { @@ -307,7 +314,10 @@ public class DictionaryInfoUtils { final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId; final File cacheFile = new File(cacheFilename); // move the staging file to cache file. - FileUtils.renameTo(stagingFile, cacheFile); + if (!FileUtils.renameTo(stagingFile, cacheFile)) { + Log.e(TAG, String.format("Failed to rename from %s to %s.", + stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile())); + } } } } |