diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
17 files changed, 522 insertions, 234 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 9a888ade4..9764df072 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -28,7 +28,7 @@ import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.nio.ByteBuffer; +import java.nio.BufferUnderflowException; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.HashMap; @@ -357,14 +357,15 @@ class BinaryDictionaryGetter { try { // Read the version of the file inStream = new FileInputStream(f); - final ByteBuffer buffer = inStream.getChannel().map( - FileChannel.MapMode.READ_ONLY, 0, f.length()); - final int magic = buffer.getInt(); + final BinaryDictInputOutput.ByteBufferWrapper buffer = + new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map( + FileChannel.MapMode.READ_ONLY, 0, f.length())); + final int magic = buffer.readInt(); if (magic != FormatSpec.VERSION_2_MAGIC_NUMBER) { return false; } - final int formatVersion = buffer.getInt(); - final int headerSize = buffer.getInt(); + final int formatVersion = buffer.readInt(); + final int headerSize = buffer.readInt(); final HashMap<String, String> options = CollectionUtils.newHashMap(); BinaryDictInputOutput.populateOptions(buffer, headerSize, options); @@ -382,6 +383,8 @@ class BinaryDictionaryGetter { return false; } catch (NumberFormatException e) { return false; + } catch (BufferUnderflowException e) { + return false; } finally { if (inStream != null) { try { diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index db8f269eb..9252b0980 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -72,6 +72,7 @@ import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.LocaleUtils.RunInLocale; +import com.android.inputmethod.latin.Utils.Stats; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.research.ResearchLogger; @@ -148,7 +149,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mIsUserDictionaryAvailable; private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private WordComposer mWordComposer = new WordComposer(); + private final WordComposer mWordComposer = new WordComposer(); private RichInputConnection mConnection = new RichInputConnection(this); // Keep track of the last selection range to decide if we need to show word alternatives @@ -741,6 +742,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the // field gets emptied and we need to re-evaluate the shift state, but not the whole layout // which would be disruptive. + // Space state must be updated before calling updateShiftState mKeyboardSwitcher.updateShiftState(); mHandler.cancelUpdateSuggestionStrip(); @@ -1100,25 +1102,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final EditorInfo ei = getCurrentInputEditorInfo(); if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; - final int inputType = ei.inputType; - if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) { - return TextUtils.CAP_MODE_CHARACTERS; - } - - final boolean noNeedToCheckCapsMode = (inputType & (InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - | InputType.TYPE_TEXT_FLAG_CAP_WORDS)) == 0; - if (noNeedToCheckCapsMode) return Constants.TextUtils.CAP_MODE_OFF; - - // Avoid making heavy round-trip IPC calls of {@link InputConnection#getCursorCapsMode} - // unless needed. - if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF; - - // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls. - // Note: getCursorCapsMode() returns the current capitalization mode that is any - // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none - // of them. - return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale()); + // Warning: this depends on mSpaceState, which may not be the most current value. If + // mSpaceState gets updated later, whoever called this may need to be told about it. + return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(), + SPACE_STATE_PHANTOM == mSpaceState); } // Factor in auto-caps and manual caps and compute the current caps mode. @@ -1344,6 +1332,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); } else { if (SPACE_STATE_PHANTOM == spaceState) { + if (ProductionFlag.IS_INTERNAL) { + if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { + Stats.onAutoCorrection( + "", mWordComposer.getTypedWord(), " ", mWordComposer); + } + } commitTyped(LastComposedWord.NOT_A_SEPARATOR); } final int keyX, keyY; @@ -1391,9 +1385,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mConnection.commitText(text, 1); mConnection.endBatchEdit(); + // Space state must be updated before calling updateShiftState + mSpaceState = SPACE_STATE_NONE; mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT); - mSpaceState = SPACE_STATE_NONE; mEnteredText = text; } @@ -1401,13 +1396,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onStartBatchInput() { mConnection.beginBatchEdit(); if (mWordComposer.isComposingWord()) { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + if (ProductionFlag.IS_INTERNAL) { + if (mWordComposer.isBatchMode()) { + Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer); + } + } + if (mWordComposer.size() <= 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 + // should become "I"). However for any longer word, we assume that the reason for + // tapping probably is that the word you intend to type is not in the dictionary, + // so we do not attempt to correct, on the assumption that if that was a dictionary + // word, the user would probably have gestured instead. + commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); + } else { + commitTyped(LastComposedWord.NOT_A_SEPARATOR); + } mExpectingUpdateSelection = true; - // TODO: Can we remove this? + // The following is necessary for the case where the user typed something but didn't + // manual pick it and didn't input any separator. mSpaceState = SPACE_STATE_PHANTOM; } mConnection.endBatchEdit(); - // TODO: Should handle TextUtils.CAP_MODE_CHARACTER. mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } @@ -1509,8 +1520,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mConnection.setComposingText(batchInputText, 1); mExpectingUpdateSelection = true; mConnection.endBatchEdit(); - mKeyboardSwitcher.updateShiftState(); + // Space state must be updated before calling updateShiftState mSpaceState = SPACE_STATE_PHANTOM; + mKeyboardSwitcher.updateShiftState(); } private CharSequence specificTldProcessingOnTextInput(final CharSequence text) { @@ -1558,7 +1570,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } else { if (mLastComposedWord.canRevertCommit()) { - Utils.Stats.onAutoCorrectionCancellation(); + if (ProductionFlag.IS_INTERNAL) { + Stats.onAutoCorrectionCancellation(); + } revertCommit(); return; } @@ -1707,7 +1721,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); } mHandler.postUpdateSuggestionStrip(); - Utils.Stats.onNonSeparator((char)primaryCode, x, y); + if (ProductionFlag.IS_INTERNAL) { + Utils.Stats.onNonSeparator((char)primaryCode, x, y); + } } // Returns true if we did an autocorrection, false otherwise. @@ -1771,8 +1787,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // already displayed or not, so it's okay. setPunctuationSuggestions(); } - - Utils.Stats.onSeparator((char)primaryCode, x, y); + if (ProductionFlag.IS_INTERNAL) { + Utils.Stats.onSeparator((char)primaryCode, x, y); + } mHandler.postUpdateShiftState(); return didAutoCorrect; @@ -1941,7 +1958,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } - Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorString); + if (ProductionFlag.IS_INTERNAL) { + Stats.onAutoCorrection( + typedWord, autoCorrection.toString(), separatorString, mWordComposer); + } mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separatorString); @@ -2019,8 +2039,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mConnection.endBatchEdit(); // Don't allow cancellation of manual pick mLastComposedWord.deactivate(); + // Space state must be updated before calling updateShiftState mSpaceState = SPACE_STATE_PHANTOM; - // TODO: is this necessary? mKeyboardSwitcher.updateShiftState(); // We should show the "Touch again to save" hint if the user pressed the first entry @@ -2031,8 +2051,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // If the suggestion is not in the dictionary, the hint should be shown. && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true); - Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + if (ProductionFlag.IS_INTERNAL) { + Stats.onSeparator((char)Keyboard.CODE_SPACE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { mSuggestionStripView.showAddToDictionaryHint( suggestion, mCurrentSettings.mHintToSaveText); @@ -2149,8 +2171,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen previousWord.toString(), committedWord.toString()); } mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); - Utils.Stats.onSeparator(mLastComposedWord.mSeparatorString, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + if (ProductionFlag.IS_INTERNAL) { + Stats.onSeparator(mLastComposedWord.mSeparatorString, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.latinIME_revertCommit(originallyTypedWord); } diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index e843848bc..9eab19c49 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -44,7 +44,12 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang String before, String after, int position, SuggestedWords suggestedWords) { } - public static void logOnAutoCorrection(String before, String after, int separatorCode) { + public static void logOnAutoCorrectionForTyping( + String before, String after, int separatorCode) { + } + + public static void logOnAutoCorrectionForGeometric(String before, String after, + int separatorCode, InputPointers inputPointers) { } public static void logOnAutoCorrectionCancelled() { diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index b85f9dcd7..28c0c0f16 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -190,10 +190,35 @@ public class RichInputConnection { } } - public int getCursorCapsMode(final int inputType, final Locale locale) { + /** + * Gets the caps modes we should be in after this specific string. + * + * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. + * This method also supports faking an additional space after the string passed in argument, + * to support cases where a space will be added automatically, like in phantom space + * state for example. + * Note that for English, we are using American typography rules (which are not specific to + * American English, it's just the most common set of rules for English). + * + * @param inputType a mask of the caps modes to test for. + * @param locale what language should be considered. + * @param hasSpaceBefore if we should consider there should be a space after the string. + * @return the caps modes that should be on as a set of bits + */ + public int getCursorCapsMode(final int inputType, final Locale locale, + final boolean hasSpaceBefore) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; - if (!TextUtils.isEmpty(mComposingText)) return Constants.TextUtils.CAP_MODE_OFF; + if (!TextUtils.isEmpty(mComposingText)) { + if (hasSpaceBefore) { + // If we have some composing text and a space before, then we should have + // MODE_CHARACTERS and MODE_WORDS on. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType; + } else { + // We have some composing text - we should be in MODE_CHARACTERS only. + return TextUtils.CAP_MODE_CHARACTERS & inputType; + } + } // TODO: this will generally work, but there may be cases where the buffer contains SOME // information but not enough to determine the caps mode accurately. This may happen after // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. @@ -205,7 +230,8 @@ public class RichInputConnection { } // This never calls InputConnection#getCapsMode - in fact, it's a static method that // never blocks or initiates IPC. - return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale); + return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale, + hasSpaceBefore); } public CharSequence getTextBeforeCursor(final int i, final int j) { diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 6251c9acd..9479a88a7 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -42,9 +42,9 @@ import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.ResearchLogger; import com.android.inputmethodcommon.InputMethodSettingsFragment; -public class Settings extends InputMethodSettingsFragment +public final class Settings extends InputMethodSettingsFragment implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final boolean ENABLE_EXPERIMENTAL_SETTINGS = false; + public static final boolean ENABLE_INTERNAL_SETTINGS = ProductionFlag.IS_INTERNAL; // In the same order as xml/prefs.xml public static final String PREF_GENERAL_SETTINGS = "general_settings"; @@ -63,8 +63,8 @@ public class Settings extends InputMethodSettingsFragment "last_user_dictionary_write_time"; public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings"; public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict"; - public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = - "pref_suppress_language_switch_key"; + public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY = + "pref_show_language_switch_key"; public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST = "pref_include_other_imes_in_language_switch_list"; public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; @@ -77,8 +77,8 @@ public class Settings extends InputMethodSettingsFragment public static final String PREF_KEYPRESS_SOUND_VOLUME = "pref_keypress_sound_volume"; public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail"; - public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = - "pref_gesture_floating_preview_text"; + public static final String PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = + "pref_show_gesture_floating_preview_text"; public static final String PREF_INPUT_LANGUAGE = "input_language"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; @@ -97,7 +97,7 @@ public class Settings extends InputMethodSettingsFragment private TextView mKeypressVibrationDurationSettingsTextView; private TextView mKeypressSoundVolumeSettingsTextView; - private static void setPreferenceEnabled(Preference preference, boolean enabled) { + private static void setPreferenceEnabled(final Preference preference, final boolean enabled) { if (preference != null) { preference.setEnabled(enabled); } @@ -111,7 +111,7 @@ public class Settings extends InputMethodSettingsFragment } @Override - public void onCreate(Bundle icicle) { + public void onCreate(final Bundle icicle) { super.onCreate(icicle); setInputMethodSettingsCategoryTitle(R.string.language_selection_title); setSubtypeEnablerTitle(R.string.select_language); @@ -192,7 +192,7 @@ public class Settings extends InputMethodSettingsFragment } setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST), - !SettingsValues.isLanguageSwitchKeySupressed(prefs)); + SettingsValues.showsLanguageSwitchKey(prefs)); final PreferenceScreen dictionaryLink = (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY); @@ -207,7 +207,7 @@ public class Settings extends InputMethodSettingsFragment R.bool.config_gesture_input_enabled_by_build_config); final Preference gesturePreviewTrail = findPreference(PREF_GESTURE_PREVIEW_TRAIL); final Preference gestureFloatingPreviewText = findPreference( - PREF_GESTURE_FLOATING_PREVIEW_TEXT); + PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT); if (!gestureInputEnabledByBuildConfig) { miscSettings.removePreference(findPreference(PREF_GESTURE_INPUT)); miscSettings.removePreference(gesturePreviewTrail); @@ -220,7 +220,7 @@ public class Settings extends InputMethodSettingsFragment final boolean showUsabilityStudyModeOption = res.getBoolean(R.bool.config_enable_usability_study_mode_option) - || ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS; + || ProductionFlag.IS_EXPERIMENTAL || ENABLE_INTERNAL_SETTINGS; final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE); if (!showUsabilityStudyModeOption) { if (usabilityStudyPref != null) { @@ -288,14 +288,14 @@ public class Settings extends InputMethodSettingsFragment } @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { (new BackupManager(getActivity())).dataChanged(); if (key.equals(PREF_POPUP_ON)) { setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY), prefs.getBoolean(PREF_POPUP_ON, true)); - } else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) { + } else if (key.equals(PREF_SHOW_LANGUAGE_SWITCH_KEY)) { setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST), - !SettingsValues.isLanguageSwitchKeySupressed(prefs)); + SettingsValues.showsLanguageSwitchKey(prefs)); } else if (key.equals(PREF_GESTURE_INPUT)) { final boolean gestureInputEnabledByConfig = getResources().getBoolean( R.bool.config_gesture_input_enabled_by_build_config); @@ -304,7 +304,7 @@ public class Settings extends InputMethodSettingsFragment PREF_GESTURE_INPUT, true); setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL), gestureInputEnabledByUser); - setPreferenceEnabled(findPreference(PREF_GESTURE_FLOATING_PREVIEW_TEXT), + setPreferenceEnabled(findPreference(PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT), gestureInputEnabledByUser); } } @@ -352,7 +352,7 @@ public class Settings extends InputMethodSettingsFragment } private void refreshEnablingsOfKeypressSoundAndVibrationSettings( - SharedPreferences sp, Resources res) { + final SharedPreferences sp, final Resources res) { if (mKeypressVibrationDurationSettingsPref != null) { final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity()) .hasVibrator(); @@ -370,7 +370,7 @@ public class Settings extends InputMethodSettingsFragment } private void updateKeypressVibrationDurationSettingsSummary( - SharedPreferences sp, Resources res) { + final SharedPreferences sp, final Resources res) { if (mKeypressVibrationDurationSettingsPref != null) { mKeypressVibrationDurationSettingsPref.setSummary( SettingsValues.getCurrentVibrationDuration(sp, res) @@ -428,7 +428,7 @@ public class Settings extends InputMethodSettingsFragment builder.create().show(); } - private void updateKeypressSoundVolumeSummary(SharedPreferences sp, Resources res) { + private void updateKeypressSoundVolumeSummary(final SharedPreferences sp, final Resources res) { if (mKeypressSoundVolumeSettingsPref != null) { mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf( (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100))); diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 5e9c870d4..9d8379a7a 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -72,7 +72,7 @@ public final class SettingsValues { @SuppressWarnings("unused") // TODO: Use this private final boolean mUsabilityStudyMode; public final boolean mIncludesOtherImesInLanguageSwitchList; - public final boolean mIsLanguageSwitchKeySuppressed; + public final boolean mShowsLanguageSwitchKey; @SuppressWarnings("unused") // TODO: Use this private final String mKeyPreviewPopupDismissDelayRawValue; public final boolean mUseContactsDict; @@ -151,7 +151,7 @@ public final class SettingsValues { mUsabilityStudyMode = getUsabilityStudyMode(prefs); mIncludesOtherImesInLanguageSwitchList = prefs.getBoolean( Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false); - mIsLanguageSwitchKeySuppressed = isLanguageSwitchKeySupressed(prefs); + mShowsLanguageSwitchKey = showsLanguageSwitchKey(prefs); mKeyPreviewPopupDismissDelayRawValue = prefs.getString( Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout))); @@ -178,7 +178,7 @@ public final class SettingsValues { && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true); mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); mGestureFloatingPreviewTextEnabled = prefs.getBoolean( - Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); + Settings.PREF_SHOW_GESTURE_FLOATING_PREVIEW_TEXT, false); mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionVisibility = createSuggestionVisibility(res); } @@ -336,12 +336,25 @@ public final class SettingsValues { return mVoiceKeyOnMain; } - public static boolean isLanguageSwitchKeySupressed(final SharedPreferences prefs) { - return prefs.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false); + // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead. + // This is being used only for the backward compatibility. + private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = + "pref_suppress_language_switch_key"; + + public static boolean showsLanguageSwitchKey(final SharedPreferences prefs) { + if (prefs.contains(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) { + final boolean suppressLanguageSwitchKey = prefs.getBoolean( + PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false); + final SharedPreferences.Editor editor = prefs.edit(); + editor.remove(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY); + editor.putBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, !suppressLanguageSwitchKey); + editor.apply(); + } + return prefs.getBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, true); } public boolean isLanguageSwitchKeyEnabled(final Context context) { - if (mIsLanguageSwitchKeySuppressed) { + if (!mShowsLanguageSwitchKey) { return false; } if (mIncludesOtherImesInLanguageSwitchList) { @@ -353,7 +366,7 @@ public final class SettingsValues { } } - public boolean isFullscreenModeAllowed(final Resources res) { + public static boolean isFullscreenModeAllowed(final Resources res) { return res.getBoolean(R.bool.config_use_fullscreen_mode); } diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 6dc1ea807..7b65b7343 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -197,13 +197,15 @@ public final class StringUtils { * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and * {@link TextUtils#CAP_MODE_SENTENCES}. * @param locale The locale to consider for capitalization rules + * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs * * @return Returns the actual capitalization modes that can be in effect * at the current position, which is any combination of * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and * {@link TextUtils#CAP_MODE_SENTENCES}. */ - public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale) { + public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale, + final boolean hasSpaceBefore) { // Quick description of what we want to do: // CAP_MODE_CHARACTERS is always on. // CAP_MODE_WORDS is on if there is some whitespace before the cursor. @@ -230,11 +232,15 @@ public final class StringUtils { // single quote since they aren't start punctuation in the unicode sense, but should still // be skipped for English. TODO: does this depend on the language? int i; - for (i = cs.length(); i > 0; i--) { - final char c = cs.charAt(i - 1); - if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE - && Character.getType(c) != Character.START_PUNCTUATION) { - break; + if (hasSpaceBefore) { + i = cs.length() + 1; + } else { + for (i = cs.length(); i > 0; i--) { + final char c = cs.charAt(i - 1); + if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE + && Character.getType(c) != Character.START_PUNCTUATION) { + break; + } } } @@ -247,6 +253,7 @@ public final class StringUtils { // if the first char that's not a space or tab is a start of line (as in, either \n or // start of text). int j = i; + if (hasSpaceBefore) --j; while (j > 0 && Character.isWhitespace(cs.charAt(j - 1))) { j--; } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 0418d3166..278c4b9ce 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -306,6 +306,10 @@ public class Suggest { wordComposer, prevWordForBigram, proximityInfo, sessionId)); } + for (SuggestedWordInfo wordInfo : suggestionsSet) { + LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict); + } + final ArrayList<SuggestedWordInfo> suggestionsContainer = CollectionUtils.newArrayList(suggestionsSet); final int suggestionsCount = suggestionsContainer.size(); diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java index 550e4e58b..4a3d11aa1 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import android.util.Log; +import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; import com.android.inputmethod.latin.makedict.BinaryDictInputOutput; import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface; import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; @@ -96,6 +97,11 @@ public class UserHistoryDictIOUtils { public void put(final byte b) { mBuffer[mPosition++] = b; } + + @Override + public int limit() { + return mBuffer.length; + } } /** @@ -162,7 +168,7 @@ public class UserHistoryDictIOUtils { final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap(); try { - BinaryDictInputOutput.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies, + BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies, bigrams); addWordsFromWordMap(unigrams, frequencies, bigrams, dict); } catch (IOException e) { diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index 1c98b92cd..876bc8e79 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -412,14 +412,24 @@ public final class Utils { } public static void onAutoCorrection(final String typedWord, final String correctedWord, - final String separatorString) { - if (TextUtils.isEmpty(typedWord)) return; + final String separatorString, final WordComposer wordComposer) { + final boolean isBatchMode = wordComposer.isBatchMode(); + if (!isBatchMode && TextUtils.isEmpty(typedWord)) return; // TODO: this fails when the separator is more than 1 code point long, but // the backend can't handle it yet. The only case when this happens is with // smileys and other multi-character keys. final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE : separatorString.codePointAt(0); - LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, codePoint); + if (!isBatchMode) { + LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint); + } else { + if (!TextUtils.isEmpty(correctedWord)) { + // We must make sure that InputPointer contains only the relative timestamps, + // not actual timestamps. + LatinImeLogger.logOnAutoCorrectionForGeometric( + "", correctedWord, codePoint, wordComposer.getInputPointers()); + } + } } public static void onAutoCorrectionCancellation() { diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 4b7adf26b..275ebf305 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -265,9 +265,12 @@ public class WordComposer { * @return true if all user typed chars are upper case, false otherwise */ public boolean isAllUpperCase() { - return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED - || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED - || (mCapsCount > 0) && (mCapsCount == size()); + if (size() <= 1) { + return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED + || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED; + } else { + return mCapsCount == size(); + } } public boolean wasShiftedNoLock() { diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java index de2057669..52c066a44 100644 --- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java +++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java @@ -22,4 +22,5 @@ public final class ProductionFlag { } public static final boolean IS_EXPERIMENTAL = false; + public static final boolean IS_INTERNAL = false; } diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java new file mode 100644 index 000000000..7a1b9dcb7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2012 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.makedict; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface; +import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; +import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Stack; + +public class BinaryDictIOUtils { + private static final boolean DBG = false; + + private static class Position { + public static final int NOT_READ_GROUPCOUNT = -1; + + public int mAddress; + public int mNumOfCharGroup; + public int mPosition; + public int mLength; + + public Position(int address, int length) { + mAddress = address; + mLength = length; + mNumOfCharGroup = NOT_READ_GROUPCOUNT; + } + } + + /** + * Tours all node without recursive call. + */ + private static void readUnigramsAndBigramsBinaryInner( + final FusionDictionaryBufferInterface buffer, final int headerSize, + final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams, + final FormatOptions formatOptions) { + int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1]; + + Stack<Position> stack = new Stack<Position>(); + int index = 0; + + Position initPos = new Position(headerSize, 0); + stack.push(initPos); + + while (!stack.empty()) { + Position p = stack.peek(); + + if (DBG) { + MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" + + p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength); + } + + if (buffer.position() != p.mAddress) buffer.position(p.mAddress); + if (index != p.mLength) index = p.mLength; + + if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) { + p.mNumOfCharGroup = BinaryDictInputOutput.readCharGroupCount(buffer); + p.mAddress += BinaryDictInputOutput.getGroupCountSize(p.mNumOfCharGroup); + p.mPosition = 0; + } + + CharGroupInfo info = BinaryDictInputOutput.readCharGroup(buffer, + p.mAddress - headerSize, formatOptions); + for (int i = 0; i < info.mCharacters.length; ++i) { + pushedChars[index++] = info.mCharacters[i]; + } + p.mPosition++; + + if (info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) { // found word + words.put(info.mOriginalAddress, new String(pushedChars, 0, index)); + frequencies.put(info.mOriginalAddress, info.mFrequency); + if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams); + } + + if (p.mPosition == p.mNumOfCharGroup) { + stack.pop(); + } else { + // the node has more groups. + p.mAddress = buffer.position(); + } + + if (BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) { + Position childrenPos = new Position(info.mChildrenAddress + headerSize, index); + stack.push(childrenPos); + } + } + } + + /** + * Reads unigrams and bigrams from the binary file. + * Doesn't make the memory representation of the dictionary. + * + * @param buffer the buffer to read. + * @param words the map to store the address as a key and the word as a value. + * @param frequencies the map to store the address as a key and the frequency as a value. + * @param bigrams the map to store the address as a key and the list of address as a value. + * @throws IOException + * @throws UnsupportedFormatException + */ + public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer, + final Map<Integer, String> words, final Map<Integer, Integer> frequencies, + final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, + UnsupportedFormatException { + // Read header + final FileHeader header = BinaryDictInputOutput.readHeader(buffer); + readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams, + header.mFormatOptions); + } + + /** + * Gets the address of the last CharGroup of the exact matching word in the dictionary. + * If no match is found, returns NOT_VALID_WORD. + * + * @param buffer the buffer to read. + * @param word the word we search for. + * @return the address of the terminal node. + * @throws IOException + * @throws UnsupportedFormatException + */ + public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer, + final String word) throws IOException, UnsupportedFormatException { + if (word == null) return FormatSpec.NOT_VALID_WORD; + if (buffer.position() != 0) buffer.position(0); + + final FileHeader header = BinaryDictInputOutput.readHeader(buffer); + int wordPos = 0; + final int wordLen = word.codePointCount(0, word.length()); + for (int depth = 0; depth < Constants.Dictionary.MAX_WORD_LENGTH; ++depth) { + if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD; + int groupOffset = buffer.position() - header.mHeaderSize; + final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer); + groupOffset += BinaryDictInputOutput.getGroupCountSize(charGroupCount); + + for (int i = 0; i < charGroupCount; ++i) { + final int charGroupPos = buffer.position(); + final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer, + buffer.position(), header.mFormatOptions); + boolean same = true; + for (int p = 0, j = word.offsetByCodePoints(0, wordPos); + p < currentInfo.mCharacters.length; + ++p, j = word.offsetByCodePoints(j, 1)) { + if (wordPos + p >= wordLen + || word.codePointAt(j) != currentInfo.mCharacters[p]) { + same = false; + break; + } + } + + if (same) { + if (wordPos + currentInfo.mCharacters.length == wordLen) { + if (currentInfo.mFrequency == CharGroup.NOT_A_TERMINAL) { + return FormatSpec.NOT_VALID_WORD; + } else { + return charGroupPos; + } + } + wordPos += currentInfo.mCharacters.length; + if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { + return FormatSpec.NOT_VALID_WORD; + } + buffer.position(currentInfo.mChildrenAddress); + break; + } + groupOffset = currentInfo.mEndAddress; + + // not found + if (i >= charGroupCount - 1) { + return FormatSpec.NOT_VALID_WORD; + } + } + } + return FormatSpec.NOT_VALID_WORD; + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java index 6f508695e..1d3e94bb7 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java @@ -63,6 +63,7 @@ public class BinaryDictInputOutput { public int position(); public void position(int newPosition); public void put(final byte b); + public int limit(); } public static final class ByteBufferWrapper implements FusionDictionaryBufferInterface { @@ -107,6 +108,11 @@ public class BinaryDictInputOutput { public void put(final byte b) { mBuffer.put(b); } + + @Override + public int limit() { + return mBuffer.limit(); + } } /** @@ -284,7 +290,7 @@ public class BinaryDictInputOutput { * @param count the group count * @return the size of the group count, either 1 or 2 bytes. */ - private static int getGroupCountSize(final int count) { + public static int getGroupCountSize(final int count) { if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) { return 1; } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) { @@ -370,13 +376,16 @@ public class BinaryDictInputOutput { g.mCachedSize = groupSize; size += groupSize; } + if (options.mHasLinkedListNode) { + size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; + } node.mCachedSize = size; } /** * Helper method to hide the actual value of the no children address. */ - private static boolean hasChildrenAddress(final int address) { + public static boolean hasChildrenAddress(final int address) { return FormatSpec.NO_CHILDREN_ADDRESS != address; } @@ -521,6 +530,9 @@ public class BinaryDictInputOutput { group.mCachedSize = groupSize; size += groupSize; } + if (formatOptions.mHasLinkedListNode) { + size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; + } if (node.mCachedSize != size) { node.mCachedSize = size; changed = true; @@ -532,9 +544,11 @@ public class BinaryDictInputOutput { * Computes the byte size of a list of nodes and updates each node cached position. * * @param flatNodes the array of nodes. + * @param formatOptions file format options. * @return the byte size of the entire stack. */ - private static int stackNodes(final ArrayList<Node> flatNodes) { + private static int stackNodes(final ArrayList<Node> flatNodes, + final FormatOptions formatOptions) { int nodeOffset = 0; for (Node n : flatNodes) { n.mCachedAddress = nodeOffset; @@ -544,7 +558,9 @@ public class BinaryDictInputOutput { g.mCachedAddress = groupCountSize + nodeOffset + groupOffset; groupOffset += g.mCachedSize; } - if (groupOffset + groupCountSize != n.mCachedSize) { + final int nodeSize = groupCountSize + groupOffset + + (formatOptions.mHasLinkedListNode ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0); + if (nodeSize != n.mCachedSize) { throw new RuntimeException("Bug : Stored and computed node size differ"); } nodeOffset += n.mCachedSize; @@ -571,7 +587,7 @@ public class BinaryDictInputOutput { final ArrayList<Node> flatNodes, final FormatOptions formatOptions) { // First get the worst sizes and offsets for (Node n : flatNodes) setNodeMaximumSize(n, formatOptions); - final int offset = stackNodes(flatNodes); + final int offset = stackNodes(flatNodes, formatOptions); MakedictLog.i("Compressing the array addresses. Original size : " + offset); MakedictLog.i("(Recursively seen size : " + offset + ")"); @@ -587,7 +603,7 @@ public class BinaryDictInputOutput { if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!"); changesDone |= changed; } - stackNodes(flatNodes); + stackNodes(flatNodes, formatOptions); ++passes; if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug"); } while (changesDone); @@ -776,7 +792,8 @@ public class BinaryDictInputOutput { return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0) + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0) + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0) - + (formatOptions.mHasParentAddress ? FormatSpec.HAS_PARENT_ADDRESS : 0); + + (formatOptions.mHasParentAddress ? FormatSpec.HAS_PARENT_ADDRESS : 0) + + (formatOptions.mHasLinkedListNode ? FormatSpec.HAS_LINKEDLIST_NODE : 0); } /** @@ -910,6 +927,11 @@ public class BinaryDictInputOutput { } } + if (formatOptions.mHasLinkedListNode) { + buffer[index] = buffer[index + 1] = buffer[index + 2] + = FormatSpec.NO_FORWARD_LINK_ADDRESS; + index += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; + } if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException( "Not the same size : written " + (index - node.mCachedAddress) + " bytes out of a node that should have " @@ -1083,7 +1105,7 @@ public class BinaryDictInputOutput { // readDictionaryBinary is the public entry point for them. private static final int[] CHARACTER_BUFFER = new int[FormatSpec.MAX_WORD_LENGTH]; - private static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer, + public static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer, final int originalGroupAddress, final FormatOptions options) { int addressPointer = originalGroupAddress; final int flags = buffer.readUnsignedByte(); @@ -1196,7 +1218,7 @@ public class BinaryDictInputOutput { /** * Reads and returns the char group count out of a buffer and forwards the pointer. */ - private static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) { + public static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) { final int msb = buffer.readUnsignedByte(); if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) { return msb; @@ -1220,8 +1242,9 @@ public class BinaryDictInputOutput { * @param formatOptions file format options. * @return the word, as a string. */ - private static String getWordAtAddress(final FusionDictionaryBufferInterface buffer, - final int headerSize, final int address, final FormatOptions formatOptions) { + /* packages for tests */ static String getWordAtAddress( + final FusionDictionaryBufferInterface buffer, final int headerSize, final int address, + final FormatOptions formatOptions) { final String cachedString = wordCache.get(address); if (null != cachedString) return cachedString; @@ -1325,146 +1348,67 @@ public class BinaryDictInputOutput { final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap, final FormatOptions options) throws IOException { - final int nodeOrigin = buffer.position() - headerSize; - final int count = readCharGroupCount(buffer); final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>(); - int groupOffset = nodeOrigin + getGroupCountSize(count); - for (int i = count; i > 0; --i) { - CharGroupInfo info = readCharGroup(buffer, groupOffset, options); - ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets; - ArrayList<WeightedString> bigrams = null; - if (null != info.mBigrams) { - bigrams = new ArrayList<WeightedString>(); - for (PendingAttribute bigram : info.mBigrams) { - final String word = getWordAtAddress( - buffer, headerSize, bigram.mAddress, options); - bigrams.add(new WeightedString(word, bigram.mFrequency)); + final int nodeOrigin = buffer.position() - headerSize; + + do { // Scan the linked-list node. + final int nodeHeadPosition = buffer.position() - headerSize; + final int count = readCharGroupCount(buffer); + int groupOffset = nodeHeadPosition + getGroupCountSize(count); + for (int i = count; i > 0; --i) { // Scan the array of CharGroup. + CharGroupInfo info = readCharGroup(buffer, groupOffset, options); + ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets; + ArrayList<WeightedString> bigrams = null; + if (null != info.mBigrams) { + bigrams = new ArrayList<WeightedString>(); + for (PendingAttribute bigram : info.mBigrams) { + final String word = getWordAtAddress( + buffer, headerSize, bigram.mAddress, options); + bigrams.add(new WeightedString(word, bigram.mFrequency)); + } + } + if (hasChildrenAddress(info.mChildrenAddress)) { + Node children = reverseNodeMap.get(info.mChildrenAddress); + if (null == children) { + final int currentPosition = buffer.position(); + buffer.position(info.mChildrenAddress + headerSize); + children = readNode( + buffer, headerSize, reverseNodeMap, reverseGroupMap, options); + buffer.position(currentPosition); + } + nodeContents.add( + new CharGroup(info.mCharacters, shortcutTargets, bigrams, + info.mFrequency, + 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children)); + } else { + nodeContents.add( + new CharGroup(info.mCharacters, shortcutTargets, bigrams, + info.mFrequency, + 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), + 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED))); } + groupOffset = info.mEndAddress; } - if (hasChildrenAddress(info.mChildrenAddress)) { - Node children = reverseNodeMap.get(info.mChildrenAddress); - if (null == children) { - final int currentPosition = buffer.position(); - buffer.position(info.mChildrenAddress + headerSize); - children = readNode( - buffer, headerSize, reverseNodeMap, reverseGroupMap, options); - buffer.position(currentPosition); + + // reach the end of the array. + if (options.mHasLinkedListNode) { + final int nextAddress = buffer.readUnsignedInt24(); + if (nextAddress >= 0 && nextAddress < buffer.limit()) { + buffer.position(nextAddress); + } else { + break; } - nodeContents.add( - new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children)); - } else { - nodeContents.add( - new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED))); } - groupOffset = info.mEndAddress; - } + } while (options.mHasLinkedListNode && + buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS); + final Node node = new Node(nodeContents); node.mCachedAddress = nodeOrigin; reverseNodeMap.put(node.mCachedAddress, node); return node; } - // TODO: move these methods (readUnigramsAndBigramsBinary(|Inner)) and an inner class (Position) - // out of this class. - private static class Position { - public static final int NOT_READ_GROUPCOUNT = -1; - - public int mAddress; - public int mNumOfCharGroup; - public int mPosition; - public int mLength; - - public Position(int address, int length) { - mAddress = address; - mLength = length; - mNumOfCharGroup = NOT_READ_GROUPCOUNT; - } - } - - /** - * Tours all node without recursive call. - */ - private static void readUnigramsAndBigramsBinaryInner( - final FusionDictionaryBufferInterface buffer, final int headerSize, - final Map<Integer, String> words, final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams, - final FormatOptions formatOptions) { - int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1]; - - Stack<Position> stack = new Stack<Position>(); - int index = 0; - - Position initPos = new Position(headerSize, 0); - stack.push(initPos); - - while (!stack.empty()) { - Position p = stack.peek(); - - if (DBG) { - MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" + - p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength); - } - - if (buffer.position() != p.mAddress) buffer.position(p.mAddress); - if (index != p.mLength) index = p.mLength; - - if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) { - p.mNumOfCharGroup = readCharGroupCount(buffer); - p.mAddress += getGroupCountSize(p.mNumOfCharGroup); - p.mPosition = 0; - } - - CharGroupInfo info = readCharGroup(buffer, p.mAddress - headerSize, formatOptions); - for (int i = 0; i < info.mCharacters.length; ++i) { - pushedChars[index++] = info.mCharacters[i]; - } - p.mPosition++; - - if (info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) { // found word - words.put(info.mOriginalAddress, new String(pushedChars, 0, index)); - frequencies.put(info.mOriginalAddress, info.mFrequency); - if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams); - } - - if (p.mPosition == p.mNumOfCharGroup) { - stack.pop(); - } else { - // the node has more groups. - p.mAddress = buffer.position(); - } - - if (hasChildrenAddress(info.mChildrenAddress)) { - Position childrenPos = new Position(info.mChildrenAddress + headerSize, index); - stack.push(childrenPos); - } - } - } - - /** - * Reads unigrams and bigrams from the binary file. - * Doesn't make the memory representation of the dictionary. - * - * @param buffer the buffer to read. - * @param words the map to store the address as a key and the word as a value. - * @param frequencies the map to store the address as a key and the frequency as a value. - * @param bigrams the map to store the address as a key and the list of address as a value. - * @throws IOException - * @throws UnsupportedFormatException - */ - public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer, - final Map<Integer, String> words, final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, - UnsupportedFormatException { - // Read header - final FileHeader header = readHeader(buffer); - readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams, - header.mFormatOptions); - } - /** * Helper function to get the binary format version from the header. * @throws IOException @@ -1501,7 +1445,7 @@ public class BinaryDictInputOutput { * @throws IOException * @throws UnsupportedFormatException */ - private static FileHeader readHeader(final FusionDictionaryBufferInterface buffer) + public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer) throws IOException, UnsupportedFormatException { final int version = checkFormatVersion(buffer); final int optionsFlags = buffer.readUnsignedShort(); @@ -1525,7 +1469,8 @@ public class BinaryDictInputOutput { 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG), 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)), new FormatOptions(version, - 0 != (optionsFlags & FormatSpec.HAS_PARENT_ADDRESS))); + 0 != (optionsFlags & FormatSpec.HAS_PARENT_ADDRESS), + 0 != (optionsFlags & FormatSpec.HAS_LINKEDLIST_NODE))); return header; } @@ -1543,11 +1488,6 @@ public class BinaryDictInputOutput { options.put(key, value); } } - // TODO: remove this method. - public static void populateOptions(final ByteBuffer buffer, final int headerSize, - final HashMap<String, String> options) { - populateOptions(new ByteBufferWrapper(buffer), headerSize, options); - } /** * Reads a buffer and returns the memory representation of the dictionary. diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index 1707ccc39..adc6037bb 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -41,6 +41,12 @@ public final class FormatSpec { * u | * ps * + * f | + * o | IF HAS_LINKEDLIST_NODE (defined in the file header) + * r | forward link address, 3byte + * w | the address must be positive. + * a | + * rdlinkaddress */ /* Node(CharGroup) layout is as follows: @@ -140,18 +146,23 @@ public final class FormatSpec { static final int NOT_A_VERSION_NUMBER = -1; static final int FIRST_VERSION_WITH_HEADER_SIZE = 2; static final int FIRST_VERSION_WITH_PARENT_ADDRESS = 3; + static final int FIRST_VERSION_WITH_LINKEDLIST_NODE = 3; // These options need to be the same numeric values as the one in the native reading code. static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; + // TODO: Make the native reading code read this variable. static final int HAS_PARENT_ADDRESS = 0x2; static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4; static final int CONTAINS_BIGRAMS_FLAG = 0x8; + // TODO: Make the native reading code read this variable. + static final int HAS_LINKEDLIST_NODE = 0x10; // TODO: Make this value adaptative to content data, store it in the header, and // use it in the reading code. static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; static final int PARENT_ADDRESS_SIZE = 3; + static final int FORWARD_LINK_ADDRESS_SIZE = 3; static final int MASK_GROUP_ADDRESS_TYPE = 0xC0; static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00; @@ -187,6 +198,7 @@ public final class FormatSpec { static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; static final int NO_PARENT_ADDRESS = 0; + static final int NO_FORWARD_LINK_ADDRESS = 0; static final int INVALID_CHARACTER = -1; static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127 @@ -195,22 +207,39 @@ public final class FormatSpec { static final int MAX_TERMINAL_FREQUENCY = 255; static final int MAX_BIGRAM_FREQUENCY = 15; + // This option needs to be the same numeric value as the one in binary_format.h. + static final int NOT_VALID_WORD = -99; + /** * Options about file format. */ public static class FormatOptions { public final int mVersion; public final boolean mHasParentAddress; + public final boolean mHasLinkedListNode; public FormatOptions(final int version) { this(version, false); } public FormatOptions(final int version, final boolean hasParentAddress) { + this(version, hasParentAddress, false); + } + public FormatOptions(final int version, final boolean hasParentAddress, + final boolean hasLinkedListNode) { mVersion = version; - if (version < FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS && hasParentAddress) { + if (version < FIRST_VERSION_WITH_PARENT_ADDRESS && hasParentAddress) { throw new RuntimeException("Parent addresses are only supported with versions " - + FormatSpec.FIRST_VERSION_WITH_PARENT_ADDRESS + " and ulterior."); + + FIRST_VERSION_WITH_PARENT_ADDRESS + " and ulterior."); } mHasParentAddress = hasParentAddress; + + if (version < FIRST_VERSION_WITH_LINKEDLIST_NODE && hasLinkedListNode) { + throw new RuntimeException("Linked list nodes are only supported with versions " + + FIRST_VERSION_WITH_LINKEDLIST_NODE + " and ulterior."); + } + if (!hasParentAddress && hasLinkedListNode) { + throw new RuntimeException("Linked list nodes need parent addresses."); + } + mHasLinkedListNode = hasLinkedListNode; } } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index 6775de8a8..98cf308c8 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -556,6 +556,7 @@ public class FusionDictionary implements Iterable<Word> { final StringBuilder checker = DBG ? new StringBuilder() : null; CharGroup currentGroup; + final int codePointCountInS = s.codePointCount(0, s.length()); do { int indexOfGroup = findIndexOfChar(node, s.codePointAt(index)); if (CHARACTER_NOT_FOUND == indexOfGroup) return null; @@ -570,12 +571,12 @@ public class FusionDictionary implements Iterable<Word> { index = newIndex; if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length)); - if (index < s.length()) { + if (index < codePointCountInS) { node = currentGroup.mChildren; } - } while (null != node && index < s.length()); + } while (null != node && index < codePointCountInS); - if (index < s.length()) return null; + if (index < codePointCountInS) return null; if (!currentGroup.isTerminal()) return null; if (DBG && !s.equals(checker.toString())) return null; return currentGroup; diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index f4784ff1a..d9b622a18 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -16,6 +16,9 @@ package com.android.inputmethod.latin.spellcheck; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.provider.UserDictionary.Words; import android.service.textservice.SpellCheckerService.Session; import android.text.TextUtils; import android.util.Log; @@ -45,6 +48,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. private final AndroidSpellCheckerService mService; protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache(); + private final ContentObserver mObserver; private static class SuggestionsParams { public final String[] mSuggestions; @@ -83,10 +87,23 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { mUnigramSuggestionsInfoCache.put( generateKey(query, prevWord), new SuggestionsParams(suggestions, flags)); } + + public void clearCache() { + mUnigramSuggestionsInfoCache.evictAll(); + } } AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) { mService = service; + final ContentResolver cres = service.getContentResolver(); + + mObserver = new ContentObserver(null) { + @Override + public void onChange(boolean self) { + mSuggestionsCache.clearCache(); + } + }; + cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); } @Override @@ -97,6 +114,12 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale); } + @Override + public void onClose() { + final ContentResolver cres = mService.getContentResolver(); + cres.unregisterContentObserver(mObserver); + } + /* * Returns whether the code point is a letter that makes sense for the specified * locale for this spell checker. |