aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
authorSatoshi Kataoka <satok@google.com>2012-07-04 15:38:14 +0900
committerSatoshi Kataoka <satok@google.com>2012-07-04 15:38:21 +0900
commitc68b37964b083015967ce290991ad69d29a4055d (patch)
tree909bddc0f0c699b6344015e7ff67e31c0a1f4eff /java/src
parent1e094ac19b99c47f0b8a5108e20949ac91cfa03e (diff)
parent30a324a58dbe1e2dc47d83c1bcc0af262ab0d542 (diff)
downloadlatinime-c68b37964b083015967ce290991ad69d29a4055d.tar.gz
latinime-c68b37964b083015967ce290991ad69d29a4055d.tar.xz
latinime-c68b37964b083015967ce290991ad69d29a4055d.zip
Merge remote-tracking branch 'goog/master' into mergescript
Conflicts: CleanSpec.mk java/Android.mk java/res/drawable-large-hdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-large-hdpi/hint_popup_holo.9.png java/res/drawable-large-hdpi/sym_keyboard_numsymbol_holo.png java/res/drawable-large-hdpi/sym_keyboard_tab_holo.png java/res/drawable-large-land-hdpi/hint_popup_holo.9.png java/res/drawable-large-land-mdpi/hint_popup_holo.9.png java/res/drawable-large-land-xhdpi/hint_popup_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_normal_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_normal_off_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_normal_on_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_pressed_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_pressed_off_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_dark_pressed_on_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_light_normal_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_light_pressed_holo.9.png java/res/drawable-large-mdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-large-mdpi/hint_popup_holo.9.png java/res/drawable-large-mdpi/keyboard_background_holo.9.png java/res/drawable-large-mdpi/keyboard_popup_panel_background_holo.9.png java/res/drawable-large-mdpi/keyboard_suggest_strip_holo.9.png java/res/drawable-large-mdpi/sym_keyboard_delete_holo.png java/res/drawable-large-mdpi/sym_keyboard_num0_holo.png java/res/drawable-large-mdpi/sym_keyboard_num1_holo.png java/res/drawable-large-mdpi/sym_keyboard_num2_holo.png java/res/drawable-large-mdpi/sym_keyboard_num3_holo.png java/res/drawable-large-mdpi/sym_keyboard_num4_holo.png java/res/drawable-large-mdpi/sym_keyboard_num5_holo.png java/res/drawable-large-mdpi/sym_keyboard_num6_holo.png java/res/drawable-large-mdpi/sym_keyboard_num7_holo.png java/res/drawable-large-mdpi/sym_keyboard_num8_holo.png java/res/drawable-large-mdpi/sym_keyboard_num9_holo.png java/res/drawable-large-mdpi/sym_keyboard_numbpound_holo.png java/res/drawable-large-mdpi/sym_keyboard_numbstar_holo.png java/res/drawable-large-mdpi/sym_keyboard_numsymbol_holo.png java/res/drawable-large-mdpi/sym_keyboard_return_holo.png java/res/drawable-large-mdpi/sym_keyboard_settings_holo.png java/res/drawable-large-mdpi/sym_keyboard_shift_holo.png java/res/drawable-large-mdpi/sym_keyboard_shift_locked_holo.png java/res/drawable-large-mdpi/sym_keyboard_space_holo.png java/res/drawable-large-mdpi/sym_keyboard_tab_holo.png java/res/drawable-large-mdpi/sym_keyboard_voice_holo.png java/res/drawable-large-mdpi/sym_keyboard_voice_off_holo.png java/res/drawable-large-xhdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-large-xhdpi/hint_popup_holo.9.png java/res/drawable-large-xhdpi/sym_keyboard_numsymbol_holo.png java/res/drawable-large-xhdpi/sym_keyboard_tab_holo.png java/res/drawable-xlarge-hdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-xlarge-hdpi/hint_popup_holo.9.png java/res/drawable-xlarge-land-hdpi/hint_popup_holo.9.png java/res/drawable-xlarge-land-mdpi/hint_popup_holo.9.png java/res/drawable-xlarge-land-xhdpi/hint_popup_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_normal_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_normal_off_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_normal_on_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_pressed_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_pressed_off_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_dark_pressed_on_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_light_normal_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_light_pressed_holo.9.png java/res/drawable-xlarge-mdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-xlarge-mdpi/hint_popup_holo.9.png java/res/drawable-xlarge-mdpi/keyboard_background_holo.9.png java/res/drawable-xlarge-mdpi/keyboard_popup_panel_background_holo.9.png java/res/drawable-xlarge-mdpi/keyboard_suggest_strip_holo.9.png java/res/drawable-xlarge-mdpi/sym_keyboard_delete_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num0_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num1_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num2_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num3_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num4_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num5_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num6_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num7_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num8_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_num9_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_numbpound_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_numbstar_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_return_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_settings_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_shift_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_shift_locked_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_space_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_voice_holo.png java/res/drawable-xlarge-mdpi/sym_keyboard_voice_off_holo.png java/res/drawable-xlarge-xhdpi/btn_keyboard_key_popup_selected_holo.9.png java/res/drawable-xlarge-xhdpi/hint_popup_holo.9.png java/res/layout-xlarge/recognition_status.xml java/res/values-af/strings.xml java/res/values-am/strings.xml java/res/values-ar/strings.xml java/res/values-be/strings.xml java/res/values-bg/strings.xml java/res/values-ca/strings.xml java/res/values-cs/strings.xml java/res/values-da/strings.xml java/res/values-de/strings.xml java/res/values-el/strings.xml java/res/values-en-rGB/strings.xml java/res/values-es-rUS/strings.xml java/res/values-es/strings.xml java/res/values-et/strings.xml java/res/values-fa/strings.xml java/res/values-fi/strings.xml java/res/values-fr/strings.xml java/res/values-hi/strings.xml java/res/values-hr/strings.xml java/res/values-hu/strings.xml java/res/values-in/strings.xml java/res/values-it/strings.xml java/res/values-iw/strings.xml java/res/values-ja/strings.xml java/res/values-ko/strings.xml java/res/values-large/donottranslate.xml java/res/values-lt/strings.xml java/res/values-lv/strings.xml java/res/values-ms/strings.xml java/res/values-nb/strings.xml java/res/values-nl/strings.xml java/res/values-pl/strings.xml java/res/values-pt-rPT/strings.xml java/res/values-pt/strings.xml java/res/values-rm/strings.xml java/res/values-ro/strings.xml java/res/values-ru/strings.xml java/res/values-sk/strings.xml java/res/values-sl/strings.xml java/res/values-sr/strings.xml java/res/values-sv/strings.xml java/res/values-sw/strings.xml java/res/values-sw600dp/donottranslate.xml java/res/values-sw768dp/donottranslate.xml java/res/values-th/strings.xml java/res/values-tl/strings.xml java/res/values-tr/strings.xml java/res/values-uk/strings.xml java/res/values-vi/strings.xml java/res/values-xlarge/donottranslate.xml java/res/values-zh-rCN/strings.xml java/res/values-zh-rTW/strings.xml java/res/values-zu/strings.xml java/res/values/keypress-vibration-durations.xml java/res/values/predefined-subtypes.xml java/res/xml-large-land/kbd_popup_template.xml java/res/xml-large/kbd_key_styles.xml java/res/xml-large/kbd_popup_template.xml java/res/xml-large/kbd_qwerty_f2.xml java/res/xml-large/kbd_qwerty_row1.xml java/res/xml-large/kbd_qwerty_row2.xml java/res/xml-large/kbd_qwerty_row3.xml java/res/xml-large/kbd_qwerty_row4.xml java/res/xml-large/kbd_row3_right.xml java/res/xml-large/kbd_rows_arabic.xml java/res/xml-large/kbd_rows_azerty.xml java/res/xml-large/kbd_rows_hebrew.xml java/res/xml-large/kbd_rows_qwerty.xml java/res/xml-large/kbd_rows_qwertz.xml java/res/xml-large/kbd_rows_russian.xml java/res/xml-large/kbd_rows_scandinavian.xml java/res/xml-large/kbd_rows_serbian.xml java/res/xml-large/kbd_rows_spanish.xml java/res/xml-large/kbd_symbols.xml java/res/xml-large/kbd_symbols_shift.xml java/res/xml-sw600dp-land/kbd_more_keys_keyboard_template.xml java/res/xml-sw600dp-land/kbd_popup_template.xml java/res/xml-sw600dp/kbd_more_keys_keyboard_template.xml java/res/xml-sw600dp/kbd_popup_template.xml java/res/xml-sw600dp/kbd_row3_right.xml java/res/xml-sw600dp/kbd_rows_qwerty.xml java/res/xml-sw600dp/keys_comma_period.xml java/res/xml-sw768dp-land/kbd_more_keys_keyboard_template.xml java/res/xml-sw768dp-land/kbd_popup_template.xml java/res/xml-sw768dp/kbd_more_keys_keyboard_template.xml java/res/xml-sw768dp/kbd_popup_template.xml java/res/xml-sw768dp/kbd_row3_right2.xml java/res/xml-sw768dp/kbd_rows_qwerty.xml java/res/xml-sw768dp/row_symbols_shift4.xml java/res/xml-xlarge-land/kbd_popup_template.xml java/res/xml-xlarge/kbd_key_styles.xml java/res/xml-xlarge/kbd_popup_template.xml java/res/xml-xlarge/kbd_qwerty_row1.xml java/res/xml-xlarge/kbd_qwerty_row2.xml java/res/xml-xlarge/kbd_qwerty_row3.xml java/res/xml-xlarge/kbd_qwerty_row4.xml java/res/xml-xlarge/kbd_row3_right2.xml java/res/xml-xlarge/kbd_rows_arabic.xml java/res/xml-xlarge/kbd_rows_azerty.xml java/res/xml-xlarge/kbd_rows_hebrew.xml java/res/xml-xlarge/kbd_rows_qwerty.xml java/res/xml-xlarge/kbd_rows_qwertz.xml java/res/xml-xlarge/kbd_rows_russian.xml java/res/xml-xlarge/kbd_rows_scandinavian.xml java/res/xml-xlarge/kbd_rows_serbian.xml java/res/xml-xlarge/kbd_rows_spanish.xml java/res/xml-xlarge/kbd_symbols.xml java/res/xml-xlarge/kbd_symbols_shift.xml java/res/xml/key_azerty_quote.xml java/res/xml/key_f1.xml java/res/xml/method.xml java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java java/src/com/android/inputmethod/latin/Utils.java native/Android.mk Change-Id: I96e8e042f636ed8e5cc023cf8514f13121e39195
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java342
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java96
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java131
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java37
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java334
-rw-r--r--java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java222
-rw-r--r--java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java39
-rw-r--r--java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java42
-rw-r--r--java/src/com/android/inputmethod/compat/ArraysCompatUtils.java50
-rw-r--r--java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java54
-rw-r--r--java/src/com/android/inputmethod/compat/CompatUtils.java40
-rw-r--r--java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java111
-rw-r--r--java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java80
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java59
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java188
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java84
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java161
-rw-r--r--java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java118
-rw-r--r--java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java55
-rw-r--r--java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java (renamed from java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java)27
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java71
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java44
-rw-r--r--java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java90
-rw-r--r--java/src/com/android/inputmethod/deprecated/VoiceProxy.java842
-rw-r--r--java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java36
-rw-r--r--java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java255
-rw-r--r--java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java234
-rw-r--r--java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java287
-rw-r--r--java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java62
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/FieldContext.java104
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Hints.java188
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java354
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java110
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java155
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java692
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java266
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java92
-rw-r--r--java/src/com/android/inputmethod/deprecated/voice/Whitelist.java68
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java866
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyDetector.java177
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java1520
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java47
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java278
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java406
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java884
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java1565
-rw-r--r--java/src/com/android/inputmethod/keyboard/LatinKeyboard.java433
-rw-r--r--java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java1103
-rw-r--r--java/src/com/android/inputmethod/keyboard/MiniKeyboard.java52
-rw-r--r--java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java59
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java51
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java371
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java178
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java57
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java874
-rw-r--r--java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java112
-rw-r--r--java/src/com/android/inputmethod/keyboard/PopupPanel.java45
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java219
-rw-r--r--java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java153
-rw-r--r--java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java (renamed from java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java)19
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java (renamed from java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java)62
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java498
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java297
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java151
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java159
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java726
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java617
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java2511
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java250
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java18
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java101
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java185
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/Row.java73
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java164
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java157
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtype.java106
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java599
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java8
-rw-r--r--java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java104
-rw-r--r--java/src/com/android/inputmethod/latin/AutoCorrection.java145
-rw-r--r--java/src/com/android/inputmethod/latin/AutoDictionary.java250
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java152
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java301
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java362
-rw-r--r--java/src/com/android/inputmethod/latin/CandidateView.java633
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java127
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java293
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsDictionary.java162
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettings.java29
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettingsActivity.java36
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java49
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java56
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java148
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java2
-rw-r--r--java/src/com/android/inputmethod/latin/EditingUtils.java293
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java473
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java672
-rw-r--r--java/src/com/android/inputmethod/latin/FileTransforms.java38
-rw-r--r--java/src/com/android/inputmethod/latin/Flag.java64
-rw-r--r--java/src/com/android/inputmethod/latin/ImfUtils.java179
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java176
-rw-r--r--java/src/com/android/inputmethod/latin/InputTypeUtils.java90
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java112
-rw-r--r--java/src/com/android/inputmethod/latin/JniUtils.java41
-rw-r--r--java/src/com/android/inputmethod/latin/LastComposedWord.java86
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java2754
-rw-r--r--java/src/com/android/inputmethod/latin/LatinImeLogger.java22
-rw-r--r--java/src/com/android/inputmethod/latin/LocaleUtils.java222
-rw-r--r--java/src/com/android/inputmethod/latin/NativeUtils.java (renamed from java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java)21
-rw-r--r--java/src/com/android/inputmethod/latin/ResearchLogger.java1043
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java429
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java712
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsActivity.java34
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java418
-rw-r--r--java/src/com/android/inputmethod/latin/SharedPreferencesCompat.java53
-rw-r--r--java/src/com/android/inputmethod/latin/StringUtils.java197
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeLocale.java180
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java577
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java672
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java255
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java55
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java47
-rw-r--r--java/src/com/android/inputmethod/latin/TargetApplicationGetter.java70
-rw-r--r--java/src/com/android/inputmethod/latin/TextEntryState.java236
-rw-r--r--java/src/com/android/inputmethod/latin/UserBigramDictionary.java399
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java224
-rw-r--r--java/src/com/android/inputmethod/latin/UserDictionary.java158
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionary.java586
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java120
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java222
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java566
-rw-r--r--java/src/com/android/inputmethod/latin/VibratorUtils.java (renamed from java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java)33
-rw-r--r--java/src/com/android/inputmethod/latin/WhitelistDictionary.java61
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java308
-rw-r--r--java/src/com/android/inputmethod/latin/WordListInfo.java (renamed from java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java)20
-rw-r--r--java/src/com/android/inputmethod/latin/XmlParseUtils.java80
-rw-r--r--java/src/com/android/inputmethod/latin/define/JniLibName.java25
-rw-r--r--java/src/com/android/inputmethod/latin/define/ProductionFlag.java25
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java1413
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java50
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java767
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/MakedictLog.java47
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java32
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java26
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Word.java91
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java837
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java32
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java86
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java115
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java214
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java39
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java39
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java231
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java221
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java889
157 files changed, 26216 insertions, 17270 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
new file mode 100644
index 000000000..70e38fdb0
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -0,0 +1,342 @@
+/*
+ * 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.accessibility;
+
+import android.graphics.Rect;
+import android.inputmethodservice.InputMethodService;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardView;
+
+/**
+ * Exposes a virtual view sub-tree for {@link KeyboardView} and generates
+ * {@link AccessibilityEvent}s for individual {@link Key}s.
+ * <p>
+ * A virtual sub-tree is composed of imaginary {@link View}s that are reported
+ * as a part of the view hierarchy for accessibility purposes. This enables
+ * custom views that draw complex content to report them selves as a tree of
+ * virtual views, thus conveying their logical structure.
+ * </p>
+ */
+public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
+ private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
+ private static final int UNDEFINED = Integer.MIN_VALUE;
+
+ private final InputMethodService mInputMethodService;
+ private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
+ private final AccessibilityUtils mAccessibilityUtils;
+
+ /** A map of integer IDs to {@link Key}s. */
+ private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();
+
+ /** Temporary rect used to calculate in-screen bounds. */
+ private final Rect mTempBoundsInScreen = new Rect();
+
+ /** The parent view's cached on-screen location. */
+ private final int[] mParentLocation = new int[2];
+
+ /** The virtual view identifier for the focused node. */
+ private int mAccessibilityFocusedView = UNDEFINED;
+
+ /** The current keyboard view. */
+ private KeyboardView mKeyboardView;
+
+ public AccessibilityEntityProvider(KeyboardView keyboardView, InputMethodService inputMethod) {
+ mInputMethodService = inputMethod;
+
+ mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
+ mAccessibilityUtils = AccessibilityUtils.getInstance();
+
+ setView(keyboardView);
+ }
+
+ /**
+ * Sets the keyboard view represented by this node provider.
+ *
+ * @param keyboardView The keyboard view to represent.
+ */
+ public void setView(KeyboardView keyboardView) {
+ mKeyboardView = keyboardView;
+ updateParentLocation();
+
+ // Since this class is constructed lazily, we might not get a subsequent
+ // call to setKeyboard() and therefore need to call it now.
+ setKeyboard(mKeyboardView.getKeyboard());
+ }
+
+ /**
+ * Sets the keyboard represented by this node provider.
+ *
+ * @param keyboard The keyboard to represent.
+ */
+ public void setKeyboard(Keyboard keyboard) {
+ assignVirtualViewIds();
+ }
+
+ /**
+ * Creates and populates an {@link AccessibilityEvent} for the specified key
+ * and event type.
+ *
+ * @param key A key on the host keyboard view.
+ * @param eventType The event type to create.
+ * @return A populated {@link AccessibilityEvent} for the key.
+ * @see AccessibilityEvent
+ */
+ public AccessibilityEvent createAccessibilityEvent(Key key, int eventType) {
+ final int virtualViewId = generateVirtualViewIdForKey(key);
+ final String keyDescription = getKeyDescription(key);
+
+ final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+ event.setPackageName(mKeyboardView.getContext().getPackageName());
+ event.setClassName(key.getClass().getName());
+ event.setContentDescription(keyDescription);
+ event.setEnabled(true);
+
+ final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event);
+ record.setSource(mKeyboardView, virtualViewId);
+
+ return event;
+ }
+
+ /**
+ * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
+ * view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
+ * the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of
+ * the view hierarchy for accessibility purposes. This enables custom views
+ * that draw complex content to report them selves as a tree of virtual
+ * views, thus conveying their logical structure.
+ * </p>
+ * <p>
+ * The implementer is responsible for obtaining an accessibility node info
+ * from the pool of reusable instances and setting the desired properties of
+ * the node info before returning it.
+ * </p>
+ *
+ * @param virtualViewId A client defined virtual view id.
+ * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual
+ * descendant or the host View.
+ * @see AccessibilityNodeInfoCompat
+ */
+ @Override
+ public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) {
+ AccessibilityNodeInfoCompat info = null;
+
+ if (virtualViewId == UNDEFINED) {
+ return null;
+ } else if (virtualViewId == View.NO_ID) {
+ // We are requested to create an AccessibilityNodeInfo describing
+ // this View, i.e. the root of the virtual sub-tree.
+ info = AccessibilityNodeInfoCompat.obtain(mKeyboardView);
+ ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, info);
+
+ // Add the virtual children of the root View.
+ final Keyboard keyboard = mKeyboardView.getKeyboard();
+ final Key[] keys = keyboard.mKeys;
+ for (Key key : keys) {
+ final int childVirtualViewId = generateVirtualViewIdForKey(key);
+ info.addChild(mKeyboardView, childVirtualViewId);
+ }
+ } else {
+ // Find the view that corresponds to the given id.
+ final Key key = mVirtualViewIdToKey.get(virtualViewId);
+ if (key == null) {
+ Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
+ return null;
+ }
+
+ final String keyDescription = getKeyDescription(key);
+ final Rect boundsInParent = key.mHitBox;
+
+ // Calculate the key's in-screen bounds.
+ mTempBoundsInScreen.set(boundsInParent);
+ mTempBoundsInScreen.offset(mParentLocation[0], mParentLocation[1]);
+
+ final Rect boundsInScreen = mTempBoundsInScreen;
+
+ // Obtain and initialize an AccessibilityNodeInfo with
+ // information about the virtual view.
+ info = AccessibilityNodeInfoCompat.obtain();
+ info.setPackageName(mKeyboardView.getContext().getPackageName());
+ info.setClassName(key.getClass().getName());
+ info.setContentDescription(keyDescription);
+ info.setBoundsInParent(boundsInParent);
+ info.setBoundsInScreen(boundsInScreen);
+ info.setParent(mKeyboardView);
+ info.setSource(mKeyboardView, virtualViewId);
+ info.setBoundsInScreen(boundsInScreen);
+ info.setEnabled(true);
+ info.setClickable(true);
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+
+ if (mAccessibilityFocusedView == virtualViewId) {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+ } else {
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
+ }
+ }
+
+ return info;
+ }
+
+ /**
+ * Simulates a key press by injecting touch events into the keyboard view.
+ * This avoids the complexity of trackers and listeners within the keyboard.
+ *
+ * @param key The key to press.
+ */
+ void simulateKeyPress(Key key) {
+ final int x = key.mHitBox.centerX();
+ final int y = key.mHitBox.centerY();
+ final long downTime = SystemClock.uptimeMillis();
+ final MotionEvent downEvent = MotionEvent.obtain(
+ downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
+ final MotionEvent upEvent = MotionEvent.obtain(
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, x, y, 0);
+
+ mKeyboardView.onTouchEvent(downEvent);
+ mKeyboardView.onTouchEvent(upEvent);
+ }
+
+ @Override
+ public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+ final Key key = mVirtualViewIdToKey.get(virtualViewId);
+
+ if (key == null) {
+ return false;
+ }
+
+ return performActionForKey(key, action, arguments);
+ }
+
+ /**
+ * Performs the specified accessibility action for the given key.
+ *
+ * @param key The on which to perform the action.
+ * @param action The action to perform.
+ * @param arguments The action's arguments.
+ * @return The result of performing the action, or false if the action is
+ * not supported.
+ */
+ boolean performActionForKey(Key key, int action, Bundle arguments) {
+ final int virtualViewId = generateVirtualViewIdForKey(key);
+
+ switch (action) {
+ case AccessibilityNodeInfoCompat.ACTION_CLICK:
+ simulateKeyPress(key);
+ return true;
+ case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
+ if (mAccessibilityFocusedView == virtualViewId) {
+ return false;
+ }
+ mAccessibilityFocusedView = virtualViewId;
+ sendAccessibilityEventForKey(
+ key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+ return true;
+ case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+ if (mAccessibilityFocusedView != virtualViewId) {
+ return false;
+ }
+ mAccessibilityFocusedView = UNDEFINED;
+ sendAccessibilityEventForKey(
+ key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Sends an accessibility event for the given {@link Key}.
+ *
+ * @param key The key that's sending the event.
+ * @param eventType The type of event to send.
+ */
+ void sendAccessibilityEventForKey(Key key, int eventType) {
+ final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
+ mAccessibilityUtils.requestSendAccessibilityEvent(event);
+ }
+
+ /**
+ * Returns the context-specific description for a {@link Key}.
+ *
+ * @param key The key to describe.
+ * @return The context-specific description of the key.
+ */
+ private String getKeyDescription(Key key) {
+ final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
+ final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
+ final String keyDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
+ mKeyboardView.getContext(), mKeyboardView.getKeyboard(), key, shouldObscure);
+
+ return keyDescription;
+ }
+
+ /**
+ * Assigns virtual view IDs to keyboard keys and populates the related maps.
+ */
+ private void assignVirtualViewIds() {
+ final Keyboard keyboard = mKeyboardView.getKeyboard();
+ if (keyboard == null) {
+ return;
+ }
+
+ mVirtualViewIdToKey.clear();
+
+ final Key[] keys = keyboard.mKeys;
+ for (Key key : keys) {
+ final int virtualViewId = generateVirtualViewIdForKey(key);
+ mVirtualViewIdToKey.put(virtualViewId, key);
+ }
+ }
+
+ /**
+ * Updates the parent's on-screen location.
+ */
+ private void updateParentLocation() {
+ mKeyboardView.getLocationOnScreen(mParentLocation);
+ }
+
+ /**
+ * Generates a virtual view identifier for the given key. Returned
+ * identifiers are valid until the next global layout state change.
+ *
+ * @param key The key to identify.
+ * @return A virtual view identifier.
+ */
+ private static int generateVirtualViewIdForKey(Key key) {
+ // The key x- and y-coordinates are stable between layout changes.
+ // Generate an identifier by bit-shifting the x-coordinate to the
+ // left-half of the integer and OR'ing with the y-coordinate.
+ return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY);
+ }
+}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index ae614b7e0..616b1c6d7 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -16,19 +16,21 @@
package com.android.inputmethod.accessibility;
-import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
-import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
+import android.media.AudioManager;
import android.os.SystemClock;
+import android.provider.Settings;
import android.util.Log;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.EditorInfo;
-import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
-import com.android.inputmethod.compat.AccessibilityManagerCompatWrapper;
-import com.android.inputmethod.compat.MotionEventCompatUtils;
+import com.android.inputmethod.compat.AudioManagerCompatWrapper;
+import com.android.inputmethod.compat.SettingsSecureCompatUtils;
+import com.android.inputmethod.latin.InputTypeUtils;
+import com.android.inputmethod.latin.R;
public class AccessibilityUtils {
private static final String TAG = AccessibilityUtils.class.getSimpleName();
@@ -38,8 +40,9 @@ public class AccessibilityUtils {
private static final AccessibilityUtils sInstance = new AccessibilityUtils();
+ private Context mContext;
private AccessibilityManager mAccessibilityManager;
- private AccessibilityManagerCompatWrapper mCompatManager;
+ private AudioManagerCompatWrapper mAudioManager;
/*
* Setting this constant to {@code false} will disable all keyboard
@@ -48,15 +51,14 @@ public class AccessibilityUtils {
*/
private static final boolean ENABLE_ACCESSIBILITY = true;
- public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
+ public static void init(InputMethodService inputMethod) {
if (!ENABLE_ACCESSIBILITY)
return;
// These only need to be initialized if the kill switch is off.
- sInstance.initInternal(inputMethod, prefs);
- KeyCodeDescriptionMapper.init(inputMethod, prefs);
- AccessibleInputMethodServiceProxy.init(inputMethod, prefs);
- AccessibleKeyboardViewProxy.init(inputMethod, prefs);
+ sInstance.initInternal(inputMethod);
+ KeyCodeDescriptionMapper.init();
+ AccessibleKeyboardViewProxy.init(inputMethod);
}
public static AccessibilityUtils getInstance() {
@@ -67,10 +69,14 @@ public class AccessibilityUtils {
// This class is not publicly instantiable.
}
- private void initInternal(Context context, SharedPreferences prefs) {
+ private void initInternal(Context context) {
+ mContext = context;
mAccessibilityManager = (AccessibilityManager) context
.getSystemService(Context.ACCESSIBILITY_SERVICE);
- mCompatManager = new AccessibilityManagerCompatWrapper(mAccessibilityManager);
+
+ final AudioManager audioManager = (AudioManager) context
+ .getSystemService(Context.AUDIO_SERVICE);
+ mAudioManager = new AudioManagerCompatWrapper(audioManager);
}
/**
@@ -82,10 +88,8 @@ public class AccessibilityUtils {
*/
public boolean isTouchExplorationEnabled() {
return ENABLE_ACCESSIBILITY
- && AccessibilityEventCompatUtils.supportsTouchExploration()
&& mAccessibilityManager.isEnabled()
- && !mCompatManager.getEnabledAccessibilityServiceList(
- AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty();
+ && mAccessibilityManager.isTouchExplorationEnabled();
}
/**
@@ -99,9 +103,35 @@ public class AccessibilityUtils {
public boolean isTouchExplorationEvent(MotionEvent event) {
final int action = event.getAction();
- return action == MotionEventCompatUtils.ACTION_HOVER_ENTER
- || action == MotionEventCompatUtils.ACTION_HOVER_EXIT
- || action == MotionEventCompatUtils.ACTION_HOVER_MOVE;
+ return action == MotionEvent.ACTION_HOVER_ENTER
+ || action == MotionEvent.ACTION_HOVER_EXIT
+ || action == MotionEvent.ACTION_HOVER_MOVE;
+ }
+
+ /**
+ * Returns whether the device should obscure typed password characters.
+ * Typically this means speaking "dot" in place of non-control characters.
+ *
+ * @return {@code true} if the device should obscure password characters.
+ */
+ public boolean shouldObscureInput(EditorInfo editorInfo) {
+ if (editorInfo == null)
+ return false;
+
+ // The user can optionally force speaking passwords.
+ if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
+ final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
+ SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
+ if (speakPassword)
+ return false;
+ }
+
+ // Always speak if the user is listening through headphones.
+ if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn())
+ return false;
+
+ // Don't speak if the IME is connected to a password field.
+ return InputTypeUtils.isPasswordInputType(editorInfo.inputType);
}
/**
@@ -120,7 +150,7 @@ public class AccessibilityUtils {
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
final AccessibilityEvent event = AccessibilityEvent
- .obtain(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
+ .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
event.setPackageName(PACKAGE);
event.setClassName(CLASS);
@@ -130,4 +160,30 @@ public class AccessibilityUtils {
mAccessibilityManager.sendAccessibilityEvent(event);
}
+
+ /**
+ * Handles speaking the "connect a headset to hear passwords" notification
+ * when connecting to a password field.
+ *
+ * @param editorInfo The input connection's editor info attribute.
+ * @param restarting Whether the connection is being restarted.
+ */
+ public void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+ if (shouldObscureInput(editorInfo)) {
+ final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
+ speak(text);
+ }
+ }
+
+ /**
+ * Sends the specified {@link AccessibilityEvent} if accessibility is
+ * enabled. No operation if accessibility is disabled.
+ *
+ * @param event The event to send.
+ */
+ public void requestSendAccessibilityEvent(AccessibilityEvent event) {
+ if (mAccessibilityManager.isEnabled()) {
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
deleted file mode 100644
index 7199550a9..000000000
--- a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2011 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.accessibility;
-
-import android.content.SharedPreferences;
-import android.inputmethodservice.InputMethodService;
-import android.os.Looper;
-import android.os.Message;
-import android.text.TextUtils;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-
-public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActionListener {
- private static final AccessibleInputMethodServiceProxy sInstance =
- new AccessibleInputMethodServiceProxy();
-
- /*
- * Delay for the handler event that's fired when Accessibility is on and the
- * user hovers outside of any valid keys. This is used to let the user know
- * that if they lift their finger, nothing will be typed.
- */
- private static final long DELAY_NO_HOVER_SELECTION = 250;
-
- private InputMethodService mInputMethod;
-
- private AccessibilityHandler mAccessibilityHandler;
-
- private static class AccessibilityHandler
- extends StaticInnerHandlerWrapper<AccessibleInputMethodServiceProxy> {
- private static final int MSG_NO_HOVER_SELECTION = 0;
-
- public AccessibilityHandler(AccessibleInputMethodServiceProxy outerInstance,
- Looper looper) {
- super(outerInstance, looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_NO_HOVER_SELECTION:
- getOuterInstance().notifyNoHoverSelection();
- break;
- }
- }
-
- public void postNoHoverSelection() {
- removeMessages(MSG_NO_HOVER_SELECTION);
- sendEmptyMessageDelayed(MSG_NO_HOVER_SELECTION, DELAY_NO_HOVER_SELECTION);
- }
-
- public void cancelNoHoverSelection() {
- removeMessages(MSG_NO_HOVER_SELECTION);
- }
- }
-
- public static void init(InputMethodService inputMethod, SharedPreferences prefs) {
- sInstance.initInternal(inputMethod, prefs);
- }
-
- public static AccessibleInputMethodServiceProxy getInstance() {
- return sInstance;
- }
-
- private AccessibleInputMethodServiceProxy() {
- // Not publicly instantiable.
- }
-
- private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
- mInputMethod = inputMethod;
- mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper());
- }
-
- /**
- * If touch exploration is enabled, cancels the event sent by
- * {@link AccessibleInputMethodServiceProxy#onHoverExit(int)} because the
- * user is currently hovering above a key.
- */
- @Override
- public void onHoverEnter(int primaryCode) {
- mAccessibilityHandler.cancelNoHoverSelection();
- }
-
- /**
- * If touch exploration is enabled, sends a delayed event to notify the user
- * that they are not currently hovering above a key.
- */
- @Override
- public void onHoverExit(int primaryCode) {
- mAccessibilityHandler.postNoHoverSelection();
- }
-
- /**
- * When Accessibility is turned on, notifies the user that they are not
- * currently hovering above a key. By default this will speak the currently
- * entered text.
- */
- private void notifyNoHoverSelection() {
- final ExtractedText extracted = mInputMethod.getCurrentInputConnection().getExtractedText(
- new ExtractedTextRequest(), 0);
-
- if (extracted == null)
- return;
-
- final CharSequence text;
-
- if (TextUtils.isEmpty(extracted.text)) {
- text = mInputMethod.getString(R.string.spoken_no_text_entered);
- } else {
- text = mInputMethod.getString(R.string.spoken_current_text_is, extracted.text);
- }
-
- AccessibilityUtils.getInstance().speak(text);
- }
-}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
deleted file mode 100644
index 12c59d0fc..000000000
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2011 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.accessibility;
-
-public interface AccessibleKeyboardActionListener {
- /**
- * Called when the user hovers inside a key. This is sent only when
- * Accessibility is turned on. For keys that repeat, this is only called
- * once.
- *
- * @param primaryCode the code of the key that was hovered over
- */
- public void onHoverEnter(int primaryCode);
-
- /**
- * Called when the user hovers outside a key. This is sent only when
- * Accessibility is turned on. For keys that repeat, this is only called
- * once.
- *
- * @param primaryCode the code of the key that was hovered over
- */
- public void onHoverExit(int primaryCode);
-}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 96f7fc9f2..f6376d5f4 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -17,185 +17,279 @@
package com.android.inputmethod.accessibility;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.util.Log;
+import android.inputmethodservice.InputMethodService;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.MotionEvent;
+import android.view.View;
import android.view.ViewConfiguration;
-import android.view.accessibility.AccessibilityEvent;
-import com.android.inputmethod.compat.AccessibilityEventCompatUtils;
-import com.android.inputmethod.compat.MotionEventCompatUtils;
import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
-import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.LatinKeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.R;
-public class AccessibleKeyboardViewProxy {
- private static final String TAG = AccessibleKeyboardViewProxy.class.getSimpleName();
+public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
- // Delay in milliseconds between key press DOWN and UP events
- private static final long DELAY_KEY_PRESS = 10;
+ private InputMethodService mInputMethod;
+ private LatinKeyboardView mView;
+ private AccessibilityEntityProvider mAccessibilityNodeProvider;
- private int mScaledEdgeSlop;
- private KeyboardView mView;
- private AccessibleKeyboardActionListener mListener;
+ private Key mLastHoverKey = null;
- private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
- private int mLastX = -1;
- private int mLastY = -1;
+ /**
+ * Inset in pixels to look for keys when the user's finger exits the
+ * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}.
+ */
+ private int mEdgeSlop;
- public static void init(Context context, SharedPreferences prefs) {
- sInstance.initInternal(context, prefs);
- sInstance.mListener = AccessibleInputMethodServiceProxy.getInstance();
+ public static void init(InputMethodService inputMethod) {
+ sInstance.initInternal(inputMethod);
}
public static AccessibleKeyboardViewProxy getInstance() {
return sInstance;
}
- public static void setView(KeyboardView view) {
- sInstance.mView = view;
- }
-
private AccessibleKeyboardViewProxy() {
// Not publicly instantiable.
}
- private void initInternal(Context context, SharedPreferences prefs) {
- final Paint paint = new Paint();
- paint.setTextAlign(Paint.Align.LEFT);
- paint.setTextSize(14.0f);
- paint.setAntiAlias(true);
- paint.setColor(Color.YELLOW);
-
- mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
+ private void initInternal(InputMethodService inputMethod) {
+ mInputMethod = inputMethod;
+ mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
}
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event,
- PointerTracker tracker) {
- if (mView == null) {
- Log.e(TAG, "No keyboard view set!");
- return false;
+ /**
+ * Sets the view wrapped by this proxy.
+ *
+ * @param view The view to wrap.
+ */
+ public void setView(LatinKeyboardView view) {
+ if (view == null) {
+ // Ignore null views.
+ return;
}
- switch (event.getEventType()) {
- case AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER:
- final Key key = tracker.getKey(mLastHoverKeyIndex);
+ mView = view;
- if (key == null)
- break;
+ // Ensure that the view has an accessibility delegate.
+ ViewCompat.setAccessibilityDelegate(view, this);
- final CharSequence description = KeyCodeDescriptionMapper.getInstance()
- .getDescriptionForKey(mView.getContext(), mView.getKeyboard(), key);
-
- if (description == null)
- return false;
-
- event.getText().add(description);
-
- break;
+ if (mAccessibilityNodeProvider != null) {
+ mAccessibilityNodeProvider.setView(view);
}
+ }
- return true;
+ public void setKeyboard(Keyboard keyboard) {
+ if (mAccessibilityNodeProvider != null) {
+ mAccessibilityNodeProvider.setKeyboard(keyboard);
+ }
}
/**
- * Receives hover events when accessibility is turned on in API > 11. In
- * earlier API levels, events are manually routed from onTouchEvent.
+ * Proxy method for View.getAccessibilityNodeProvider(). This method is
+ * called in SDK version 15 and higher to obtain the virtual node hierarchy
+ * provider.
*
- * @param event The hover event.
- * @return {@code true} if the event is handled
+ * @return The accessibility node provider for the current keyboard.
*/
- public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) {
- return onTouchExplorationEvent(event, tracker);
- }
-
- public boolean dispatchTouchEvent(MotionEvent event) {
- // Since touch exploration translates hover double-tap to a regular
- // single-tap, we're going to drop non-touch exploration events.
- if (!AccessibilityUtils.getInstance().isTouchExplorationEvent(event))
- return true;
-
- return false;
+ @Override
+ public AccessibilityEntityProvider getAccessibilityNodeProvider(View host) {
+ return getAccessibilityNodeProvider();
}
/**
- * Handles touch exploration events when Accessibility is turned on.
+ * Receives hover events when accessibility is turned on in SDK versions ICS
+ * and higher.
*
- * @param event The touch exploration hover event.
- * @return {@code true} if the event was handled
+ * @param event The hover event.
+ * @return {@code true} if the event is handled
*/
- private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) {
+ public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) {
final int x = (int) event.getX();
final int y = (int) event.getY();
+ final Key key = tracker.getKeyOn(x, y);
+ final Key previousKey = mLastHoverKey;
+
+ mLastHoverKey = key;
switch (event.getAction()) {
- case MotionEventCompatUtils.ACTION_HOVER_ENTER:
- case MotionEventCompatUtils.ACTION_HOVER_MOVE:
- final int keyIndex = tracker.getKeyIndexOn(x, y);
-
- if (keyIndex != mLastHoverKeyIndex) {
- fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
- mLastHoverKeyIndex = keyIndex;
- mLastX = x;
- mLastY = y;
- fireKeyHoverEvent(tracker, mLastHoverKeyIndex, true);
+ case MotionEvent.ACTION_HOVER_EXIT:
+ // Make sure we're not getting an EXIT event because the user slid
+ // off the keyboard area, then force a key press.
+ if (pointInView(x, y) && (key != null)) {
+ getAccessibilityNodeProvider().simulateKeyPress(key);
}
-
- return true;
- case MotionEventCompatUtils.ACTION_HOVER_EXIT:
- final int width = mView.getWidth();
- final int height = mView.getHeight();
-
- if (x < mScaledEdgeSlop || y < mScaledEdgeSlop || x >= (width - mScaledEdgeSlop)
- || y >= (height - mScaledEdgeSlop)) {
- fireKeyHoverEvent(tracker, mLastHoverKeyIndex, false);
- mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
- mLastX = -1;
- mLastY = -1;
- } else if (mLastHoverKeyIndex != KeyDetector.NOT_A_KEY) {
- fireKeyPressEvent(tracker, mLastX, mLastY, event.getEventTime());
+ //$FALL-THROUGH$
+ case MotionEvent.ACTION_HOVER_ENTER:
+ return onHoverKey(key, event);
+ case MotionEvent.ACTION_HOVER_MOVE:
+ if (key != previousKey) {
+ return onTransitionKey(key, previousKey, event);
+ } else {
+ return onHoverKey(key, event);
}
-
- return true;
}
return false;
}
- private void fireKeyHoverEvent(PointerTracker tracker, int keyIndex, boolean entering) {
- if (mListener == null) {
- Log.e(TAG, "No accessible keyboard action listener set!");
- return;
+ /**
+ * @return A lazily-instantiated node provider for this view proxy.
+ */
+ private AccessibilityEntityProvider getAccessibilityNodeProvider() {
+ // Instantiate the provide only when requested. Since the system
+ // will call this method multiple times it is a good practice to
+ // cache the provider instance.
+ if (mAccessibilityNodeProvider == null) {
+ mAccessibilityNodeProvider = new AccessibilityEntityProvider(mView, mInputMethod);
}
+ return mAccessibilityNodeProvider;
+ }
- if (mView == null) {
- Log.e(TAG, "No keyboard view set!");
- return;
+ /**
+ * Utility method to determine whether the given point, in local
+ * coordinates, is inside the view, where the area of the view is contracted
+ * by the edge slop factor.
+ *
+ * @param localX The local x-coordinate.
+ * @param localY The local y-coordinate.
+ */
+ private boolean pointInView(int localX, int localY) {
+ return (localX >= mEdgeSlop) && (localY >= mEdgeSlop)
+ && (localX < (mView.getWidth() - mEdgeSlop))
+ && (localY < (mView.getHeight() - mEdgeSlop));
+ }
+
+ /**
+ * Simulates a transition between two {@link Key}s by sending a HOVER_EXIT
+ * on the previous key, a HOVER_ENTER on the current key, and a HOVER_MOVE
+ * on the current key.
+ *
+ * @param currentKey The currently hovered key.
+ * @param previousKey The previously hovered key.
+ * @param event The event that triggered the transition.
+ * @return {@code true} if the event was handled.
+ */
+ private boolean onTransitionKey(Key currentKey, Key previousKey, MotionEvent event) {
+ final int savedAction = event.getAction();
+
+ event.setAction(MotionEvent.ACTION_HOVER_EXIT);
+ onHoverKey(previousKey, event);
+
+ event.setAction(MotionEvent.ACTION_HOVER_ENTER);
+ onHoverKey(currentKey, event);
+
+ event.setAction(MotionEvent.ACTION_HOVER_MOVE);
+ final boolean handled = onHoverKey(currentKey, event);
+
+ event.setAction(savedAction);
+
+ return handled;
+ }
+
+ /**
+ * Handles a hover event on a key. If {@link Key} extended View, this would
+ * be analogous to calling View.onHoverEvent(MotionEvent).
+ *
+ * @param key The currently hovered key.
+ * @param event The hover event.
+ * @return {@code true} if the event was handled.
+ */
+ private boolean onHoverKey(Key key, MotionEvent event) {
+ // Null keys can't receive events.
+ if (key == null) {
+ return false;
}
- if (keyIndex == KeyDetector.NOT_A_KEY)
- return;
+ final AccessibilityEntityProvider provider = getAccessibilityNodeProvider();
- final Key key = tracker.getKey(keyIndex);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ provider.sendAccessibilityEventForKey(
+ key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
+ provider.performActionForKey(
+ key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ provider.sendAccessibilityEventForKey(
+ key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
+ break;
+ }
- if (key == null)
- return;
+ return true;
+ }
- if (entering) {
- mListener.onHoverEnter(key.mCode);
- mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_ENTER);
- } else {
- mListener.onHoverExit(key.mCode);
- mView.sendAccessibilityEvent(AccessibilityEventCompatUtils.TYPE_VIEW_HOVER_EXIT);
+ /**
+ * Notifies the user of changes in the keyboard shift state.
+ */
+ public void notifyShiftState() {
+ final Keyboard keyboard = mView.getKeyboard();
+ final KeyboardId keyboardId = keyboard.mId;
+ final int elementId = keyboardId.mElementId;
+ final Context context = mView.getContext();
+ final CharSequence text;
+
+ switch (elementId) {
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ text = context.getText(R.string.spoken_description_shiftmode_locked);
+ break;
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
+ text = context.getText(R.string.spoken_description_shiftmode_on);
+ break;
+ default:
+ text = context.getText(R.string.spoken_description_shiftmode_off);
}
+
+ AccessibilityUtils.getInstance().speak(text);
}
- private void fireKeyPressEvent(PointerTracker tracker, int x, int y, long eventTime) {
- tracker.onDownEvent(x, y, eventTime, null);
- tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null);
+ /**
+ * Notifies the user of changes in the keyboard symbols state.
+ */
+ public void notifySymbolsState() {
+ final Keyboard keyboard = mView.getKeyboard();
+ final Context context = mView.getContext();
+ final KeyboardId keyboardId = keyboard.mId;
+ final int elementId = keyboardId.mElementId;
+ final int resId;
+
+ switch (elementId) {
+ case KeyboardId.ELEMENT_ALPHABET:
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ resId = R.string.spoken_description_mode_alpha;
+ break;
+ case KeyboardId.ELEMENT_SYMBOLS:
+ case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
+ resId = R.string.spoken_description_mode_symbol;
+ break;
+ case KeyboardId.ELEMENT_PHONE:
+ resId = R.string.spoken_description_mode_phone;
+ break;
+ case KeyboardId.ELEMENT_PHONE_SYMBOLS:
+ resId = R.string.spoken_description_mode_phone_shift;
+ break;
+ default:
+ resId = -1;
+ }
+
+ if (resId < 0) {
+ return;
+ }
+
+ final String text = context.getString(resId);
+ AccessibilityUtils.getInstance().speak(text);
}
}
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index a31911d60..23acb8b74 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -17,8 +17,9 @@
package com.android.inputmethod.accessibility;
import android.content.Context;
-import android.content.SharedPreferences;
import android.text.TextUtils;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
@@ -28,6 +29,11 @@ import com.android.inputmethod.latin.R;
import java.util.HashMap;
public class KeyCodeDescriptionMapper {
+ private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
+
+ // The resource ID of the string spoken for obscured keys
+ private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
+
private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
// Map of key labels to spoken description resource IDs
@@ -36,14 +42,8 @@ public class KeyCodeDescriptionMapper {
// Map of key codes to spoken description resource IDs
private final HashMap<Integer, Integer> mKeyCodeMap;
- // Map of shifted key codes to spoken description resource IDs
- private final HashMap<Integer, Integer> mShiftedKeyCodeMap;
-
- // Map of shift-locked key codes to spoken description resource IDs
- private final HashMap<Integer, Integer> mShiftLockedKeyCodeMap;
-
- public static void init(Context context, SharedPreferences prefs) {
- sInstance.initInternal(context, prefs);
+ public static void init() {
+ sInstance.initInternal();
}
public static KeyCodeDescriptionMapper getInstance() {
@@ -53,39 +53,15 @@ public class KeyCodeDescriptionMapper {
private KeyCodeDescriptionMapper() {
mKeyLabelMap = new HashMap<CharSequence, Integer>();
mKeyCodeMap = new HashMap<Integer, Integer>();
- mShiftedKeyCodeMap = new HashMap<Integer, Integer>();
- mShiftLockedKeyCodeMap = new HashMap<Integer, Integer>();
}
- private void initInternal(Context context, SharedPreferences prefs) {
+ private void initInternal() {
// Manual label substitutions for key labels with no string resource
mKeyLabelMap.put(":-)", R.string.spoken_description_smiley);
// Symbols that most TTS engines can't speak
- mKeyCodeMap.put((int) '.', R.string.spoken_description_period);
- mKeyCodeMap.put((int) ',', R.string.spoken_description_comma);
- mKeyCodeMap.put((int) '(', R.string.spoken_description_left_parenthesis);
- mKeyCodeMap.put((int) ')', R.string.spoken_description_right_parenthesis);
- mKeyCodeMap.put((int) ':', R.string.spoken_description_colon);
- mKeyCodeMap.put((int) ';', R.string.spoken_description_semicolon);
- mKeyCodeMap.put((int) '!', R.string.spoken_description_exclamation_mark);
- mKeyCodeMap.put((int) '?', R.string.spoken_description_question_mark);
- mKeyCodeMap.put((int) '\"', R.string.spoken_description_double_quote);
- mKeyCodeMap.put((int) '\'', R.string.spoken_description_single_quote);
- mKeyCodeMap.put((int) '*', R.string.spoken_description_star);
- mKeyCodeMap.put((int) '#', R.string.spoken_description_pound);
mKeyCodeMap.put((int) ' ', R.string.spoken_description_space);
- // Non-ASCII symbols (must use escape codes!)
- mKeyCodeMap.put((int) '\u2022', R.string.spoken_description_dot);
- mKeyCodeMap.put((int) '\u221A', R.string.spoken_description_square_root);
- mKeyCodeMap.put((int) '\u03C0', R.string.spoken_description_pi);
- mKeyCodeMap.put((int) '\u0394', R.string.spoken_description_delta);
- mKeyCodeMap.put((int) '\u2122', R.string.spoken_description_trademark);
- mKeyCodeMap.put((int) '\u2105', R.string.spoken_description_care_of);
- mKeyCodeMap.put((int) '\u2026', R.string.spoken_description_ellipsis);
- mKeyCodeMap.put((int) '\u201E', R.string.spoken_description_low_double_quote);
-
// Special non-character codes defined in Keyboard
mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete);
mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return);
@@ -94,12 +70,6 @@ public class KeyCodeDescriptionMapper {
mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic);
mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab);
-
- // Shifted versions of non-character codes defined in Keyboard
- mShiftedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_shift_shifted);
-
- // Shift-locked versions of non-character codes defined in Keyboard
- mShiftLockedKeyCodeMap.put(Keyboard.CODE_SHIFT, R.string.spoken_description_caps_lock);
}
/**
@@ -117,30 +87,40 @@ public class KeyCodeDescriptionMapper {
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key from which to obtain a description.
+ * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
* @return a character sequence describing the action performed by pressing
* the key
*/
- public CharSequence getDescriptionForKey(Context context, Keyboard keyboard, Key key) {
- if (key.mCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
- final CharSequence description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
+ public String getDescriptionForKey(Context context, Keyboard keyboard, Key key,
+ boolean shouldObscure) {
+ final int code = key.mCode;
+
+ if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
if (description != null)
return description;
}
+ if (code == Keyboard.CODE_SHIFT) {
+ return getDescriptionForShiftKey(context, keyboard);
+ }
+
+ if (code == Keyboard.CODE_ACTION_ENTER) {
+ return getDescriptionForActionKey(context, keyboard, key);
+ }
+
if (!TextUtils.isEmpty(key.mLabel)) {
final String label = key.mLabel.toString().trim();
+ // First, attempt to map the label to a pre-defined description.
if (mKeyLabelMap.containsKey(label)) {
return context.getString(mKeyLabelMap.get(label));
- } else if (label.length() == 1
- || (keyboard.isManualTemporaryUpperCase() && !TextUtils
- .isEmpty(key.mHintLabel))) {
- return getDescriptionForKeyCode(context, keyboard, key);
- } else {
- return label;
}
- } else if (key.mCode != Keyboard.CODE_DUMMY) {
- return getDescriptionForKeyCode(context, keyboard, key);
+ }
+
+ // Just attempt to speak the description.
+ if (key.mCode != Keyboard.CODE_UNSPECIFIED) {
+ return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
}
return null;
@@ -156,36 +136,110 @@ public class KeyCodeDescriptionMapper {
* @return a character sequence describing the action performed by pressing
* the key
*/
- private CharSequence getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) {
- final KeyboardId id = keyboard.mId;
-
- if (id.isAlphabetKeyboard()) {
- return context.getString(R.string.spoken_description_to_symbol);
- } else if (id.isSymbolsKeyboard()) {
- return context.getString(R.string.spoken_description_to_alpha);
- } else if (id.isPhoneSymbolsKeyboard()) {
- return context.getString(R.string.spoken_description_to_numeric);
- } else if (id.isPhoneKeyboard()) {
- return context.getString(R.string.spoken_description_to_symbol);
- } else {
+ private String getDescriptionForSwitchAlphaSymbol(Context context, Keyboard keyboard) {
+ final KeyboardId keyboardId = keyboard.mId;
+ final int elementId = keyboardId.mElementId;
+ final int resId;
+
+ switch (elementId) {
+ case KeyboardId.ELEMENT_ALPHABET:
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ resId = R.string.spoken_description_to_symbol;
+ break;
+ case KeyboardId.ELEMENT_SYMBOLS:
+ case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
+ resId = R.string.spoken_description_to_alpha;
+ break;
+ case KeyboardId.ELEMENT_PHONE:
+ resId = R.string.spoken_description_to_symbol;
+ break;
+ case KeyboardId.ELEMENT_PHONE_SYMBOLS:
+ resId = R.string.spoken_description_to_numeric;
+ break;
+ default:
+ Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
return null;
}
+
+ return context.getString(resId);
}
/**
- * Returns the keycode for the specified key given the current keyboard
- * state.
+ * Returns a context-sensitive description of the "Shift" key.
*
+ * @param context The package's context.
* @param keyboard The keyboard on which the key resides.
- * @param key The key from which to obtain a key code.
- * @return the key code for the specified key
+ * @return A context-sensitive description of the "Shift" key.
*/
- private int getCorrectKeyCode(Keyboard keyboard, Key key) {
- if (keyboard.isManualTemporaryUpperCase() && !TextUtils.isEmpty(key.mHintLabel)) {
- return key.mHintLabel.charAt(0);
- } else {
- return key.mCode;
+ private String getDescriptionForShiftKey(Context context, Keyboard keyboard) {
+ final KeyboardId keyboardId = keyboard.mId;
+ final int elementId = keyboardId.mElementId;
+ final int resId;
+
+ switch (elementId) {
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ resId = R.string.spoken_description_caps_lock;
+ break;
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
+ resId = R.string.spoken_description_shift_shifted;
+ break;
+ default:
+ resId = R.string.spoken_description_shift;
+ }
+
+ return context.getString(resId);
+ }
+
+ /**
+ * Returns a context-sensitive description of the "Enter" action key.
+ *
+ * @param context The package's context.
+ * @param keyboard The keyboard on which the key resides.
+ * @param key The key to describe.
+ * @return Returns a context-sensitive description of the "Enter" action
+ * key.
+ */
+ private String getDescriptionForActionKey(Context context, Keyboard keyboard, Key key) {
+ final KeyboardId keyboardId = keyboard.mId;
+ final int actionId = keyboardId.imeActionId();
+ final int resId;
+
+ // Always use the label, if available.
+ if (!TextUtils.isEmpty(key.mLabel)) {
+ return key.mLabel.toString().trim();
+ }
+
+ // Otherwise, use the action ID.
+ switch (actionId) {
+ case EditorInfo.IME_ACTION_SEARCH:
+ resId = R.string.spoken_description_search;
+ break;
+ case EditorInfo.IME_ACTION_GO:
+ resId = R.string.label_go_key;
+ break;
+ case EditorInfo.IME_ACTION_SEND:
+ resId = R.string.label_send_key;
+ break;
+ case EditorInfo.IME_ACTION_NEXT:
+ resId = R.string.label_next_key;
+ break;
+ case EditorInfo.IME_ACTION_DONE:
+ resId = R.string.label_done_key;
+ break;
+ case EditorInfo.IME_ACTION_PREVIOUS:
+ resId = R.string.label_previous_key;
+ break;
+ default:
+ resId = R.string.spoken_description_return;
}
+
+ return context.getString(resId);
}
/**
@@ -205,20 +259,26 @@ public class KeyCodeDescriptionMapper {
* @param context The package's context.
* @param keyboard The keyboard on which the key resides.
* @param key The key from which to obtain a description.
+ * @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
* @return a character sequence describing the action performed by pressing
* the key
*/
- private CharSequence getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key) {
- final int code = getCorrectKeyCode(keyboard, key);
-
- if (keyboard.isShiftLocked() && mShiftLockedKeyCodeMap.containsKey(code)) {
- return context.getString(mShiftLockedKeyCodeMap.get(code));
- } else if (keyboard.isShiftedOrShiftLocked() && mShiftedKeyCodeMap.containsKey(code)) {
- return context.getString(mShiftedKeyCodeMap.get(code));
- } else if (mKeyCodeMap.containsKey(code)) {
+ private String getDescriptionForKeyCode(Context context, Keyboard keyboard, Key key,
+ boolean shouldObscure) {
+ final int code = key.mCode;
+
+ // If the key description should be obscured, now is the time to do it.
+ final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code);
+ if (shouldObscure && isDefinedNonCtrl) {
+ return context.getString(OBSCURED_KEY_RES_ID);
+ }
+
+ if (mKeyCodeMap.containsKey(code)) {
return context.getString(mKeyCodeMap.get(code));
- } else if (Character.isDefined(code) && !Character.isISOControl(code)) {
+ } else if (isDefinedNonCtrl) {
return Character.toString((char) code);
+ } else if (!TextUtils.isEmpty(key.mLabel)) {
+ return key.mLabel;
} else {
return context.getString(R.string.spoken_description_unknown, code);
}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java b/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
deleted file mode 100644
index 50057727a..000000000
--- a/java/src/com/android/inputmethod/compat/AccessibilityEventCompatUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.view.accessibility.AccessibilityEvent;
-
-import java.lang.reflect.Field;
-
-public class AccessibilityEventCompatUtils {
- public static final int TYPE_VIEW_HOVER_ENTER = 0x80;
- public static final int TYPE_VIEW_HOVER_EXIT = 0x100;
-
- private static final Field FIELD_TYPE_VIEW_HOVER_ENTER = CompatUtils.getField(
- AccessibilityEvent.class, "TYPE_VIEW_HOVER_ENTER");
- private static final Field FIELD_TYPE_VIEW_HOVER_EXIT = CompatUtils.getField(
- AccessibilityEvent.class, "TYPE_VIEW_HOVER_EXIT");
- private static final Integer OBJ_TYPE_VIEW_HOVER_ENTER = (Integer) CompatUtils
- .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_ENTER);
- private static final Integer OBJ_TYPE_VIEW_HOVER_EXIT = (Integer) CompatUtils
- .getFieldValue(null, null, FIELD_TYPE_VIEW_HOVER_EXIT);
-
- public static boolean supportsTouchExploration() {
- return OBJ_TYPE_VIEW_HOVER_ENTER != null && OBJ_TYPE_VIEW_HOVER_EXIT != null;
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
deleted file mode 100644
index 4db1c7a24..000000000
--- a/java/src/com/android/inputmethod/compat/AccessibilityManagerCompatWrapper.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.view.accessibility.AccessibilityManager;
-
-import java.lang.reflect.Method;
-import java.util.Collections;
-import java.util.List;
-
-public class AccessibilityManagerCompatWrapper {
- private static final Method METHOD_getEnabledAccessibilityServiceList = CompatUtils.getMethod(
- AccessibilityManager.class, "getEnabledAccessibilityServiceList", int.class);
-
- private final AccessibilityManager mManager;
-
- public AccessibilityManagerCompatWrapper(AccessibilityManager manager) {
- mManager = manager;
- }
-
- @SuppressWarnings("unchecked")
- public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) {
- return (List<AccessibilityServiceInfo>) CompatUtils.invoke(mManager,
- Collections.<AccessibilityServiceInfo>emptyList(),
- METHOD_getEnabledAccessibilityServiceList, feedbackType);
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java b/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java
deleted file mode 100644
index f6afbcfe2..000000000
--- a/java/src/com/android/inputmethod/compat/ArraysCompatUtils.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import java.lang.reflect.Method;
-import java.util.Arrays;
-
-public class ArraysCompatUtils {
- private static final Method METHOD_Arrays_binarySearch = CompatUtils
- .getMethod(Arrays.class, "binarySearch", int[].class, int.class, int.class, int.class);
-
- public static int binarySearch(int[] array, int startIndex, int endIndex, int value) {
- if (METHOD_Arrays_binarySearch != null) {
- final Object index = CompatUtils.invoke(null, 0, METHOD_Arrays_binarySearch,
- array, startIndex, endIndex, value);
- return (Integer)index;
- } else {
- return compatBinarySearch(array, startIndex, endIndex, value);
- }
- }
-
- /* package */ static int compatBinarySearch(int[] array, int startIndex, int endIndex,
- int value) {
- if (startIndex > endIndex) throw new IllegalArgumentException();
- if (startIndex < 0 || endIndex > array.length) throw new ArrayIndexOutOfBoundsException();
-
- final int work[] = new int[endIndex - startIndex];
- System.arraycopy(array, startIndex, work, 0, work.length);
- final int index = Arrays.binarySearch(work, value);
- if (index >= 0) {
- return index + startIndex;
- } else {
- return ~(~index + startIndex);
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
new file mode 100644
index 000000000..b6c3e2a88
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2011 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.compat;
+
+import android.media.AudioManager;
+
+import java.lang.reflect.Method;
+
+public class AudioManagerCompatWrapper {
+ private static final Method METHOD_isWiredHeadsetOn = CompatUtils.getMethod(
+ AudioManager.class, "isWiredHeadsetOn");
+ private static final Method METHOD_isBluetoothA2dpOn = CompatUtils.getMethod(
+ AudioManager.class, "isBluetoothA2dpOn");
+
+ private final AudioManager mManager;
+
+ public AudioManagerCompatWrapper(AudioManager manager) {
+ mManager = manager;
+ }
+
+ /**
+ * Checks whether audio routing to the wired headset is on or off.
+ *
+ * @return true if audio is being routed to/from wired headset;
+ * false if otherwise
+ */
+ public boolean isWiredHeadsetOn() {
+ return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isWiredHeadsetOn);
+ }
+
+ /**
+ * Checks whether A2DP audio routing to the Bluetooth headset is on or off.
+ *
+ * @return true if A2DP audio is being routed to/from Bluetooth headset;
+ * false if otherwise
+ */
+ public boolean isBluetoothA2dpOn() {
+ return (Boolean) CompatUtils.invoke(mManager, false, METHOD_isBluetoothA2dpOn);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java
index b42633cd9..ce427e9c9 100644
--- a/java/src/com/android/inputmethod/compat/CompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/CompatUtils.java
@@ -23,8 +23,6 @@ import android.util.Log;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
public class CompatUtils {
private static final String TAG = CompatUtils.class.getSimpleName();
@@ -32,28 +30,17 @@ public class CompatUtils {
// TODO: Can these be constants instead of literal String constants?
private static final String INPUT_METHOD_SUBTYPE_SETTINGS =
"android.settings.INPUT_METHOD_SUBTYPE_SETTINGS";
- private static final String INPUT_LANGUAGE_SELECTION =
- "com.android.inputmethod.latin.INPUT_LANGUAGE_SELECTION";
public static Intent getInputLanguageSelectionIntent(String inputMethodId,
int flagsForSubtypeSettings) {
- final String action;
- Intent intent;
- if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED
- /* android.os.Build.VERSION_CODES.HONEYCOMB */
- && android.os.Build.VERSION.SDK_INT >= 11) {
- // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
- action = INPUT_METHOD_SUBTYPE_SETTINGS;
- intent = new Intent(action);
- if (!TextUtils.isEmpty(inputMethodId)) {
- intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
- }
- if (flagsForSubtypeSettings > 0) {
- intent.setFlags(flagsForSubtypeSettings);
- }
- } else {
- action = INPUT_LANGUAGE_SELECTION;
- intent = new Intent(action);
+ // Refer to android.provider.Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS
+ final String action = INPUT_METHOD_SUBTYPE_SETTINGS;
+ final Intent intent = new Intent(action);
+ if (!TextUtils.isEmpty(inputMethodId)) {
+ intent.putExtra(EXTRA_INPUT_METHOD_ID, inputMethodId);
+ }
+ if (flagsForSubtypeSettings > 0) {
+ intent.setFlags(flagsForSubtypeSettings);
}
return intent;
}
@@ -142,15 +129,4 @@ public class CompatUtils {
Log.e(TAG, "Exception in setFieldValue: " + e.getClass().getSimpleName());
}
}
-
- public static List<InputMethodSubtypeCompatWrapper> copyInputMethodSubtypeListToWrapper(
- Object listObject) {
- if (!(listObject instanceof List<?>)) return null;
- final List<InputMethodSubtypeCompatWrapper> subtypes =
- new ArrayList<InputMethodSubtypeCompatWrapper>();
- for (Object o: (List<?>)listObject) {
- subtypes.add(new InputMethodSubtypeCompatWrapper(o));
- }
- return subtypes;
- }
}
diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
index bcdcef7dc..08c246f8b 100644
--- a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
@@ -17,86 +17,65 @@
package com.android.inputmethod.compat;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
import java.lang.reflect.Field;
public class EditorInfoCompatUtils {
- private static final Field FIELD_IME_FLAG_NAVIGATE_NEXT = CompatUtils.getField(
- EditorInfo.class, "IME_FLAG_NAVIGATE_NEXT");
- private static final Field FIELD_IME_FLAG_NAVIGATE_PREVIOUS = CompatUtils.getField(
- EditorInfo.class, "IME_FLAG_NAVIGATE_PREVIOUS");
- private static final Field FIELD_IME_ACTION_PREVIOUS = CompatUtils.getField(
- EditorInfo.class, "IME_ACTION_PREVIOUS");
- private static final Integer OBJ_IME_FLAG_NAVIGATE_NEXT = (Integer) CompatUtils
- .getFieldValue(null, null, FIELD_IME_FLAG_NAVIGATE_NEXT);
- private static final Integer OBJ_IME_FLAG_NAVIGATE_PREVIOUS = (Integer) CompatUtils
- .getFieldValue(null, null, FIELD_IME_FLAG_NAVIGATE_PREVIOUS);
- private static final Integer OBJ_IME_ACTION_PREVIOUS = (Integer) CompatUtils
- .getFieldValue(null, null, FIELD_IME_ACTION_PREVIOUS);
+ // EditorInfo.IME_FLAG_FORCE_ASCII has been introduced since API#16 (JellyBean).
+ private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
+ EditorInfo.class, "IME_FLAG_FORCE_ASCII");
+ private static final Integer OBJ_IME_FLAG_FORCE_ASCII = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_IME_FLAG_FORCE_ASCII);
- public static boolean hasFlagNavigateNext(int imeOptions) {
- if (OBJ_IME_FLAG_NAVIGATE_NEXT == null)
- return false;
- return (imeOptions & OBJ_IME_FLAG_NAVIGATE_NEXT) != 0;
+ private EditorInfoCompatUtils() {
+ // This utility class is not publicly instantiable.
}
- public static boolean hasFlagNavigatePrevious(int imeOptions) {
- if (OBJ_IME_FLAG_NAVIGATE_PREVIOUS == null)
+ public static boolean hasFlagForceAscii(int imeOptions) {
+ if (OBJ_IME_FLAG_FORCE_ASCII == null)
return false;
- return (imeOptions & OBJ_IME_FLAG_NAVIGATE_PREVIOUS) != 0;
- }
-
- public static void performEditorActionNext(InputConnection ic) {
- ic.performEditorAction(EditorInfo.IME_ACTION_NEXT);
+ return (imeOptions & OBJ_IME_FLAG_FORCE_ASCII) != 0;
}
- public static void performEditorActionPrevious(InputConnection ic) {
- if (OBJ_IME_ACTION_PREVIOUS == null)
- return;
- ic.performEditorAction(OBJ_IME_ACTION_PREVIOUS);
- }
-
- public static String imeOptionsName(int imeOptions) {
- if (imeOptions == -1)
- return null;
+ public static String imeActionName(int imeOptions) {
final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION;
- final String action;
switch (actionId) {
- case EditorInfo.IME_ACTION_UNSPECIFIED:
- action = "actionUnspecified";
- break;
- case EditorInfo.IME_ACTION_NONE:
- action = "actionNone";
- break;
- case EditorInfo.IME_ACTION_GO:
- action = "actionGo";
- break;
- case EditorInfo.IME_ACTION_SEARCH:
- action = "actionSearch";
- break;
- case EditorInfo.IME_ACTION_SEND:
- action = "actionSend";
- break;
- case EditorInfo.IME_ACTION_NEXT:
- action = "actionNext";
- break;
- case EditorInfo.IME_ACTION_DONE:
- action = "actionDone";
- break;
- default: {
- if (OBJ_IME_ACTION_PREVIOUS != null && actionId == OBJ_IME_ACTION_PREVIOUS) {
- action = "actionPrevious";
- } else {
- action = "actionUnknown(" + actionId + ")";
- }
- break;
- }
+ case EditorInfo.IME_ACTION_UNSPECIFIED:
+ return "actionUnspecified";
+ case EditorInfo.IME_ACTION_NONE:
+ return "actionNone";
+ case EditorInfo.IME_ACTION_GO:
+ return "actionGo";
+ case EditorInfo.IME_ACTION_SEARCH:
+ return "actionSearch";
+ case EditorInfo.IME_ACTION_SEND:
+ return "actionSend";
+ case EditorInfo.IME_ACTION_NEXT:
+ return "actionNext";
+ case EditorInfo.IME_ACTION_DONE:
+ return "actionDone";
+ case EditorInfo.IME_ACTION_PREVIOUS:
+ return "actionPrevious";
+ default:
+ return "actionUnknown(" + actionId + ")";
}
+ }
+
+ public static String imeOptionsName(int imeOptions) {
+ final String action = imeActionName(imeOptions);
+ final StringBuilder flags = new StringBuilder();
if ((imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
- return "flagNoEnterAction|" + action;
- } else {
- return action;
+ flags.append("flagNoEnterAction|");
+ }
+ if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
+ flags.append("flagNavigateNext|");
+ }
+ if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0) {
+ flags.append("flagNavigatePrevious|");
+ }
+ if (hasFlagForceAscii(imeOptions)) {
+ flags.append("flagForceAscii|");
}
+ return (action != null) ? flags + action : flags.toString();
}
}
diff --git a/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java b/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java
deleted file mode 100644
index 7d00b6007..000000000
--- a/java/src/com/android/inputmethod/compat/InputConnectionCompatUtils.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import com.android.inputmethod.latin.EditingUtils.SelectedWord;
-
-import android.view.inputmethod.InputConnection;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-
-public class InputConnectionCompatUtils {
- private static final Class<?> CLASS_CorrectionInfo = CompatUtils
- .getClass("android.view.inputmethod.CorrectionInfo");
- private static final Class<?>[] INPUT_TYPE_CorrectionInfo = new Class<?>[] { int.class,
- CharSequence.class, CharSequence.class };
- private static final Constructor<?> CONSTRUCTOR_CorrectionInfo = CompatUtils
- .getConstructor(CLASS_CorrectionInfo, INPUT_TYPE_CorrectionInfo);
- private static final Method METHOD_InputConnection_commitCorrection = CompatUtils
- .getMethod(InputConnection.class, "commitCorrection", CLASS_CorrectionInfo);
- private static final Method METHOD_getSelectedText = CompatUtils
- .getMethod(InputConnection.class, "getSelectedText", int.class);
- private static final Method METHOD_setComposingRegion = CompatUtils
- .getMethod(InputConnection.class, "setComposingRegion", int.class, int.class);
- public static final boolean RECORRECTION_SUPPORTED;
-
- static {
- RECORRECTION_SUPPORTED = METHOD_getSelectedText != null
- && METHOD_setComposingRegion != null;
- }
-
- public static void commitCorrection(InputConnection ic, int offset, CharSequence oldText,
- CharSequence newText) {
- if (ic == null || CONSTRUCTOR_CorrectionInfo == null
- || METHOD_InputConnection_commitCorrection == null) {
- return;
- }
- Object[] args = { offset, oldText, newText };
- Object correctionInfo = CompatUtils.newInstance(CONSTRUCTOR_CorrectionInfo, args);
- if (correctionInfo != null) {
- CompatUtils.invoke(ic, null, METHOD_InputConnection_commitCorrection,
- correctionInfo);
- }
- }
-
-
- /**
- * Returns the selected text between the selStart and selEnd positions.
- */
- public static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
- // Use reflection, for backward compatibility
- return (CharSequence) CompatUtils.invoke(
- ic, null, METHOD_getSelectedText, 0);
- }
-
- /**
- * Tries to set the text into composition mode if there is support for it in the framework.
- */
- public static void underlineWord(InputConnection ic, SelectedWord word) {
- // Use reflection, for backward compatibility
- // If method not found, there's nothing we can do. It still works but just wont underline
- // the word.
- CompatUtils.invoke(
- ic, null, METHOD_setComposingRegion, word.mStart, word.mEnd);
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java
deleted file mode 100644
index 8e22bbc79..000000000
--- a/java/src/com/android/inputmethod/compat/InputMethodInfoCompatWrapper.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.content.pm.ServiceInfo;
-import android.view.inputmethod.InputMethodInfo;
-
-import java.lang.reflect.Method;
-
-public class InputMethodInfoCompatWrapper {
- private final InputMethodInfo mImi;
- private static final Method METHOD_getSubtypeAt = CompatUtils.getMethod(
- InputMethodInfo.class, "getSubtypeAt", int.class);
- private static final Method METHOD_getSubtypeCount = CompatUtils.getMethod(
- InputMethodInfo.class, "getSubtypeCount");
-
- public InputMethodInfoCompatWrapper(InputMethodInfo imi) {
- mImi = imi;
- }
-
- public InputMethodInfo getInputMethodInfo() {
- return mImi;
- }
-
- public String getId() {
- return mImi.getId();
- }
-
- public String getPackageName() {
- return mImi.getPackageName();
- }
-
- public ServiceInfo getServiceInfo() {
- return mImi.getServiceInfo();
- }
-
- public int getSubtypeCount() {
- return (Integer) CompatUtils.invoke(mImi, 0, METHOD_getSubtypeCount);
- }
-
- public InputMethodSubtypeCompatWrapper getSubtypeAt(int index) {
- return new InputMethodSubtypeCompatWrapper(CompatUtils.invoke(mImi, null,
- METHOD_getSubtypeAt, index));
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
index 1cc13f249..cc10a4ed2 100644
--- a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
+++ b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
@@ -16,212 +16,56 @@
package com.android.inputmethod.compat;
-import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.Utils;
-
import android.content.Context;
import android.os.IBinder;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.ImfUtils;
import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
// TODO: Override this class with the concrete implementation if we need to take care of the
// performance.
public class InputMethodManagerCompatWrapper {
private static final String TAG = InputMethodManagerCompatWrapper.class.getSimpleName();
- private static final Method METHOD_getCurrentInputMethodSubtype =
- CompatUtils.getMethod(InputMethodManager.class, "getCurrentInputMethodSubtype");
- private static final Method METHOD_getEnabledInputMethodSubtypeList =
- CompatUtils.getMethod(InputMethodManager.class, "getEnabledInputMethodSubtypeList",
- InputMethodInfo.class, boolean.class);
- private static final Method METHOD_getShortcutInputMethodsAndSubtypes =
- CompatUtils.getMethod(InputMethodManager.class, "getShortcutInputMethodsAndSubtypes");
- private static final Method METHOD_setInputMethodAndSubtype =
- CompatUtils.getMethod(
- InputMethodManager.class, "setInputMethodAndSubtype", IBinder.class,
- String.class, InputMethodSubtypeCompatWrapper.CLASS_InputMethodSubtype);
- private static final Method METHOD_switchToLastInputMethod = CompatUtils.getMethod(
- InputMethodManager.class, "switchToLastInputMethod", IBinder.class);
+ private static final Method METHOD_switchToNextInputMethod = CompatUtils.getMethod(
+ InputMethodManager.class, "switchToNextInputMethod", IBinder.class, Boolean.TYPE);
private static final InputMethodManagerCompatWrapper sInstance =
new InputMethodManagerCompatWrapper();
- public static final boolean SUBTYPE_SUPPORTED;
-
- static {
- // This static initializer guarantees that METHOD_getShortcutInputMethodsAndSubtypes is
- // already instantiated.
- SUBTYPE_SUPPORTED = METHOD_getShortcutInputMethodsAndSubtypes != null;
- }
-
- // For the compatibility, IMM will create dummy subtypes if subtypes are not found.
- // This is required to be false if the current behavior is broken. For now, it's ok to be true.
- public static final boolean FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES =
- !InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED;
- private static final String VOICE_MODE = "voice";
- private static final String KEYBOARD_MODE = "keyboard";
-
private InputMethodManager mImm;
- private LanguageSwitcherProxy mLanguageSwitcherProxy;
- private String mLatinImePackageName;
private InputMethodManagerCompatWrapper() {
+ // This wrapper class is not publicly instantiable.
}
- public static InputMethodManagerCompatWrapper getInstance(Context context) {
+ public static InputMethodManagerCompatWrapper getInstance() {
if (sInstance.mImm == null) {
- sInstance.init(context);
+ throw new RuntimeException(TAG + ".getInstance() is called before initialization");
}
return sInstance;
}
- private synchronized void init(Context context) {
- mImm = (InputMethodManager) context.getSystemService(
- Context.INPUT_METHOD_SERVICE);
- if (context instanceof LatinIME) {
- mLatinImePackageName = context.getPackageName();
- }
- mLanguageSwitcherProxy = LanguageSwitcherProxy.getInstance();
+ public static void init(Context context) {
+ sInstance.mImm = ImfUtils.getInputMethodManager(context);
}
- public InputMethodSubtypeCompatWrapper getCurrentInputMethodSubtype() {
- if (!SUBTYPE_SUPPORTED) {
- return new InputMethodSubtypeCompatWrapper(
- 0, 0, mLanguageSwitcherProxy.getInputLocale().toString(), KEYBOARD_MODE, "");
- }
- Object o = CompatUtils.invoke(mImm, null, METHOD_getCurrentInputMethodSubtype);
- return new InputMethodSubtypeCompatWrapper(o);
- }
-
- public List<InputMethodSubtypeCompatWrapper> getEnabledInputMethodSubtypeList(
- InputMethodInfoCompatWrapper imi, boolean allowsImplicitlySelectedSubtypes) {
- if (!SUBTYPE_SUPPORTED) {
- String[] languages = mLanguageSwitcherProxy.getEnabledLanguages(
- allowsImplicitlySelectedSubtypes);
- List<InputMethodSubtypeCompatWrapper> subtypeList =
- new ArrayList<InputMethodSubtypeCompatWrapper>();
- for (String lang: languages) {
- subtypeList.add(new InputMethodSubtypeCompatWrapper(0, 0, lang, KEYBOARD_MODE, ""));
- }
- return subtypeList;
- }
- Object retval = CompatUtils.invoke(mImm, null, METHOD_getEnabledInputMethodSubtypeList,
- (imi != null ? imi.getInputMethodInfo() : null), allowsImplicitlySelectedSubtypes);
- if (retval == null || !(retval instanceof List<?>) || ((List<?>)retval).isEmpty()) {
- if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
- // Returns an empty list
- return Collections.emptyList();
- }
- // Creates dummy subtypes
- @SuppressWarnings("unused")
- List<InputMethodSubtypeCompatWrapper> subtypeList =
- new ArrayList<InputMethodSubtypeCompatWrapper>();
- InputMethodSubtypeCompatWrapper keyboardSubtype = getLastResortSubtype(KEYBOARD_MODE);
- InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
- if (keyboardSubtype != null) {
- subtypeList.add(keyboardSubtype);
- }
- if (voiceSubtype != null) {
- subtypeList.add(voiceSubtype);
- }
- return subtypeList;
- }
- return CompatUtils.copyInputMethodSubtypeListToWrapper(retval);
- }
-
- private InputMethodInfoCompatWrapper getLatinImeInputMethodInfo() {
- if (TextUtils.isEmpty(mLatinImePackageName))
- return null;
- return Utils.getInputMethodInfo(this, mLatinImePackageName);
- }
-
- @SuppressWarnings("unused")
- private InputMethodSubtypeCompatWrapper getLastResortSubtype(String mode) {
- if (VOICE_MODE.equals(mode) && !FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES)
- return null;
- Locale inputLocale = SubtypeSwitcher.getInstance().getInputLocale();
- if (inputLocale == null)
- return null;
- return new InputMethodSubtypeCompatWrapper(0, 0, inputLocale.toString(), mode, "");
- }
-
- public Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
- getShortcutInputMethodsAndSubtypes() {
- Object retval = CompatUtils.invoke(mImm, null, METHOD_getShortcutInputMethodsAndSubtypes);
- if (retval == null || !(retval instanceof Map<?, ?>) || ((Map<?, ?>)retval).isEmpty()) {
- if (!FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
- // Returns an empty map
- return Collections.emptyMap();
- }
- // Creates dummy subtypes
- @SuppressWarnings("unused")
- InputMethodInfoCompatWrapper imi = getLatinImeInputMethodInfo();
- InputMethodSubtypeCompatWrapper voiceSubtype = getLastResortSubtype(VOICE_MODE);
- if (imi != null && voiceSubtype != null) {
- Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>
- shortcutMap =
- new HashMap<InputMethodInfoCompatWrapper,
- List<InputMethodSubtypeCompatWrapper>>();
- List<InputMethodSubtypeCompatWrapper> subtypeList =
- new ArrayList<InputMethodSubtypeCompatWrapper>();
- subtypeList.add(voiceSubtype);
- shortcutMap.put(imi, subtypeList);
- return shortcutMap;
- } else {
- return Collections.emptyMap();
- }
- }
- Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcutMap =
- new HashMap<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>>();
- final Map<?, ?> retvalMap = (Map<?, ?>)retval;
- for (Object key : retvalMap.keySet()) {
- if (!(key instanceof InputMethodInfo)) {
- Log.e(TAG, "Class type error.");
- return null;
- }
- shortcutMap.put(new InputMethodInfoCompatWrapper((InputMethodInfo)key),
- CompatUtils.copyInputMethodSubtypeListToWrapper(retvalMap.get(key)));
- }
- return shortcutMap;
- }
-
- public void setInputMethodAndSubtype(
- IBinder token, String id, InputMethodSubtypeCompatWrapper subtype) {
- if (subtype != null && subtype.hasOriginalObject()) {
- CompatUtils.invoke(mImm, null, METHOD_setInputMethodAndSubtype,
- token, id, subtype.getOriginalObject());
- }
+ public InputMethodSubtype getLastInputMethodSubtype() {
+ return mImm.getLastInputMethodSubtype();
}
public boolean switchToLastInputMethod(IBinder token) {
- if (SubtypeSwitcher.getInstance().isDummyVoiceMode()) {
- return true;
- }
- return (Boolean)CompatUtils.invoke(mImm, false, METHOD_switchToLastInputMethod, token);
+ return mImm.switchToLastInputMethod(token);
}
- public List<InputMethodInfoCompatWrapper> getEnabledInputMethodList() {
- if (mImm == null) return null;
- List<InputMethodInfoCompatWrapper> imis = new ArrayList<InputMethodInfoCompatWrapper>();
- for (InputMethodInfo imi : mImm.getEnabledInputMethodList()) {
- imis.add(new InputMethodInfoCompatWrapper(imi));
- }
- return imis;
+ public boolean switchToNextInputMethod(IBinder token, boolean onlyCurrentIme) {
+ return (Boolean)CompatUtils.invoke(mImm, false, METHOD_switchToNextInputMethod, token,
+ onlyCurrentIme);
}
public void showInputMethodPicker() {
- if (mImm == null) return;
mImm.showInputMethodPicker();
}
}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java
deleted file mode 100644
index ec7250c2a..000000000
--- a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatWrapper.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.inputmethodservice.InputMethodService;
-// import android.view.inputmethod.InputMethodSubtype;
-
-import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-
-public class InputMethodServiceCompatWrapper extends InputMethodService {
- // CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED needs to be false if the API level is 10
- // or previous. Note that InputMethodSubtype was added in the API level 11.
- // For the API level 11 or later, LatinIME should override onCurrentInputMethodSubtypeChanged().
- // For the API level 10 or previous, we handle the "subtype changed" events by ourselves
- // without having support from framework -- onCurrentInputMethodSubtypeChanged().
- public static final boolean CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED = false;
-
- private InputMethodManagerCompatWrapper mImm;
-
- @Override
- public void onCreate() {
- super.onCreate();
- mImm = InputMethodManagerCompatWrapper.getInstance(this);
- }
-
- // When the API level is 10 or previous, notifyOnCurrentInputMethodSubtypeChanged should
- // handle the event the current subtype was changed. LatinIME calls
- // notifyOnCurrentInputMethodSubtypeChanged every time LatinIME
- // changes the current subtype.
- // This call is required to let LatinIME itself know a subtype changed
- // event when the API level is 10 or previous.
- @SuppressWarnings("unused")
- public void notifyOnCurrentInputMethodSubtypeChanged(InputMethodSubtypeCompatWrapper subtype) {
- // Do nothing when the API level is 11 or later
- // and FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES is not true
- if (CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED && !InputMethodManagerCompatWrapper.
- FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES) {
- return;
- }
- if (subtype == null) {
- subtype = mImm.getCurrentInputMethodSubtype();
- }
- if (subtype != null) {
- if (!InputMethodManagerCompatWrapper.FORCE_ENABLE_VOICE_EVEN_WITH_NO_VOICE_SUBTYPES
- && !subtype.isDummy()) return;
- if (!InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) {
- LanguageSwitcherProxy.getInstance().setLocale(subtype.getLocale());
- }
- SubtypeSwitcher.getInstance().updateSubtype(subtype);
- }
- }
-
- //////////////////////////////////////
- // Functions using API v11 or later //
- //////////////////////////////////////
- /*@Override
- public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
- // Do nothing when the API level is 10 or previous
- if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) return;
- SubtypeSwitcher.getInstance().updateSubtype(
- new InputMethodSubtypeCompatWrapper(subtype));
- }*/
-
- protected static void setTouchableRegionCompat(InputMethodService.Insets outInsets,
- int x, int y, int width, int height) {
- //outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
- //outInsets.touchableRegion.set(x, y, width, height);
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java
deleted file mode 100644
index 667d86c42..000000000
--- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatWrapper.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import com.android.inputmethod.latin.LatinImeLogger;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.lang.reflect.Method;
-import java.util.Arrays;
-
-// TODO: Override this class with the concrete implementation if we need to take care of the
-// performance.
-public final class InputMethodSubtypeCompatWrapper extends AbstractCompatWrapper {
- private static final boolean DBG = LatinImeLogger.sDBG;
- private static final String TAG = InputMethodSubtypeCompatWrapper.class.getSimpleName();
- private static final String DEFAULT_LOCALE = "en_US";
- private static final String DEFAULT_MODE = "keyboard";
-
- public static final Class<?> CLASS_InputMethodSubtype =
- CompatUtils.getClass("android.view.inputmethod.InputMethodSubtype");
- private static final Method METHOD_getNameResId =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getNameResId");
- private static final Method METHOD_getIconResId =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getIconResId");
- private static final Method METHOD_getLocale =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getLocale");
- private static final Method METHOD_getMode =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getMode");
- private static final Method METHOD_getExtraValue =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getExtraValue");
- private static final Method METHOD_containsExtraValueKey =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "containsExtraValueKey", String.class);
- private static final Method METHOD_getExtraValueOf =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "getExtraValueOf", String.class);
- private static final Method METHOD_isAuxiliary =
- CompatUtils.getMethod(CLASS_InputMethodSubtype, "isAuxiliary");
-
- private final int mDummyNameResId;
- private final int mDummyIconResId;
- private final String mDummyLocale;
- private final String mDummyMode;
- private final String mDummyExtraValues;
-
- public InputMethodSubtypeCompatWrapper(Object subtype) {
- super((CLASS_InputMethodSubtype != null && CLASS_InputMethodSubtype.isInstance(subtype))
- ? subtype : null);
- mDummyNameResId = 0;
- mDummyIconResId = 0;
- mDummyLocale = DEFAULT_LOCALE;
- mDummyMode = DEFAULT_MODE;
- mDummyExtraValues = "";
- }
-
- // Constructor for creating a dummy subtype.
- public InputMethodSubtypeCompatWrapper(int nameResId, int iconResId, String locale,
- String mode, String extraValues) {
- super(null);
- if (DBG) {
- Log.d(TAG, "CreateInputMethodSubtypeCompatWrapper");
- }
- mDummyNameResId = nameResId;
- mDummyIconResId = iconResId;
- mDummyLocale = locale != null ? locale : "";
- mDummyMode = mode != null ? mode : "";
- mDummyExtraValues = extraValues != null ? extraValues : "";
- }
-
- public int getNameResId() {
- if (mObj == null) return mDummyNameResId;
- return (Integer)CompatUtils.invoke(mObj, 0, METHOD_getNameResId);
- }
-
- public int getIconResId() {
- if (mObj == null) return mDummyIconResId;
- return (Integer)CompatUtils.invoke(mObj, 0, METHOD_getIconResId);
- }
-
- public String getLocale() {
- if (mObj == null) return mDummyLocale;
- final String s = (String)CompatUtils.invoke(mObj, null, METHOD_getLocale);
- if (TextUtils.isEmpty(s)) return DEFAULT_LOCALE;
- return s;
- }
-
- public String getMode() {
- if (mObj == null) return mDummyMode;
- String s = (String)CompatUtils.invoke(mObj, null, METHOD_getMode);
- if (TextUtils.isEmpty(s)) return DEFAULT_MODE;
- return s;
- }
-
- public String getExtraValue() {
- if (mObj == null) return mDummyExtraValues;
- return (String)CompatUtils.invoke(mObj, null, METHOD_getExtraValue);
- }
-
- public boolean containsExtraValueKey(String key) {
- return (Boolean)CompatUtils.invoke(mObj, false, METHOD_containsExtraValueKey, key);
- }
-
- public String getExtraValueOf(String key) {
- return (String)CompatUtils.invoke(mObj, null, METHOD_getExtraValueOf, key);
- }
-
- public boolean isAuxiliary() {
- return (Boolean)CompatUtils.invoke(mObj, false, METHOD_isAuxiliary);
- }
-
- public boolean isDummy() {
- return !hasOriginalObject();
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof InputMethodSubtypeCompatWrapper) {
- InputMethodSubtypeCompatWrapper subtype = (InputMethodSubtypeCompatWrapper)o;
- if (mObj == null) {
- // easy check of dummy subtypes
- return (mDummyNameResId == subtype.mDummyNameResId
- && mDummyIconResId == subtype.mDummyIconResId
- && mDummyLocale.equals(subtype.mDummyLocale)
- && mDummyMode.equals(subtype.mDummyMode)
- && mDummyExtraValues.equals(subtype.mDummyExtraValues));
- }
- return mObj.equals(subtype.getOriginalObject());
- } else {
- return mObj.equals(o);
- }
- }
-
- @Override
- public int hashCode() {
- if (mObj == null) {
- return hashCodeInternal(mDummyNameResId, mDummyIconResId, mDummyLocale,
- mDummyMode, mDummyExtraValues);
- }
- return mObj.hashCode();
- }
-
- private static int hashCodeInternal(int nameResId, int iconResId, String locale,
- String mode, String extraValue) {
- return Arrays
- .hashCode(new Object[] { nameResId, iconResId, locale, mode, extraValue });
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java
deleted file mode 100644
index 6c2f0f799..000000000
--- a/java/src/com/android/inputmethod/compat/InputTypeCompatUtils.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.text.InputType;
-
-import java.lang.reflect.Field;
-
-public class InputTypeCompatUtils {
- private static final Field FIELD_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS =
- CompatUtils.getField(InputType.class, "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS");
- private static final Field FIELD_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD = CompatUtils
- .getField(InputType.class, "TYPE_TEXT_VARIATION_WEB_PASSWORD");
- private static final Field FIELD_InputType_TYPE_NUMBER_VARIATION_PASSWORD = CompatUtils
- .getField(InputType.class, "TYPE_NUMBER_VARIATION_PASSWORD");
- private static final Integer OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS =
- (Integer) CompatUtils.getFieldValue(null, null,
- FIELD_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
- private static final Integer OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD =
- (Integer) CompatUtils.getFieldValue(null, null,
- FIELD_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD);
- private static final Integer OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD =
- (Integer) CompatUtils.getFieldValue(null, null,
- FIELD_InputType_TYPE_NUMBER_VARIATION_PASSWORD);
- private static final int WEB_TEXT_PASSWORD_INPUT_TYPE;
- private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
- private static final int NUMBER_PASSWORD_INPUT_TYPE;
- private static final int TEXT_PASSWORD_INPUT_TYPE =
- InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
- private static final int TEXT_VISIBLE_PASSWORD_INPUT_TYPE =
- InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
-
- static {
- WEB_TEXT_PASSWORD_INPUT_TYPE =
- OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD != null
- ? InputType.TYPE_CLASS_TEXT | OBJ_InputType_TYPE_TEXT_VARIATION_WEB_PASSWORD
- : 0;
- WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
- OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != null
- ? InputType.TYPE_CLASS_TEXT
- | OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
- : 0;
- NUMBER_PASSWORD_INPUT_TYPE =
- OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD != null
- ? InputType.TYPE_CLASS_NUMBER | OBJ_InputType_TYPE_NUMBER_VARIATION_PASSWORD
- : 0;
- }
-
- private static boolean isWebEditTextInputType(int inputType) {
- return inputType == (InputType.TYPE_CLASS_TEXT
- | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
- }
-
- private static boolean isWebPasswordInputType(int inputType) {
- return WEB_TEXT_PASSWORD_INPUT_TYPE != 0
- && inputType == WEB_TEXT_PASSWORD_INPUT_TYPE;
- }
-
- private static boolean isWebEmailAddressInputType(int inputType) {
- return WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE != 0
- && inputType == WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
- }
-
- private static boolean isNumberPasswordInputType(int inputType) {
- return NUMBER_PASSWORD_INPUT_TYPE != 0
- && inputType == NUMBER_PASSWORD_INPUT_TYPE;
- }
-
- private static boolean isTextPasswordInputType(int inputType) {
- return inputType == TEXT_PASSWORD_INPUT_TYPE;
- }
-
- private static boolean isWebEmailAddressVariation(int variation) {
- return OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != null
- && variation == OBJ_InputType_TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
- }
-
- public static boolean isEmailVariation(int variation) {
- return variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
- || isWebEmailAddressVariation(variation);
- }
-
- public static boolean isWebInputType(int inputType) {
- final int maskedInputType =
- inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
- return isWebEditTextInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
- || isWebEmailAddressInputType(maskedInputType);
- }
-
- // Please refer to TextView.isPasswordInputType
- public static boolean isPasswordInputType(int inputType) {
- final int maskedInputType =
- inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
- return isTextPasswordInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
- || isNumberPasswordInputType(maskedInputType);
- }
-
- // Please refer to TextView.isVisiblePasswordInputType
- public static boolean isVisiblePasswordInputType(int inputType) {
- final int maskedInputType =
- inputType & (InputType.TYPE_MASK_CLASS | InputType.TYPE_MASK_VARIATION);
- return maskedInputType == TEXT_VISIBLE_PASSWORD_INPUT_TYPE;
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java b/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java
deleted file mode 100644
index 674cbe74b..000000000
--- a/java/src/com/android/inputmethod/compat/LinearLayoutCompatUtils.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2011 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.compat;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.Log;
-
-import java.lang.reflect.Field;
-
-public class LinearLayoutCompatUtils {
- private static final String TAG = LinearLayoutCompatUtils.class.getSimpleName();
-
- private static final Class<?> CLASS_R_STYLEABLE = CompatUtils.getClass(
- "com.android.internal.R$styleable");
- private static final Field STYLEABLE_VIEW = CompatUtils.getField(
- CLASS_R_STYLEABLE, "View");
- private static final Field STYLEABLE_VIEW_BACKGROUND = CompatUtils.getField(
- CLASS_R_STYLEABLE, "View_background");
- private static final Object VALUE_STYLEABLE_VIEW = CompatUtils.getFieldValue(
- null, null, STYLEABLE_VIEW);
- private static final Integer VALUE_STYLEABLE_VIEW_BACKGROUND =
- (Integer)CompatUtils.getFieldValue(null, null, STYLEABLE_VIEW_BACKGROUND);
-
- public static Drawable getBackgroundDrawable(Context context, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- if (!(VALUE_STYLEABLE_VIEW instanceof int[]) || VALUE_STYLEABLE_VIEW_BACKGROUND == null) {
- Log.w(TAG, "Can't get View background attribute using reflection");
- return null;
- }
-
- final int[] styleableView = (int[])VALUE_STYLEABLE_VIEW;
- final TypedArray a = context.obtainStyledAttributes(
- attrs, styleableView, defStyleAttr, defStyleRes);
- final Drawable background = a.getDrawable(VALUE_STYLEABLE_VIEW_BACKGROUND);
- a.recycle();
- return background;
- }
-}
diff --git a/java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
index 65949357f..1b79992f0 100644
--- a/java/src/com/android/inputmethod/compat/AbstractCompatWrapper.java
+++ b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
@@ -16,24 +16,19 @@
package com.android.inputmethod.compat;
-import android.util.Log;
+import java.lang.reflect.Field;
-public abstract class AbstractCompatWrapper {
- private static final String TAG = AbstractCompatWrapper.class.getSimpleName();
- protected final Object mObj;
+public class SettingsSecureCompatUtils {
+ private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField(
+ android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD");
- public AbstractCompatWrapper(Object obj) {
- if (obj == null) {
- Log.e(TAG, "Invalid input to AbstructCompatWrapper");
- }
- mObj = obj;
+ private SettingsSecureCompatUtils() {
+ // This class is non-instantiable.
}
- public Object getOriginalObject() {
- return mObj;
- }
-
- public boolean hasOriginalObject() {
- return mObj != null;
- }
+ /**
+ * Whether to speak passwords while in accessibility mode.
+ */
+ public static final String ACCESSIBILITY_SPEAK_PASSWORD = (String) CompatUtils.getFieldValue(
+ null, null, FIELD_ACCESSIBILITY_SPEAK_PASSWORD);
}
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index 4929dd948..a0f48d24c 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.compat;
+import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
@@ -24,19 +25,21 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
+import android.util.Log;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Locale;
public class SuggestionSpanUtils {
+ private static final String TAG = SuggestionSpanUtils.class.getSimpleName();
// TODO: Use reflection to get field values
public static final String ACTION_SUGGESTION_PICKED =
"android.text.style.SUGGESTION_PICKED";
public static final String SUGGESTION_SPAN_PICKED_AFTER = "after";
public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
- public static final int SUGGESTION_MAX_SIZE = 5;
public static final boolean SUGGESTION_SPAN_IS_SUPPORTED;
private static final Class<?> CLASS_SuggestionSpan = CompatUtils
@@ -45,15 +48,68 @@ public class SuggestionSpanUtils {
Context.class, Locale.class, String[].class, int.class, Class.class };
private static final Constructor<?> CONSTRUCTOR_SuggestionSpan = CompatUtils
.getConstructor(CLASS_SuggestionSpan, INPUT_TYPE_SuggestionSpan);
+ public static final Field FIELD_FLAG_EASY_CORRECT =
+ CompatUtils.getField(CLASS_SuggestionSpan, "FLAG_EASY_CORRECT");
+ public static final Field FIELD_FLAG_MISSPELLED =
+ CompatUtils.getField(CLASS_SuggestionSpan, "FLAG_MISSPELLED");
+ public static final Field FIELD_FLAG_AUTO_CORRECTION =
+ CompatUtils.getField(CLASS_SuggestionSpan, "FLAG_AUTO_CORRECTION");
+ public static final Field FIELD_SUGGESTIONS_MAX_SIZE
+ = CompatUtils.getField(CLASS_SuggestionSpan, "SUGGESTIONS_MAX_SIZE");
+ public static final Integer OBJ_FLAG_EASY_CORRECT = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_FLAG_EASY_CORRECT);
+ public static final Integer OBJ_FLAG_MISSPELLED = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_FLAG_MISSPELLED);
+ public static final Integer OBJ_FLAG_AUTO_CORRECTION = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_FLAG_AUTO_CORRECTION);
+ public static final Integer OBJ_SUGGESTIONS_MAX_SIZE = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_SUGGESTIONS_MAX_SIZE);
+
static {
SUGGESTION_SPAN_IS_SUPPORTED =
CLASS_SuggestionSpan != null && CONSTRUCTOR_SuggestionSpan != null;
+ if (LatinImeLogger.sDBG) {
+ if (SUGGESTION_SPAN_IS_SUPPORTED
+ && (OBJ_FLAG_AUTO_CORRECTION == null || OBJ_SUGGESTIONS_MAX_SIZE == null
+ || OBJ_FLAG_MISSPELLED == null || OBJ_FLAG_EASY_CORRECT == null)) {
+ throw new RuntimeException("Field is accidentially null.");
+ }
+ }
+ }
+
+ private SuggestionSpanUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static CharSequence getTextWithAutoCorrectionIndicatorUnderline(
+ Context context, CharSequence text) {
+ if (TextUtils.isEmpty(text) || CONSTRUCTOR_SuggestionSpan == null
+ || OBJ_FLAG_AUTO_CORRECTION == null || OBJ_SUGGESTIONS_MAX_SIZE == null
+ || OBJ_FLAG_MISSPELLED == null || OBJ_FLAG_EASY_CORRECT == null) {
+ return text;
+ }
+ final Spannable spannable = text instanceof Spannable
+ ? (Spannable) text : new SpannableString(text);
+ final Object[] args =
+ { context, null, new String[] {}, (int)OBJ_FLAG_AUTO_CORRECTION,
+ (Class<?>) SuggestionSpanPickedNotificationReceiver.class };
+ final Object ss = CompatUtils.newInstance(CONSTRUCTOR_SuggestionSpan, args);
+ if (ss == null) {
+ Log.w(TAG, "Suggestion span was not created.");
+ return text;
+ }
+ spannable.setSpan(ss, 0, text.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
+ return spannable;
}
public static CharSequence getTextWithSuggestionSpan(Context context,
- CharSequence pickedWord, SuggestedWords suggestedWords) {
- if (TextUtils.isEmpty(pickedWord) || CONSTRUCTOR_SuggestionSpan == null
- || suggestedWords == null || suggestedWords.size() == 0) {
+ CharSequence pickedWord, SuggestedWords suggestedWords, boolean dictionaryAvailable) {
+ if (!dictionaryAvailable || TextUtils.isEmpty(pickedWord)
+ || CONSTRUCTOR_SuggestionSpan == null
+ || suggestedWords == null || suggestedWords.size() == 0
+ || suggestedWords.mIsPrediction || suggestedWords.mIsPunctuationSuggestions
+ || OBJ_SUGGESTIONS_MAX_SIZE == null) {
return pickedWord;
}
@@ -64,16 +120,21 @@ public class SuggestionSpanUtils {
spannable = new SpannableString(pickedWord);
}
final ArrayList<String> suggestionsList = new ArrayList<String>();
+ boolean sameAsTyped = false;
for (int i = 0; i < suggestedWords.size(); ++i) {
- if (suggestionsList.size() >= SUGGESTION_MAX_SIZE) {
+ if (suggestionsList.size() >= OBJ_SUGGESTIONS_MAX_SIZE) {
break;
}
final CharSequence word = suggestedWords.getWord(i);
if (!TextUtils.equals(pickedWord, word)) {
suggestionsList.add(word.toString());
+ } else if (i == 0) {
+ sameAsTyped = true;
}
}
+ // TODO: We should avoid adding suggestion span candidates that came from the bigram
+ // prediction.
final Object[] args =
{ context, null, suggestionsList.toArray(new String[suggestionsList.size()]), 0,
(Class<?>) SuggestionSpanPickedNotificationReceiver.class };
diff --git a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
new file mode 100644
index 000000000..e5f9db27c
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 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.compat;
+
+import android.view.textservice.SuggestionsInfo;
+
+import java.lang.reflect.Field;
+
+public class SuggestionsInfoCompatUtils {
+ private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = CompatUtils.getField(
+ SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS");
+ private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = (Integer) CompatUtils
+ .getFieldValue(null, null, FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS);
+ private static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS =
+ OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS != null
+ ? OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS : 0;
+
+ private SuggestionsInfoCompatUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /**
+ * Returns the flag value of the attributes of the suggestions that can be obtained by
+ * {@link SuggestionsInfo#getSuggestionsAttributes()}: this tells that the text service thinks
+ * the result suggestions include highly recommended ones.
+ */
+ public static int getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() {
+ return RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS;
+ }
+}
diff --git a/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java b/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java
deleted file mode 100644
index 290e6b554..000000000
--- a/java/src/com/android/inputmethod/deprecated/LanguageSwitcherProxy.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2011 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.deprecated;
-
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.deprecated.languageswitcher.LanguageSwitcher;
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.Settings;
-
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-
-import java.util.Locale;
-
-// This class is used only when the IME doesn't use method.xml for language switching.
-public class LanguageSwitcherProxy implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final LanguageSwitcherProxy sInstance = new LanguageSwitcherProxy();
- private LatinIME mService;
- private LanguageSwitcher mLanguageSwitcher;
- private SharedPreferences mPrefs;
-
- public static LanguageSwitcherProxy getInstance() {
- if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return null;
- return sInstance;
- }
-
- public static void init(LatinIME service, SharedPreferences prefs) {
- if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
- final Configuration conf = service.getResources().getConfiguration();
- sInstance.mLanguageSwitcher = new LanguageSwitcher(service);
- sInstance.mLanguageSwitcher.loadLocales(prefs, conf.locale);
- sInstance.mPrefs = prefs;
- sInstance.mService = service;
- prefs.registerOnSharedPreferenceChangeListener(sInstance);
- }
-
- public static void onConfigurationChanged(Configuration conf) {
- if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
- sInstance.mLanguageSwitcher.onConfigurationChanged(conf, sInstance.mPrefs);
- }
-
- public static void loadSettings() {
- if (InputMethodManagerCompatWrapper.SUBTYPE_SUPPORTED) return;
- sInstance.mLanguageSwitcher.loadLocales(sInstance.mPrefs, null);
- }
-
- public int getLocaleCount() {
- return mLanguageSwitcher.getLocaleCount();
- }
-
- public String[] getEnabledLanguages(boolean allowImplicitlySelectedLanguages) {
- return mLanguageSwitcher.getEnabledLanguages(allowImplicitlySelectedLanguages);
- }
-
- public Locale getInputLocale() {
- return mLanguageSwitcher.getInputLocale();
- }
-
- public void setLocale(String localeStr) {
- mLanguageSwitcher.setLocale(localeStr);
- mLanguageSwitcher.persist(mPrefs);
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- // PREF_SELECTED_LANGUAGES: enabled input subtypes
- // PREF_INPUT_LANGUAGE: current input subtype
- if (key.equals(Settings.PREF_SELECTED_LANGUAGES)
- || key.equals(Settings.PREF_INPUT_LANGUAGE)) {
- mLanguageSwitcher.loadLocales(prefs, null);
- if (mService != null) {
- mService.onRefreshKeyboard();
- }
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/VoiceProxy.java b/java/src/com/android/inputmethod/deprecated/VoiceProxy.java
deleted file mode 100644
index 85993ea4d..000000000
--- a/java/src/com/android/inputmethod/deprecated/VoiceProxy.java
+++ /dev/null
@@ -1,842 +0,0 @@
-/*
- * Copyright (C) 2010 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.deprecated;
-
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.deprecated.voice.FieldContext;
-import com.android.inputmethod.deprecated.voice.Hints;
-import com.android.inputmethod.deprecated.voice.SettingsUtil;
-import com.android.inputmethod.deprecated.voice.VoiceInput;
-import com.android.inputmethod.deprecated.voice.VoiceInputLogger;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.latin.EditingUtils;
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.LatinIME.UIHandler;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SharedPreferencesCompat;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.Utils;
-
-import android.app.AlertDialog;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.provider.Browser;
-import android.speech.SpeechRecognizer;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.text.style.URLSpan;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.Window;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class VoiceProxy implements VoiceInput.UiListener {
- private static final VoiceProxy sInstance = new VoiceProxy();
-
- public static final boolean VOICE_INSTALLED = true;
- private static final boolean ENABLE_VOICE_BUTTON = true;
- private static final String PREF_VOICE_MODE = "voice_mode";
- // Whether or not the user has used voice input before (and thus, whether to show the
- // first-run warning dialog or not).
- private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
- // Whether or not the user has used voice input from an unsupported locale UI before.
- // For example, the user has a Chinese UI but activates voice input.
- private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
- "has_used_voice_input_unsupported_locale";
- private static final int RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO = 6;
- // TODO: Adjusted on phones for now
- private static final int RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP = 244;
-
- private static final String TAG = VoiceProxy.class.getSimpleName();
- private static final boolean DEBUG = LatinImeLogger.sDBG;
-
- private boolean mAfterVoiceInput;
- private boolean mHasUsedVoiceInput;
- private boolean mHasUsedVoiceInputUnsupportedLocale;
- private boolean mImmediatelyAfterVoiceInput;
- private boolean mIsShowingHint;
- private boolean mLocaleSupportedForVoiceInput;
- private boolean mPasswordText;
- private boolean mRecognizing;
- private boolean mShowingVoiceSuggestions;
- private boolean mVoiceButtonEnabled;
- private boolean mVoiceButtonOnPrimary;
- private boolean mVoiceInputHighlighted;
-
- private int mMinimumVoiceRecognitionViewHeightPixel;
- private InputMethodManagerCompatWrapper mImm;
- private LatinIME mService;
- private AlertDialog mVoiceWarningDialog;
- private VoiceInput mVoiceInput;
- private final VoiceResults mVoiceResults = new VoiceResults();
- private Hints mHints;
- private UIHandler mHandler;
- private SubtypeSwitcher mSubtypeSwitcher;
-
- // For each word, a list of potential replacements, usually from voice.
- private final Map<String, List<CharSequence>> mWordToSuggestions =
- new HashMap<String, List<CharSequence>>();
-
- public static VoiceProxy init(LatinIME context, SharedPreferences prefs, UIHandler h) {
- sInstance.initInternal(context, prefs, h);
- return sInstance;
- }
-
- public static VoiceProxy getInstance() {
- return sInstance;
- }
-
- private void initInternal(LatinIME service, SharedPreferences prefs, UIHandler h) {
- mService = service;
- mHandler = h;
- mMinimumVoiceRecognitionViewHeightPixel = Utils.dipToPixel(
- Utils.getDipScale(service), RECOGNITIONVIEW_MINIMUM_HEIGHT_DIP);
- mImm = InputMethodManagerCompatWrapper.getInstance(service);
- mSubtypeSwitcher = SubtypeSwitcher.getInstance();
- if (VOICE_INSTALLED) {
- mVoiceInput = new VoiceInput(service, this);
- mHints = new Hints(service, prefs, new Hints.Display() {
- @Override
- public void showHint(int viewResource) {
- View view = LayoutInflater.from(mService).inflate(viewResource, null);
-// mService.setCandidatesView(view);
-// mService.setCandidatesViewShown(true);
- mIsShowingHint = true;
- }
- });
- }
- }
-
- private VoiceProxy() {
- // Intentional empty constructor for singleton.
- }
-
- public void resetVoiceStates(boolean isPasswordText) {
- mAfterVoiceInput = false;
- mImmediatelyAfterVoiceInput = false;
- mShowingVoiceSuggestions = false;
- mVoiceInputHighlighted = false;
- mPasswordText = isPasswordText;
- }
-
- public void flushVoiceInputLogs(boolean configurationChanged) {
- if (VOICE_INSTALLED && !configurationChanged) {
- if (mAfterVoiceInput) {
- mVoiceInput.flushAllTextModificationCounters();
- mVoiceInput.logInputEnded();
- }
- mVoiceInput.flushLogs();
- mVoiceInput.cancel();
- }
- }
-
- public void flushAndLogAllTextModificationCounters(int index, CharSequence suggestion,
- String wordSeparators) {
- if (mAfterVoiceInput && mShowingVoiceSuggestions) {
- mVoiceInput.flushAllTextModificationCounters();
- // send this intent AFTER logging any prior aggregated edits.
- mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
- wordSeparators, mService.getCurrentInputConnection());
- }
- }
-
- private void showVoiceWarningDialog(final boolean swipe, IBinder token) {
- if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
- return;
- }
- AlertDialog.Builder builder = new UrlLinkAlertDialogBuilder(mService);
- builder.setCancelable(true);
- builder.setIcon(R.drawable.ic_mic_dialog);
- builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- mVoiceInput.logKeyboardWarningDialogOk();
- reallyStartListening(swipe);
- }
- });
- builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- mVoiceInput.logKeyboardWarningDialogCancel();
- switchToLastInputMethod();
- }
- });
- // When the dialog is dismissed by user's cancellation, switch back to the last input method
- builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
- @Override
- public void onCancel(DialogInterface arg0) {
- mVoiceInput.logKeyboardWarningDialogCancel();
- switchToLastInputMethod();
- }
- });
-
- final CharSequence message;
- if (mLocaleSupportedForVoiceInput) {
- message = TextUtils.concat(
- mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
- mService.getText(R.string.voice_warning_how_to_turn_off));
- } else {
- message = TextUtils.concat(
- mService.getText(R.string.voice_warning_locale_not_supported), "\n\n",
- mService.getText(R.string.voice_warning_may_not_understand), "\n\n",
- mService.getText(R.string.voice_warning_how_to_turn_off));
- }
- builder.setMessage(message);
- builder.setTitle(R.string.voice_warning_title);
- mVoiceWarningDialog = builder.create();
- final Window window = mVoiceWarningDialog.getWindow();
- final WindowManager.LayoutParams lp = window.getAttributes();
- lp.token = token;
- lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
- window.setAttributes(lp);
- window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
- mVoiceInput.logKeyboardWarningDialogShown();
- mVoiceWarningDialog.show();
- }
-
- private static class UrlLinkAlertDialogBuilder extends AlertDialog.Builder {
- private AlertDialog mAlertDialog;
-
- public UrlLinkAlertDialogBuilder(Context context) {
- super(context);
- }
-
- @Override
- public AlertDialog.Builder setMessage(CharSequence message) {
- return super.setMessage(replaceURLSpan(message));
- }
-
- private Spanned replaceURLSpan(CharSequence message) {
- // Replace all spans with the custom span
- final SpannableStringBuilder ssb = new SpannableStringBuilder(message);
- for (URLSpan span : ssb.getSpans(0, ssb.length(), URLSpan.class)) {
- int spanStart = ssb.getSpanStart(span);
- int spanEnd = ssb.getSpanEnd(span);
- int spanFlags = ssb.getSpanFlags(span);
- ssb.removeSpan(span);
- ssb.setSpan(new ClickableSpan(span.getURL()), spanStart, spanEnd, spanFlags);
- }
- return ssb;
- }
-
- @Override
- public AlertDialog create() {
- final AlertDialog dialog = super.create();
-
- dialog.setOnShowListener(new DialogInterface.OnShowListener() {
- @Override
- public void onShow(DialogInterface dialogInterface) {
- // Make URL in the dialog message click-able.
- TextView textView = (TextView) mAlertDialog.findViewById(android.R.id.message);
- if (textView != null) {
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- }
- });
- mAlertDialog = dialog;
- return dialog;
- }
-
- class ClickableSpan extends URLSpan {
- public ClickableSpan(String url) {
- super(url);
- }
-
- @Override
- public void onClick(View widget) {
- Uri uri = Uri.parse(getURL());
- Context context = widget.getContext();
- Intent intent = new Intent(Intent.ACTION_VIEW, uri);
- // Add this flag to start an activity from service
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
- // Dismiss the warning dialog and go back to the previous IME.
- // TODO: If we can find a way to bring the new activity to front while keeping
- // the warning dialog, we don't need to dismiss it here.
- mAlertDialog.cancel();
- context.startActivity(intent);
- }
- }
- }
-
- public void showPunctuationHintIfNecessary() {
- InputConnection ic = mService.getCurrentInputConnection();
- if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
- if (mHints.showPunctuationHintIfNecessary(ic)) {
- mVoiceInput.logPunctuationHintDisplayed();
- }
- }
- mImmediatelyAfterVoiceInput = false;
- }
-
- public void hideVoiceWindow(boolean configurationChanging) {
- if (!configurationChanging) {
- if (mAfterVoiceInput)
- mVoiceInput.logInputEnded();
- if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
- mVoiceInput.logKeyboardWarningDialogDismissed();
- mVoiceWarningDialog.dismiss();
- mVoiceWarningDialog = null;
- }
- if (VOICE_INSTALLED & mRecognizing) {
- mVoiceInput.cancel();
- }
- }
- mWordToSuggestions.clear();
- }
-
- public void setCursorAndSelection(int newSelEnd, int newSelStart) {
- if (mAfterVoiceInput) {
- mVoiceInput.setCursorPos(newSelEnd);
- mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
- }
- }
-
- public void setVoiceInputHighlighted(boolean b) {
- mVoiceInputHighlighted = b;
- }
-
- public void setShowingVoiceSuggestions(boolean b) {
- mShowingVoiceSuggestions = b;
- }
-
- public boolean isVoiceButtonEnabled() {
- return mVoiceButtonEnabled;
- }
-
- public boolean isVoiceButtonOnPrimary() {
- return mVoiceButtonOnPrimary;
- }
-
- public boolean isVoiceInputHighlighted() {
- return mVoiceInputHighlighted;
- }
-
- public boolean isRecognizing() {
- return mRecognizing;
- }
-
- public boolean needsToShowWarningDialog() {
- return !mHasUsedVoiceInput
- || (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale);
- }
-
- public boolean getAndResetIsShowingHint() {
- boolean ret = mIsShowingHint;
- mIsShowingHint = false;
- return ret;
- }
-
- private void revertVoiceInput() {
- InputConnection ic = mService.getCurrentInputConnection();
- if (ic != null) ic.commitText("", 1);
- mService.updateSuggestions();
- mVoiceInputHighlighted = false;
- }
-
- public void commitVoiceInput() {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- InputConnection ic = mService.getCurrentInputConnection();
- if (ic != null) ic.finishComposingText();
- mService.updateSuggestions();
- mVoiceInputHighlighted = false;
- }
- }
-
- public boolean logAndRevertVoiceInput() {
- if (VOICE_INSTALLED && mVoiceInputHighlighted) {
- mVoiceInput.incrementTextModificationDeleteCount(
- mVoiceResults.candidates.get(0).toString().length());
- revertVoiceInput();
- return true;
- } else {
- return false;
- }
- }
-
- public void rememberReplacedWord(CharSequence suggestion,String wordSeparators) {
- if (mShowingVoiceSuggestions) {
- // Retain the replaced word in the alternatives array.
- String wordToBeReplaced = EditingUtils.getWordAtCursor(
- mService.getCurrentInputConnection(), wordSeparators);
- if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
- wordToBeReplaced = wordToBeReplaced.toLowerCase();
- }
- if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
- List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
- if (suggestions.contains(suggestion)) {
- suggestions.remove(suggestion);
- }
- suggestions.add(wordToBeReplaced);
- mWordToSuggestions.remove(wordToBeReplaced);
- mWordToSuggestions.put(suggestion.toString(), suggestions);
- }
- }
- }
-
- /**
- * Tries to apply any voice alternatives for the word if this was a spoken word and
- * there are voice alternatives.
- * @param touching The word that the cursor is touching, with position information
- * @return true if an alternative was found, false otherwise.
- */
- public boolean applyVoiceAlternatives(EditingUtils.SelectedWord touching) {
- // Search for result in spoken word alternatives
- String selectedWord = touching.mWord.toString().trim();
- if (!mWordToSuggestions.containsKey(selectedWord)) {
- selectedWord = selectedWord.toLowerCase();
- }
- if (mWordToSuggestions.containsKey(selectedWord)) {
- mShowingVoiceSuggestions = true;
- List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
- SuggestedWords.Builder builder = new SuggestedWords.Builder();
- // If the first letter of touching is capitalized, make all the suggestions
- // start with a capital letter.
- if (Character.isUpperCase(touching.mWord.charAt(0))) {
- for (CharSequence word : suggestions) {
- String str = word.toString();
- word = Character.toUpperCase(str.charAt(0)) + str.substring(1);
- builder.addWord(word);
- }
- } else {
- builder.addWords(suggestions, null);
- }
- builder.setTypedWordValid(true).setHasMinimalSuggestion(true);
- mService.setSuggestions(builder.build());
-// mService.setCandidatesViewShown(true);
- return true;
- }
- return false;
- }
-
- public void handleBackspace() {
- if (mAfterVoiceInput) {
- // Don't log delete if the user is pressing delete at
- // the beginning of the text box (hence not deleting anything)
- if (mVoiceInput.getCursorPos() > 0) {
- // If anything was selected before the delete was pressed, increment the
- // delete count by the length of the selection
- int deleteLen = mVoiceInput.getSelectionSpan() > 0 ?
- mVoiceInput.getSelectionSpan() : 1;
- mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
- }
- }
- }
-
- public void handleCharacter() {
- commitVoiceInput();
- if (mAfterVoiceInput) {
- // Assume input length is 1. This assumption fails for smiley face insertions.
- mVoiceInput.incrementTextModificationInsertCount(1);
- }
- }
-
- public void handleSeparator() {
- commitVoiceInput();
- if (mAfterVoiceInput){
- // Assume input length is 1. This assumption fails for smiley face insertions.
- mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
- }
- }
-
- public void handleClose() {
- if (VOICE_INSTALLED & mRecognizing) {
- mVoiceInput.cancel();
- }
- }
-
-
- public void handleVoiceResults(boolean capitalizeFirstWord) {
- mAfterVoiceInput = true;
- mImmediatelyAfterVoiceInput = true;
-
- InputConnection ic = mService.getCurrentInputConnection();
- if (!mService.isFullscreenMode()) {
- // Start listening for updates to the text from typing, etc.
- if (ic != null) {
- ExtractedTextRequest req = new ExtractedTextRequest();
- ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
- }
- }
- mService.vibrate();
-
- final List<CharSequence> nBest = new ArrayList<CharSequence>();
- for (String c : mVoiceResults.candidates) {
- if (capitalizeFirstWord) {
- c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
- }
- nBest.add(c);
- }
- if (nBest.size() == 0) {
- return;
- }
- String bestResult = nBest.get(0).toString();
- mVoiceInput.logVoiceInputDelivered(bestResult.length());
- mHints.registerVoiceResult(bestResult);
-
- if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
- mService.commitTyped(ic);
- EditingUtils.appendText(ic, bestResult);
- if (ic != null) ic.endBatchEdit();
-
- mVoiceInputHighlighted = true;
- mWordToSuggestions.putAll(mVoiceResults.alternatives);
- onCancelVoice();
- }
-
- public void switchToRecognitionStatusView(final Configuration configuration) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
-// mService.setCandidatesViewShown(false);
- mRecognizing = true;
- mVoiceInput.newView();
- View v = mVoiceInput.getView();
-
- ViewParent p = v.getParent();
- if (p != null && p instanceof ViewGroup) {
- ((ViewGroup) p).removeView(v);
- }
-
- View keyboardView = KeyboardSwitcher.getInstance().getKeyboardView();
-
- // The full height of the keyboard is difficult to calculate
- // as the dimension is expressed in "mm" and not in "pixel"
- // As we add mm, we don't know how the rounding is going to work
- // thus we may end up with few pixels extra (or less).
- if (keyboardView != null) {
- View popupLayout = v.findViewById(R.id.popup_layout);
- final int displayHeight =
- mService.getResources().getDisplayMetrics().heightPixels;
- final int currentHeight = popupLayout.getLayoutParams().height;
- final int keyboardHeight = keyboardView.getHeight();
- if (mMinimumVoiceRecognitionViewHeightPixel > keyboardHeight
- || mMinimumVoiceRecognitionViewHeightPixel > currentHeight) {
- popupLayout.getLayoutParams().height =
- mMinimumVoiceRecognitionViewHeightPixel;
- } else if (keyboardHeight > currentHeight || keyboardHeight
- > (displayHeight / RECOGNITIONVIEW_HEIGHT_THRESHOLD_RATIO)) {
- popupLayout.getLayoutParams().height = keyboardHeight;
- }
- }
- mService.setInputView(v);
- mService.updateInputViewShown();
-
- if (configuration != null) {
- mVoiceInput.onConfigurationChanged(configuration);
- }
- }});
- }
-
- private void switchToLastInputMethod() {
- final IBinder token = mService.getWindow().getWindow().getAttributes().token;
- new AsyncTask<Void, Void, Boolean>() {
- @Override
- protected Boolean doInBackground(Void... params) {
- return mImm.switchToLastInputMethod(token);
- }
-
- @Override
- protected void onPostExecute(Boolean result) {
- // Calls in this method need to be done in the same thread as the thread which
- // called switchToLastInputMethod()
- if (!result) {
- if (DEBUG) {
- Log.d(TAG, "Couldn't switch back to last IME.");
- }
- // Because the current IME and subtype failed to switch to any other IME and
- // subtype by switchToLastInputMethod, the current IME and subtype should keep
- // being LatinIME and voice subtype in the next time. And for re-showing voice
- // mode, the state of voice input should be reset and the voice view should be
- // hidden.
- mVoiceInput.reset();
- mService.requestHideSelf(0);
- } else {
- // Notify an event that the current subtype was changed. This event will be
- // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented
- // when the API level is 10 or previous.
- mService.notifyOnCurrentInputMethodSubtypeChanged(null);
- }
- }
- }.execute();
- }
-
- private void reallyStartListening(boolean swipe) {
- if (!VOICE_INSTALLED) {
- return;
- }
- if (!mHasUsedVoiceInput) {
- // The user has started a voice input, so remember that in the
- // future (so we don't show the warning dialog after the first run).
- SharedPreferences.Editor editor =
- PreferenceManager.getDefaultSharedPreferences(mService).edit();
- editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
- SharedPreferencesCompat.apply(editor);
- mHasUsedVoiceInput = true;
- }
-
- if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
- // The user has started a voice input from an unsupported locale, so remember that
- // in the future (so we don't show the warning dialog the next time they do this).
- SharedPreferences.Editor editor =
- PreferenceManager.getDefaultSharedPreferences(mService).edit();
- editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
- SharedPreferencesCompat.apply(editor);
- mHasUsedVoiceInputUnsupportedLocale = true;
- }
-
- // Clear N-best suggestions
- mService.clearSuggestions();
-
- FieldContext context = makeFieldContext();
- mVoiceInput.startListening(context, swipe);
- switchToRecognitionStatusView(null);
- }
-
- public void startListening(final boolean swipe, IBinder token) {
- // TODO: remove swipe which is no longer used.
- if (VOICE_INSTALLED) {
- if (needsToShowWarningDialog()) {
- // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
- showVoiceWarningDialog(swipe, token);
- } else {
- reallyStartListening(swipe);
- }
- }
- }
-
- private boolean fieldCanDoVoice(FieldContext fieldContext) {
- return !mPasswordText
- && mVoiceInput != null
- && !mVoiceInput.isBlacklistedField(fieldContext);
- }
-
- private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
- @SuppressWarnings("deprecation")
- final boolean noMic = Utils.inPrivateImeOptions(null,
- LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, attribute)
- || Utils.inPrivateImeOptions(mService.getPackageName(),
- LatinIME.IME_OPTION_NO_MICROPHONE, attribute);
- return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext) && !noMic
- && SpeechRecognizer.isRecognitionAvailable(mService);
- }
-
- public void loadSettings(EditorInfo attribute, SharedPreferences sp) {
- mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
- mHasUsedVoiceInputUnsupportedLocale =
- sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
-
- mLocaleSupportedForVoiceInput = SubtypeSwitcher.getInstance().isVoiceSupported(
- SubtypeSwitcher.getInstance().getInputLocaleStr());
-
- if (VOICE_INSTALLED) {
- final String voiceMode = sp.getString(PREF_VOICE_MODE,
- mService.getString(R.string.voice_mode_main));
- mVoiceButtonEnabled = !voiceMode.equals(mService.getString(R.string.voice_mode_off))
- && shouldShowVoiceButton(makeFieldContext(), attribute);
- mVoiceButtonOnPrimary = voiceMode.equals(mService.getString(R.string.voice_mode_main));
- }
- }
-
- public void destroy() {
- if (VOICE_INSTALLED && mVoiceInput != null) {
- mVoiceInput.destroy();
- }
- }
-
- public void onStartInputView(IBinder keyboardViewToken) {
- // If keyboardViewToken is null, keyboardView is not attached but voiceView is attached.
- IBinder windowToken = keyboardViewToken != null ? keyboardViewToken
- : mVoiceInput.getView().getWindowToken();
- // If IME is in voice mode, but still needs to show the voice warning dialog,
- // keep showing the warning.
- if (mSubtypeSwitcher.isVoiceMode() && windowToken != null) {
- // Close keyboard view if it is been shown.
- if (KeyboardSwitcher.getInstance().isInputViewShown())
- KeyboardSwitcher.getInstance().getKeyboardView().purgeKeyboardAndClosing();
- startListening(false, windowToken);
- }
- // If we have no token, onAttachedToWindow will take care of showing dialog and start
- // listening.
- }
-
- public void onAttachedToWindow() {
- // After onAttachedToWindow, we can show the voice warning dialog. See startListening()
- // above.
- VoiceInputWrapper.getInstance().setVoiceInput(mVoiceInput, mSubtypeSwitcher);
- }
-
- public void onConfigurationChanged(Configuration configuration) {
- if (mRecognizing) {
- switchToRecognitionStatusView(configuration);
- }
- }
-
- @Override
- public void onCancelVoice() {
- if (mRecognizing) {
- if (mSubtypeSwitcher.isVoiceMode()) {
- // If voice mode is being canceled within LatinIME (i.e. time-out or user
- // cancellation etc.), onCancelVoice() will be called first. LatinIME thinks it's
- // still in voice mode. LatinIME needs to call switchToLastInputMethod().
- // Note that onCancelVoice() will be called again from SubtypeSwitcher.
- switchToLastInputMethod();
- } else if (mSubtypeSwitcher.isKeyboardMode()) {
- // If voice mode is being canceled out of LatinIME (i.e. by user's IME switching or
- // as a result of switchToLastInputMethod() etc.),
- // onCurrentInputMethodSubtypeChanged() will be called first. LatinIME will know
- // that it's in keyboard mode and SubtypeSwitcher will call onCancelVoice().
- mRecognizing = false;
- mService.switchToKeyboardView();
- }
- }
- }
-
- @Override
- public void onVoiceResults(List<String> candidates,
- Map<String, List<CharSequence>> alternatives) {
- if (!mRecognizing) {
- return;
- }
- mVoiceResults.candidates = candidates;
- mVoiceResults.alternatives = alternatives;
- mHandler.updateVoiceResults();
- }
-
- private FieldContext makeFieldContext() {
- SubtypeSwitcher switcher = SubtypeSwitcher.getInstance();
- return new FieldContext(mService.getCurrentInputConnection(),
- mService.getCurrentInputEditorInfo(), switcher.getInputLocaleStr(),
- switcher.getEnabledLanguages());
- }
-
- private class VoiceResults {
- List<String> candidates;
- Map<String, List<CharSequence>> alternatives;
- }
-
- public static class VoiceLoggerWrapper {
- private static final VoiceLoggerWrapper sLoggerWrapperInstance = new VoiceLoggerWrapper();
- private VoiceInputLogger mLogger;
-
- public static VoiceLoggerWrapper getInstance(Context context) {
- if (sLoggerWrapperInstance.mLogger == null) {
- // Not thread safe, but it's ok.
- sLoggerWrapperInstance.mLogger = VoiceInputLogger.getLogger(context);
- }
- return sLoggerWrapperInstance;
- }
-
- // private for the singleton
- private VoiceLoggerWrapper() {
- }
-
- public void settingsWarningDialogCancel() {
- mLogger.settingsWarningDialogCancel();
- }
-
- public void settingsWarningDialogOk() {
- mLogger.settingsWarningDialogOk();
- }
-
- public void settingsWarningDialogShown() {
- mLogger.settingsWarningDialogShown();
- }
-
- public void settingsWarningDialogDismissed() {
- mLogger.settingsWarningDialogDismissed();
- }
-
- public void voiceInputSettingEnabled(boolean enabled) {
- if (enabled) {
- mLogger.voiceInputSettingEnabled();
- } else {
- mLogger.voiceInputSettingDisabled();
- }
- }
- }
-
- public static class VoiceInputWrapper {
- private static final VoiceInputWrapper sInputWrapperInstance = new VoiceInputWrapper();
- private VoiceInput mVoiceInput;
- public static VoiceInputWrapper getInstance() {
- return sInputWrapperInstance;
- }
- public void setVoiceInput(VoiceInput voiceInput, SubtypeSwitcher switcher) {
- if (mVoiceInput == null && voiceInput != null) {
- mVoiceInput = voiceInput;
- }
- switcher.setVoiceInputWrapper(this);
- }
-
- private VoiceInputWrapper() {
- }
-
- public void cancel() {
- if (mVoiceInput != null) mVoiceInput.cancel();
- }
-
- public void reset() {
- if (mVoiceInput != null) mVoiceInput.reset();
- }
- }
-
- // A list of locales which are supported by default for voice input, unless we get a
- // different list from Gservices.
- private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES =
- "en " +
- "en_US " +
- "en_GB " +
- "en_AU " +
- "en_CA " +
- "en_IE " +
- "en_IN " +
- "en_NZ " +
- "en_SG " +
- "en_ZA ";
-
- public static String getSupportedLocalesString (ContentResolver resolver) {
- return SettingsUtil.getSettingsString(
- resolver,
- SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES,
- DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java b/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java
deleted file mode 100644
index 488390fbc..000000000
--- a/java/src/com/android/inputmethod/deprecated/compat/VoiceInputLoggerCompatUtils.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2011 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.deprecated.compat;
-
-import com.android.common.userhappiness.UserHappinessSignals;
-import com.android.inputmethod.compat.CompatUtils;
-
-import java.lang.reflect.Method;
-
-public class VoiceInputLoggerCompatUtils {
- public static final String EXTRA_TEXT_REPLACED_LENGTH = "length";
- public static final String EXTRA_BEFORE_N_BEST_CHOOSE = "before";
- public static final String EXTRA_AFTER_N_BEST_CHOOSE = "after";
- private static final Method METHOD_UserHappinessSignals_setHasVoiceLoggingInfo =
- CompatUtils.getMethod(UserHappinessSignals.class, "setHasVoiceLoggingInfo",
- boolean.class);
-
- public static void setHasVoiceLoggingInfoCompat(boolean hasLoggingInfo) {
- CompatUtils.invoke(null, null, METHOD_UserHappinessSignals_setHasVoiceLoggingInfo,
- hasLoggingInfo);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
deleted file mode 100644
index cf6cd0f5e..000000000
--- a/java/src/com/android/inputmethod/deprecated/languageswitcher/InputLanguageSelection.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright (C) 2008-2009 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.deprecated.languageswitcher;
-
-import com.android.inputmethod.keyboard.internal.KeyboardParser;
-import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethod.latin.SharedPreferencesCompat;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.Utils;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.preference.CheckBoxPreference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceGroup;
-import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.Pair;
-
-import java.io.IOException;
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-
-public class InputLanguageSelection extends PreferenceActivity {
-
- private SharedPreferences mPrefs;
- private String mSelectedLanguages;
- private HashMap<CheckBoxPreference, Locale> mLocaleMap =
- new HashMap<CheckBoxPreference, Locale>();
-
- private static class LocaleEntry implements Comparable<Object> {
- private static Collator sCollator = Collator.getInstance();
-
- private String mLabel;
- public final Locale mLocale;
-
- public LocaleEntry(String label, Locale locale) {
- this.mLabel = label;
- this.mLocale = locale;
- }
-
- @Override
- public String toString() {
- return this.mLabel;
- }
-
- @Override
- public int compareTo(Object o) {
- return sCollator.compare(this.mLabel, ((LocaleEntry) o).mLabel);
- }
- }
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- addPreferencesFromResource(R.xml.language_prefs);
- // Get the settings preferences
- mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
- mSelectedLanguages = mPrefs.getString(Settings.PREF_SELECTED_LANGUAGES, "");
- String[] languageList = mSelectedLanguages.split(",");
- ArrayList<LocaleEntry> availableLanguages = getUniqueLocales();
- PreferenceGroup parent = getPreferenceScreen();
- final HashMap<Long, LocaleEntry> dictionaryIdLocaleMap = new HashMap<Long, LocaleEntry>();
- final TreeMap<LocaleEntry, Boolean> localeHasDictionaryMap =
- new TreeMap<LocaleEntry, Boolean>();
- for (int i = 0; i < availableLanguages.size(); i++) {
- LocaleEntry loc = availableLanguages.get(i);
- Locale locale = loc.mLocale;
- final Pair<Long, Boolean> hasDictionaryOrLayout = hasDictionaryOrLayout(locale);
- final Long dictionaryId = hasDictionaryOrLayout.first;
- final boolean hasLayout = hasDictionaryOrLayout.second;
- final boolean hasDictionary = dictionaryId != null;
- // Add this locale to the supported list if:
- // 1) this locale has a layout/ 2) this locale has a dictionary
- // If some locales have no layout but have a same dictionary, the shortest locale
- // will be added to the supported list.
- if (!hasLayout && !hasDictionary) {
- continue;
- }
- if (hasLayout) {
- localeHasDictionaryMap.put(loc, hasDictionary);
- }
- if (!hasDictionary) {
- continue;
- }
- if (dictionaryIdLocaleMap.containsKey(dictionaryId)) {
- final String newLocale = locale.toString();
- final String oldLocale =
- dictionaryIdLocaleMap.get(dictionaryId).mLocale.toString();
- // Check if this locale is more appropriate to be the candidate of the input locale.
- if (oldLocale.length() <= newLocale.length() && !hasLayout) {
- // Don't add this new locale to the map<dictionary id, locale> if:
- // 1) the new locale's name is longer than the existing one, and
- // 2) the new locale doesn't have its layout
- continue;
- }
- }
- dictionaryIdLocaleMap.put(dictionaryId, loc);
- }
-
- for (LocaleEntry localeEntry : dictionaryIdLocaleMap.values()) {
- if (!localeHasDictionaryMap.containsKey(localeEntry)) {
- localeHasDictionaryMap.put(localeEntry, true);
- }
- }
-
- for (Entry<LocaleEntry, Boolean> entry : localeHasDictionaryMap.entrySet()) {
- final LocaleEntry localeEntry = entry.getKey();
- final Locale locale = localeEntry.mLocale;
- final Boolean hasDictionary = entry.getValue();
- CheckBoxPreference pref = new CheckBoxPreference(this);
- pref.setTitle(localeEntry.mLabel);
- boolean checked = isLocaleIn(locale, languageList);
- pref.setChecked(checked);
- if (hasDictionary) {
- pref.setSummary(R.string.has_dictionary);
- }
- mLocaleMap.put(pref, locale);
- parent.addPreference(pref);
- }
- }
-
- private boolean isLocaleIn(Locale locale, String[] list) {
- String lang = get5Code(locale);
- for (int i = 0; i < list.length; i++) {
- if (lang.equalsIgnoreCase(list[i])) return true;
- }
- return false;
- }
-
- private Pair<Long, Boolean> hasDictionaryOrLayout(Locale locale) {
- if (locale == null) return new Pair<Long, Boolean>(null, false);
- final Resources res = getResources();
- final Locale saveLocale = Utils.setSystemLocale(res, locale);
- final Long dictionaryId = DictionaryFactory.getDictionaryId(this, locale);
- boolean hasLayout = false;
-
- try {
- final String localeStr = locale.toString();
- final String[] layoutCountryCodes = KeyboardParser.parseKeyboardLocale(
- this, R.xml.kbd_qwerty).split(",", -1);
- if (!TextUtils.isEmpty(localeStr) && layoutCountryCodes.length > 0) {
- for (String s : layoutCountryCodes) {
- if (s.equals(localeStr)) {
- hasLayout = true;
- break;
- }
- }
- }
- } catch (XmlPullParserException e) {
- } catch (IOException e) {
- }
- Utils.setSystemLocale(res, saveLocale);
- return new Pair<Long, Boolean>(dictionaryId, hasLayout);
- }
-
- private String get5Code(Locale locale) {
- String country = locale.getCountry();
- return locale.getLanguage()
- + (TextUtils.isEmpty(country) ? "" : "_" + country);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- // Save the selected languages
- String checkedLanguages = "";
- PreferenceGroup parent = getPreferenceScreen();
- int count = parent.getPreferenceCount();
- for (int i = 0; i < count; i++) {
- CheckBoxPreference pref = (CheckBoxPreference) parent.getPreference(i);
- if (pref.isChecked()) {
- checkedLanguages += get5Code(mLocaleMap.get(pref)) + ",";
- }
- }
- if (checkedLanguages.length() < 1) checkedLanguages = null; // Save null
- Editor editor = mPrefs.edit();
- editor.putString(Settings.PREF_SELECTED_LANGUAGES, checkedLanguages);
- SharedPreferencesCompat.apply(editor);
- }
-
- public ArrayList<LocaleEntry> getUniqueLocales() {
- String[] locales = getAssets().getLocales();
- Arrays.sort(locales);
- ArrayList<LocaleEntry> uniqueLocales = new ArrayList<LocaleEntry>();
-
- final int origSize = locales.length;
- LocaleEntry[] preprocess = new LocaleEntry[origSize];
- int finalSize = 0;
- for (int i = 0 ; i < origSize; i++ ) {
- String s = locales[i];
- int len = s.length();
- String language = "";
- String country = "";
- if (len == 5) {
- language = s.substring(0, 2);
- country = s.substring(3, 5);
- } else if (len < 5) {
- language = s;
- }
- Locale l = new Locale(language, country);
-
- // Exclude languages that are not relevant to LatinIME
- if (TextUtils.isEmpty(language)) {
- continue;
- }
-
- if (finalSize == 0) {
- preprocess[finalSize++] =
- new LocaleEntry(SubtypeSwitcher.getFullDisplayName(l, false), l);
- } else {
- if (s.equals("zz_ZZ")) {
- // ignore this locale
- } else {
- final String displayName = SubtypeSwitcher.getFullDisplayName(l, false);
- preprocess[finalSize++] = new LocaleEntry(displayName, l);
- }
- }
- }
- for (int i = 0; i < finalSize ; i++) {
- uniqueLocales.add(preprocess[i]);
- }
- return uniqueLocales;
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java b/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
deleted file mode 100644
index 1eedb5ee1..000000000
--- a/java/src/com/android/inputmethod/deprecated/languageswitcher/LanguageSwitcher.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2010 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.deprecated.languageswitcher;
-
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethod.latin.SharedPreferencesCompat;
-import com.android.inputmethod.latin.Utils;
-
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.content.res.Configuration;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Locale;
-
-/**
- * Keeps track of list of selected input languages and the current
- * input language that the user has selected.
- */
-public class LanguageSwitcher {
- private static final String TAG = LanguageSwitcher.class.getSimpleName();
-
- @SuppressWarnings("unused")
- private static final String KEYBOARD_MODE = "keyboard";
- private static final String[] EMPTY_STIRNG_ARRAY = new String[0];
-
- private final ArrayList<Locale> mLocales = new ArrayList<Locale>();
- private final LatinIME mIme;
- private String[] mSelectedLanguageArray = EMPTY_STIRNG_ARRAY;
- private String mSelectedLanguages;
- private int mCurrentIndex = 0;
- private String mDefaultInputLanguage;
- private Locale mDefaultInputLocale;
- private Locale mSystemLocale;
-
- public LanguageSwitcher(LatinIME ime) {
- mIme = ime;
- }
-
- public int getLocaleCount() {
- return mLocales.size();
- }
-
- public void onConfigurationChanged(Configuration conf, SharedPreferences prefs) {
- final Locale newLocale = conf.locale;
- if (!getSystemLocale().toString().equals(newLocale.toString())) {
- loadLocales(prefs, newLocale);
- }
- }
-
- /**
- * Loads the currently selected input languages from shared preferences.
- * @param sp shared preference for getting the current input language and enabled languages
- * @param systemLocale the current system locale, stored for changing the current input language
- * based on the system current system locale.
- * @return whether there was any change
- */
- public boolean loadLocales(SharedPreferences sp, Locale systemLocale) {
- if (LatinImeLogger.sDBG) {
- Log.d(TAG, "load locales");
- }
- if (systemLocale != null) {
- setSystemLocale(systemLocale);
- }
- String selectedLanguages = sp.getString(Settings.PREF_SELECTED_LANGUAGES, null);
- String currentLanguage = sp.getString(Settings.PREF_INPUT_LANGUAGE, null);
- if (TextUtils.isEmpty(selectedLanguages)) {
- mSelectedLanguageArray = EMPTY_STIRNG_ARRAY;
- mSelectedLanguages = null;
- loadDefaults();
- if (mLocales.size() == 0) {
- return false;
- }
- mLocales.clear();
- return true;
- }
- if (selectedLanguages.equals(mSelectedLanguages)) {
- return false;
- }
- mSelectedLanguageArray = selectedLanguages.split(",");
- mSelectedLanguages = selectedLanguages; // Cache it for comparison later
- constructLocales();
- mCurrentIndex = 0;
- if (currentLanguage != null) {
- // Find the index
- mCurrentIndex = 0;
- for (int i = 0; i < mLocales.size(); i++) {
- if (mSelectedLanguageArray[i].equals(currentLanguage)) {
- mCurrentIndex = i;
- break;
- }
- }
- // If we didn't find the index, use the first one
- }
- return true;
- }
-
- private void loadDefaults() {
- if (LatinImeLogger.sDBG) {
- Log.d(TAG, "load default locales:");
- }
- mDefaultInputLocale = mIme.getResources().getConfiguration().locale;
- String country = mDefaultInputLocale.getCountry();
- mDefaultInputLanguage = mDefaultInputLocale.getLanguage() +
- (TextUtils.isEmpty(country) ? "" : "_" + country);
- }
-
- private void constructLocales() {
- mLocales.clear();
- for (final String lang : mSelectedLanguageArray) {
- final Locale locale = Utils.constructLocaleFromString(lang);
- mLocales.add(locale);
- }
- }
-
- /**
- * Returns the currently selected input language code, or the display language code if
- * no specific locale was selected for input.
- */
- public String getInputLanguage() {
- if (getLocaleCount() == 0) return mDefaultInputLanguage;
-
- return mSelectedLanguageArray[mCurrentIndex];
- }
-
- /**
- * Returns the list of enabled language codes.
- */
- public String[] getEnabledLanguages(boolean allowImplicitlySelectedLanguages) {
- if (mSelectedLanguageArray.length == 0 && allowImplicitlySelectedLanguages) {
- return new String[] { mDefaultInputLanguage };
- }
- return mSelectedLanguageArray;
- }
-
- /**
- * Returns the currently selected input locale, or the display locale if no specific
- * locale was selected for input.
- */
- public Locale getInputLocale() {
- if (getLocaleCount() == 0) return mDefaultInputLocale;
-
- return mLocales.get(mCurrentIndex);
- }
-
- private int nextLocaleIndex() {
- final int size = mLocales.size();
- return (mCurrentIndex + 1) % size;
- }
-
- private int prevLocaleIndex() {
- final int size = mLocales.size();
- return (mCurrentIndex - 1 + size) % size;
- }
-
- /**
- * Returns the next input locale in the list. Wraps around to the beginning of the
- * list if we're at the end of the list.
- */
- public Locale getNextInputLocale() {
- if (getLocaleCount() == 0) return mDefaultInputLocale;
- return mLocales.get(nextLocaleIndex());
- }
-
- /**
- * Sets the system locale (display UI) used for comparing with the input language.
- * @param locale the locale of the system
- */
- private void setSystemLocale(Locale locale) {
- mSystemLocale = locale;
- }
-
- /**
- * Returns the system locale.
- * @return the system locale
- */
- private Locale getSystemLocale() {
- return mSystemLocale;
- }
-
- /**
- * Returns the previous input locale in the list. Wraps around to the end of the
- * list if we're at the beginning of the list.
- */
- public Locale getPrevInputLocale() {
- if (getLocaleCount() == 0) return mDefaultInputLocale;
- return mLocales.get(prevLocaleIndex());
- }
-
- public void reset() {
- mCurrentIndex = 0;
- }
-
- public void next() {
- mCurrentIndex = nextLocaleIndex();
- }
-
- public void prev() {
- mCurrentIndex = prevLocaleIndex();
- }
-
- public void setLocale(String localeStr) {
- final int N = mLocales.size();
- for (int i = 0; i < N; ++i) {
- if (mLocales.get(i).toString().equals(localeStr)) {
- mCurrentIndex = i;
- }
- }
- }
-
- public void persist(SharedPreferences prefs) {
- Editor editor = prefs.edit();
- editor.putString(Settings.PREF_INPUT_LANGUAGE, getInputLanguage());
- SharedPreferencesCompat.apply(editor);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java b/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java
deleted file mode 100644
index d40728d25..000000000
--- a/java/src/com/android/inputmethod/deprecated/recorrection/Recorrection.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2011 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.deprecated.recorrection;
-
-import com.android.inputmethod.compat.InputConnectionCompatUtils;
-import com.android.inputmethod.compat.SuggestionSpanUtils;
-import com.android.inputmethod.deprecated.VoiceProxy;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.latin.AutoCorrection;
-import com.android.inputmethod.latin.CandidateView;
-import com.android.inputmethod.latin.EditingUtils;
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethod.latin.Suggest;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.TextEntryState;
-import com.android.inputmethod.latin.WordComposer;
-
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.ArrayList;
-
-/**
- * Manager of re-correction functionalities
- */
-public class Recorrection implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final Recorrection sInstance = new Recorrection();
-
- private LatinIME mService;
- private boolean mRecorrectionEnabled = false;
- private final ArrayList<RecorrectionSuggestionEntries> mRecorrectionSuggestionsList =
- new ArrayList<RecorrectionSuggestionEntries>();
-
- public static Recorrection getInstance() {
- return sInstance;
- }
-
- public static void init(LatinIME context, SharedPreferences prefs) {
- if (context == null || prefs == null) {
- return;
- }
- sInstance.initInternal(context, prefs);
- }
-
- private Recorrection() {
- }
-
- public boolean isRecorrectionEnabled() {
- return mRecorrectionEnabled;
- }
-
- private void initInternal(LatinIME context, SharedPreferences prefs) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) {
- mRecorrectionEnabled = false;
- return;
- }
- updateRecorrectionEnabled(context.getResources(), prefs);
- mService = context;
- prefs.registerOnSharedPreferenceChangeListener(this);
- }
-
- public void checkRecorrectionOnStart() {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
-
- final InputConnection ic = mService.getCurrentInputConnection();
- if (ic == null) return;
- // There could be a pending composing span. Clean it up first.
- ic.finishComposingText();
-
- if (mService.isShowingSuggestionsStrip() && mService.isSuggestionsRequested()) {
- // First get the cursor position. This is required by setOldSuggestions(), so that
- // it can pass the correct range to setComposingRegion(). At this point, we don't
- // have valid values for mLastSelectionStart/End because onUpdateSelection() has
- // not been called yet.
- ExtractedTextRequest etr = new ExtractedTextRequest();
- etr.token = 0; // anything is fine here
- ExtractedText et = ic.getExtractedText(etr, 0);
- if (et == null) return;
- mService.setLastSelection(
- et.startOffset + et.selectionStart, et.startOffset + et.selectionEnd);
-
- // Then look for possible corrections in a delayed fashion
- if (!TextUtils.isEmpty(et.text) && mService.isCursorTouchingWord()) {
- mService.mHandler.postUpdateOldSuggestions();
- }
- }
- }
-
- public void updateRecorrectionSelection(KeyboardSwitcher keyboardSwitcher,
- CandidateView candidateView, int candidatesStart, int candidatesEnd,
- int newSelStart, int newSelEnd, int oldSelStart, int lastSelectionStart,
- int lastSelectionEnd, boolean hasUncommittedTypedChars) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
- if (!mService.isShowingSuggestionsStrip()) return;
- if (!keyboardSwitcher.isInputViewShown()) return;
- if (!mService.isSuggestionsRequested()) return;
- // Don't look for corrections if the keyboard is not visible
- // Check if we should go in or out of correction mode.
- if ((candidatesStart == candidatesEnd || newSelStart != oldSelStart || TextEntryState
- .isRecorrecting())
- && (newSelStart < newSelEnd - 1 || !hasUncommittedTypedChars)) {
- if (mService.isCursorTouchingWord() || lastSelectionStart < lastSelectionEnd) {
- mService.mHandler.cancelUpdateBigramPredictions();
- mService.mHandler.postUpdateOldSuggestions();
- } else {
- abortRecorrection(false);
- // If showing the "touch again to save" hint, do not replace it. Else,
- // show the bigrams if we are at the end of the text, punctuation
- // otherwise.
- if (candidateView != null && !candidateView.isShowingAddToDictionaryHint()) {
- InputConnection ic = mService.getCurrentInputConnection();
- if (null == ic || !TextUtils.isEmpty(ic.getTextAfterCursor(1, 0))) {
- if (!mService.isShowingPunctuationList()) {
- mService.setPunctuationSuggestions();
- }
- } else {
- mService.mHandler.postUpdateBigramPredictions();
- }
- }
- }
- }
- }
-
- public void saveRecorrectionSuggestion(WordComposer word, CharSequence result) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
- if (word.size() <= 1) {
- return;
- }
- // Skip if result is null. It happens in some edge case.
- if (TextUtils.isEmpty(result)) {
- return;
- }
-
- // Make a copy of the CharSequence, since it is/could be a mutable CharSequence
- final String resultCopy = result.toString();
- RecorrectionSuggestionEntries entry = new RecorrectionSuggestionEntries(
- resultCopy, new WordComposer(word));
- mRecorrectionSuggestionsList.add(entry);
- }
-
- public void clearWordsInHistory() {
- mRecorrectionSuggestionsList.clear();
- }
-
- /**
- * Tries to apply any typed alternatives for the word if we have any cached alternatives,
- * otherwise tries to find new corrections and completions for the word.
- * @param touching The word that the cursor is touching, with position information
- * @return true if an alternative was found, false otherwise.
- */
- public boolean applyTypedAlternatives(WordComposer word, Suggest suggest,
- KeyboardSwitcher keyboardSwitcher, EditingUtils.SelectedWord touching) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return false;
- // If we didn't find a match, search for result in typed word history
- WordComposer foundWord = null;
- RecorrectionSuggestionEntries alternatives = null;
- // Search old suggestions to suggest re-corrected suggestions.
- for (RecorrectionSuggestionEntries entry : mRecorrectionSuggestionsList) {
- if (TextUtils.equals(entry.getChosenWord(), touching.mWord)) {
- foundWord = entry.mWordComposer;
- alternatives = entry;
- break;
- }
- }
- // If we didn't find a match, at least suggest corrections as re-corrected suggestions.
- if (foundWord == null
- && (AutoCorrection.isValidWord(suggest.getUnigramDictionaries(),
- touching.mWord, true))) {
- foundWord = new WordComposer();
- for (int i = 0; i < touching.mWord.length(); i++) {
- foundWord.add(touching.mWord.charAt(i),
- new int[] { touching.mWord.charAt(i) }, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE);
- }
- foundWord.setFirstCharCapitalized(Character.isUpperCase(touching.mWord.charAt(0)));
- }
- // Found a match, show suggestions
- if (foundWord != null || alternatives != null) {
- if (alternatives == null) {
- alternatives = new RecorrectionSuggestionEntries(touching.mWord, foundWord);
- }
- showRecorrections(suggest, keyboardSwitcher, alternatives);
- if (foundWord != null) {
- word.init(foundWord);
- } else {
- word.reset();
- }
- return true;
- }
- return false;
- }
-
-
- private void showRecorrections(Suggest suggest, KeyboardSwitcher keyboardSwitcher,
- RecorrectionSuggestionEntries entries) {
- SuggestedWords.Builder builder = entries.getAlternatives(suggest, keyboardSwitcher);
- builder.setTypedWordValid(false).setHasMinimalSuggestion(false);
- mService.showSuggestions(builder.build(), entries.getOriginalWord());
- }
-
- public void fetchAndDisplayRecorrectionSuggestions(VoiceProxy voiceProxy,
- CandidateView candidateView, Suggest suggest, KeyboardSwitcher keyboardSwitcher,
- WordComposer word, boolean hasUncommittedTypedChars, int lastSelectionStart,
- int lastSelectionEnd, String wordSeparators) {
- if (!InputConnectionCompatUtils.RECORRECTION_SUPPORTED) return;
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED || !mRecorrectionEnabled) return;
- voiceProxy.setShowingVoiceSuggestions(false);
- if (candidateView != null && candidateView.isShowingAddToDictionaryHint()) {
- return;
- }
- InputConnection ic = mService.getCurrentInputConnection();
- if (ic == null) return;
- if (!hasUncommittedTypedChars) {
- // Extract the selected or touching text
- EditingUtils.SelectedWord touching = EditingUtils.getWordAtCursorOrSelection(ic,
- lastSelectionStart, lastSelectionEnd, wordSeparators);
-
- if (touching != null && touching.mWord.length() > 1) {
- ic.beginBatchEdit();
-
- if (applyTypedAlternatives(word, suggest, keyboardSwitcher, touching)
- || voiceProxy.applyVoiceAlternatives(touching)) {
- TextEntryState.selectedForRecorrection();
- InputConnectionCompatUtils.underlineWord(ic, touching);
- } else {
- abortRecorrection(true);
- }
-
- ic.endBatchEdit();
- } else {
- abortRecorrection(true);
- mService.updateBigramPredictions();
- }
- } else {
- abortRecorrection(true);
- }
- }
-
- public void abortRecorrection(boolean force) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) return;
- if (force || TextEntryState.isRecorrecting()) {
- TextEntryState.onAbortRecorrection();
- mService.setCandidatesViewShown(mService.isCandidateStripVisible());
- mService.getCurrentInputConnection().finishComposingText();
- mService.clearSuggestions();
- }
- }
-
- public void updateRecorrectionEnabled(Resources res, SharedPreferences prefs) {
- // If the option should not be shown, do not read the re-correction preference
- // but always use the default setting defined in the resources.
- if (res.getBoolean(R.bool.config_enable_show_recorrection_option)) {
- mRecorrectionEnabled = prefs.getBoolean(Settings.PREF_RECORRECTION_ENABLED,
- res.getBoolean(R.bool.config_default_recorrection_enabled));
- } else {
- mRecorrectionEnabled = res.getBoolean(R.bool.config_default_recorrection_enabled);
- }
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (SuggestionSpanUtils.SUGGESTION_SPAN_IS_SUPPORTED) return;
- if (key.equals(Settings.PREF_RECORRECTION_ENABLED)) {
- updateRecorrectionEnabled(mService.getResources(), prefs);
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java b/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java
deleted file mode 100644
index 5e6c87044..000000000
--- a/java/src/com/android/inputmethod/deprecated/recorrection/RecorrectionSuggestionEntries.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2011 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.deprecated.recorrection;
-
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.latin.Suggest;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.WordComposer;
-
-import android.text.TextUtils;
-
-public class RecorrectionSuggestionEntries {
- public final CharSequence mChosenWord;
- public final WordComposer mWordComposer;
-
- public RecorrectionSuggestionEntries(CharSequence chosenWord, WordComposer wordComposer) {
- mChosenWord = chosenWord;
- mWordComposer = wordComposer;
- }
-
- public CharSequence getChosenWord() {
- return mChosenWord;
- }
-
- public CharSequence getOriginalWord() {
- return mWordComposer.getTypedWord();
- }
-
- public SuggestedWords.Builder getAlternatives(
- Suggest suggest, KeyboardSwitcher keyboardSwitcher) {
- return getTypedSuggestions(suggest, keyboardSwitcher, mWordComposer);
- }
-
- @Override
- public int hashCode() {
- return mChosenWord.hashCode();
- }
-
- @Override
- public boolean equals(Object o) {
- return o instanceof CharSequence && TextUtils.equals(mChosenWord, (CharSequence)o);
- }
-
- private static SuggestedWords.Builder getTypedSuggestions(
- Suggest suggest, KeyboardSwitcher keyboardSwitcher, WordComposer word) {
- return suggest.getSuggestedWordBuilder(keyboardSwitcher.getKeyboardView(), word, null);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java b/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
deleted file mode 100644
index 3c79cc218..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/FieldContext.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import android.os.Bundle;
-import android.util.Log;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-/**
- * Represents information about a given text field, which can be passed
- * to the speech recognizer as context information.
- */
-public class FieldContext {
- private static final boolean DBG = false;
-
- static final String LABEL = "label";
- static final String HINT = "hint";
- static final String PACKAGE_NAME = "packageName";
- static final String FIELD_ID = "fieldId";
- static final String FIELD_NAME = "fieldName";
- static final String SINGLE_LINE = "singleLine";
- static final String INPUT_TYPE = "inputType";
- static final String IME_OPTIONS = "imeOptions";
- static final String SELECTED_LANGUAGE = "selectedLanguage";
- static final String ENABLED_LANGUAGES = "enabledLanguages";
-
- Bundle mFieldInfo;
-
- public FieldContext(InputConnection conn, EditorInfo info,
- String selectedLanguage, String[] enabledLanguages) {
- mFieldInfo = new Bundle();
- addEditorInfoToBundle(info, mFieldInfo);
- addInputConnectionToBundle(conn, mFieldInfo);
- addLanguageInfoToBundle(selectedLanguage, enabledLanguages, mFieldInfo);
- if (DBG) Log.i("FieldContext", "Bundle = " + mFieldInfo.toString());
- }
-
- private static String safeToString(Object o) {
- if (o == null) {
- return "";
- }
- return o.toString();
- }
-
- private static void addEditorInfoToBundle(EditorInfo info, Bundle bundle) {
- if (info == null) {
- return;
- }
-
- bundle.putString(LABEL, safeToString(info.label));
- bundle.putString(HINT, safeToString(info.hintText));
- bundle.putString(PACKAGE_NAME, safeToString(info.packageName));
- bundle.putInt(FIELD_ID, info.fieldId);
- bundle.putString(FIELD_NAME, safeToString(info.fieldName));
- bundle.putInt(INPUT_TYPE, info.inputType);
- bundle.putInt(IME_OPTIONS, info.imeOptions);
- }
-
- @SuppressWarnings("static-access")
- private static void addInputConnectionToBundle(
- InputConnection conn, Bundle bundle) {
- if (conn == null) {
- return;
- }
-
- ExtractedText et = conn.getExtractedText(new ExtractedTextRequest(), 0);
- if (et == null) {
- return;
- }
- bundle.putBoolean(SINGLE_LINE, (et.flags & et.FLAG_SINGLE_LINE) > 0);
- }
-
- private static void addLanguageInfoToBundle(
- String selectedLanguage, String[] enabledLanguages, Bundle bundle) {
- bundle.putString(SELECTED_LANGUAGE, selectedLanguage);
- bundle.putStringArray(ENABLED_LANGUAGES, enabledLanguages);
- }
-
- public Bundle getBundle() {
- return mFieldInfo;
- }
-
- @Override
- public String toString() {
- return mFieldInfo.toString();
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/Hints.java b/java/src/com/android/inputmethod/deprecated/voice/Hints.java
deleted file mode 100644
index 06b234381..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/Hints.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SharedPreferencesCompat;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.view.inputmethod.InputConnection;
-
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Logic to determine when to display hints on usage to the user.
- */
-public class Hints {
- public interface Display {
- public void showHint(int viewResource);
- }
-
- private static final String PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN =
- "voice_hint_num_unique_days_shown";
- private static final String PREF_VOICE_HINT_LAST_TIME_SHOWN =
- "voice_hint_last_time_shown";
- private static final String PREF_VOICE_INPUT_LAST_TIME_USED =
- "voice_input_last_time_used";
- private static final String PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT =
- "voice_punctuation_hint_view_count";
- private static final int DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW = 7;
- private static final int DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS = 7;
-
- private final Context mContext;
- private final SharedPreferences mPrefs;
- private final Display mDisplay;
- private boolean mVoiceResultContainedPunctuation;
- private int mSwipeHintMaxDaysToShow;
- private int mPunctuationHintMaxDisplays;
-
- // Only show punctuation hint if voice result did not contain punctuation.
- static final Map<CharSequence, String> SPEAKABLE_PUNCTUATION
- = new HashMap<CharSequence, String>();
- static {
- SPEAKABLE_PUNCTUATION.put(",", "comma");
- SPEAKABLE_PUNCTUATION.put(".", "period");
- SPEAKABLE_PUNCTUATION.put("?", "question mark");
- }
-
- public Hints(Context context, SharedPreferences prefs, Display display) {
- mContext = context;
- mPrefs = prefs;
- mDisplay = display;
-
- ContentResolver cr = mContext.getContentResolver();
- mSwipeHintMaxDaysToShow = SettingsUtil.getSettingsInt(
- cr,
- SettingsUtil.LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS,
- DEFAULT_SWIPE_HINT_MAX_DAYS_TO_SHOW);
- mPunctuationHintMaxDisplays = SettingsUtil.getSettingsInt(
- cr,
- SettingsUtil.LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS,
- DEFAULT_PUNCTUATION_HINT_MAX_DISPLAYS);
- }
-
- public boolean showSwipeHintIfNecessary(boolean fieldRecommended) {
- if (fieldRecommended && shouldShowSwipeHint()) {
- showHint(R.layout.voice_swipe_hint);
- return true;
- }
-
- return false;
- }
-
- public boolean showPunctuationHintIfNecessary(InputConnection ic) {
- if (!mVoiceResultContainedPunctuation
- && ic != null
- && getAndIncrementPref(PREF_VOICE_PUNCTUATION_HINT_VIEW_COUNT)
- < mPunctuationHintMaxDisplays) {
- CharSequence charBeforeCursor = ic.getTextBeforeCursor(1, 0);
- if (SPEAKABLE_PUNCTUATION.containsKey(charBeforeCursor)) {
- showHint(R.layout.voice_punctuation_hint);
- return true;
- }
- }
-
- return false;
- }
-
- public void registerVoiceResult(String text) {
- // Update the current time as the last time voice input was used.
- SharedPreferences.Editor editor = mPrefs.edit();
- editor.putLong(PREF_VOICE_INPUT_LAST_TIME_USED, System.currentTimeMillis());
- SharedPreferencesCompat.apply(editor);
-
- mVoiceResultContainedPunctuation = false;
- for (CharSequence s : SPEAKABLE_PUNCTUATION.keySet()) {
- if (text.indexOf(s.toString()) >= 0) {
- mVoiceResultContainedPunctuation = true;
- break;
- }
- }
- }
-
- private boolean shouldShowSwipeHint() {
- final SharedPreferences prefs = mPrefs;
-
- int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
-
- // If we've already shown the hint for enough days, we'll return false.
- if (numUniqueDaysShown < mSwipeHintMaxDaysToShow) {
-
- long lastTimeVoiceWasUsed = prefs.getLong(PREF_VOICE_INPUT_LAST_TIME_USED, 0);
-
- // If the user has used voice today, we'll return false. (We don't show the hint on
- // any day that the user has already used voice.)
- if (!isFromToday(lastTimeVoiceWasUsed)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Determines whether the provided time is from some time today (i.e., this day, month,
- * and year).
- */
- private boolean isFromToday(long timeInMillis) {
- if (timeInMillis == 0) return false;
-
- Calendar today = Calendar.getInstance();
- today.setTimeInMillis(System.currentTimeMillis());
-
- Calendar timestamp = Calendar.getInstance();
- timestamp.setTimeInMillis(timeInMillis);
-
- return (today.get(Calendar.YEAR) == timestamp.get(Calendar.YEAR) &&
- today.get(Calendar.DAY_OF_MONTH) == timestamp.get(Calendar.DAY_OF_MONTH) &&
- today.get(Calendar.MONTH) == timestamp.get(Calendar.MONTH));
- }
-
- private void showHint(int hintViewResource) {
- final SharedPreferences prefs = mPrefs;
-
- int numUniqueDaysShown = prefs.getInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, 0);
- long lastTimeHintWasShown = prefs.getLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, 0);
-
- // If this is the first time the hint is being shown today, increase the saved values
- // to represent that. We don't need to increase the last time the hint was shown unless
- // it is a different day from the current value.
- if (!isFromToday(lastTimeHintWasShown)) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putInt(PREF_VOICE_HINT_NUM_UNIQUE_DAYS_SHOWN, numUniqueDaysShown + 1);
- editor.putLong(PREF_VOICE_HINT_LAST_TIME_SHOWN, System.currentTimeMillis());
- SharedPreferencesCompat.apply(editor);
- }
-
- if (mDisplay != null) {
- mDisplay.showHint(hintViewResource);
- }
- }
-
- private int getAndIncrementPref(String pref) {
- final SharedPreferences prefs = mPrefs;
- int value = prefs.getInt(pref, 0);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putInt(pref, value + 1);
- SharedPreferencesCompat.apply(editor);
- return value;
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java b/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
deleted file mode 100644
index dcb826e8f..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/RecognitionView.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.CornerPathEffect;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PathEffect;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import java.io.ByteArrayOutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.ShortBuffer;
-import java.util.Locale;
-
-/**
- * The user interface for the "Speak now" and "working" states.
- * Displays a recognition dialog (with waveform, voice meter, etc.),
- * plays beeps, shows errors, etc.
- */
-public class RecognitionView {
- private static final String TAG = "RecognitionView";
-
- private Handler mUiHandler; // Reference to UI thread
- private View mView;
- private Context mContext;
-
- private TextView mText;
- private ImageView mImage;
- private View mProgress;
- private SoundIndicator mSoundIndicator;
- private TextView mLanguage;
- private Button mButton;
-
- private Drawable mInitializing;
- private Drawable mError;
-
- private static final int INIT = 0;
- private static final int LISTENING = 1;
- private static final int WORKING = 2;
- private static final int READY = 3;
-
- private int mState = INIT;
-
- private final View mPopupLayout;
-
- private final Drawable mListeningBorder;
- private final Drawable mWorkingBorder;
- private final Drawable mErrorBorder;
-
- public RecognitionView(Context context, OnClickListener clickListener) {
- mUiHandler = new Handler();
-
- LayoutInflater inflater = (LayoutInflater) context.getSystemService(
- Context.LAYOUT_INFLATER_SERVICE);
-
- mView = inflater.inflate(R.layout.recognition_status, null);
-
- mPopupLayout= mView.findViewById(R.id.popup_layout);
-
- // Pre-load volume level images
- Resources r = context.getResources();
-
- mListeningBorder = r.getDrawable(R.drawable.vs_dialog_red);
- mWorkingBorder = r.getDrawable(R.drawable.vs_dialog_blue);
- mErrorBorder = r.getDrawable(R.drawable.vs_dialog_yellow);
-
- mInitializing = r.getDrawable(R.drawable.mic_slash);
- mError = r.getDrawable(R.drawable.caution);
-
- mImage = (ImageView) mView.findViewById(R.id.image);
- mProgress = mView.findViewById(R.id.progress);
- mSoundIndicator = (SoundIndicator) mView.findViewById(R.id.sound_indicator);
-
- mButton = (Button) mView.findViewById(R.id.button);
- mButton.setOnClickListener(clickListener);
- mText = (TextView) mView.findViewById(R.id.text);
- mLanguage = (TextView) mView.findViewById(R.id.language);
-
- mContext = context;
- }
-
- public View getView() {
- return mView;
- }
-
- public void restoreState() {
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- // Restart the spinner
- if (mState == WORKING) {
- ((ProgressBar) mProgress).setIndeterminate(false);
- ((ProgressBar) mProgress).setIndeterminate(true);
- }
- }
- });
- }
-
- public void showInitializing() {
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- mState = INIT;
- prepareDialog(mContext.getText(R.string.voice_initializing), mInitializing,
- mContext.getText(R.string.cancel));
- }
- });
- }
-
- public void showListening() {
- Log.d(TAG, "#showListening");
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- mState = LISTENING;
- prepareDialog(mContext.getText(R.string.voice_listening), null,
- mContext.getText(R.string.cancel));
- }
- });
- }
-
- public void updateVoiceMeter(float rmsdB) {
- mSoundIndicator.setRmsdB(rmsdB);
- }
-
- public void showError(final String message) {
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- mState = READY;
- prepareDialog(message, mError, mContext.getText(R.string.ok));
- }
- });
- }
-
- public void showWorking(
- final ByteArrayOutputStream waveBuffer,
- final int speechStartPosition,
- final int speechEndPosition) {
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- mState = WORKING;
- prepareDialog(mContext.getText(R.string.voice_working), null, mContext
- .getText(R.string.cancel));
- final ShortBuffer buf = ByteBuffer.wrap(waveBuffer.toByteArray()).order(
- ByteOrder.nativeOrder()).asShortBuffer();
- buf.position(0);
- waveBuffer.reset();
- showWave(buf, speechStartPosition / 2, speechEndPosition / 2);
- }
- });
- }
-
- private void prepareDialog(CharSequence text, Drawable image,
- CharSequence btnTxt) {
-
- /*
- * The mic of INIT and of LISTENING has to be displayed in the same position. To accomplish
- * that, some text visibility are not set as GONE but as INVISIBLE.
- */
- switch (mState) {
- case INIT:
- mText.setVisibility(View.INVISIBLE);
-
- mProgress.setVisibility(View.GONE);
-
- mImage.setVisibility(View.VISIBLE);
- mImage.setImageResource(R.drawable.mic_slash);
-
- mSoundIndicator.setVisibility(View.GONE);
- mSoundIndicator.stop();
-
- mLanguage.setVisibility(View.INVISIBLE);
-
- mPopupLayout.setBackgroundDrawable(mListeningBorder);
- break;
- case LISTENING:
- mText.setVisibility(View.VISIBLE);
- mText.setText(text);
-
- mProgress.setVisibility(View.GONE);
-
- mImage.setVisibility(View.GONE);
-
- mSoundIndicator.setVisibility(View.VISIBLE);
- mSoundIndicator.start();
-
- Locale locale = SubtypeSwitcher.getInstance().getInputLocale();
-
- mLanguage.setVisibility(View.VISIBLE);
- mLanguage.setText(SubtypeSwitcher.getFullDisplayName(locale, true));
-
- mPopupLayout.setBackgroundDrawable(mListeningBorder);
- break;
- case WORKING:
-
- mText.setVisibility(View.VISIBLE);
- mText.setText(text);
-
- mProgress.setVisibility(View.VISIBLE);
-
- mImage.setVisibility(View.VISIBLE);
-
- mSoundIndicator.setVisibility(View.GONE);
- mSoundIndicator.stop();
-
- mLanguage.setVisibility(View.GONE);
-
- mPopupLayout.setBackgroundDrawable(mWorkingBorder);
- break;
- case READY:
- mText.setVisibility(View.VISIBLE);
- mText.setText(text);
-
- mProgress.setVisibility(View.GONE);
-
- mImage.setVisibility(View.VISIBLE);
- mImage.setImageResource(R.drawable.caution);
-
- mSoundIndicator.setVisibility(View.GONE);
- mSoundIndicator.stop();
-
- mLanguage.setVisibility(View.GONE);
-
- mPopupLayout.setBackgroundDrawable(mErrorBorder);
- break;
- default:
- Log.w(TAG, "Unknown state " + mState);
- }
- mPopupLayout.requestLayout();
- mButton.setText(btnTxt);
- }
-
- /**
- * @return an average abs of the specified buffer.
- */
- private static int getAverageAbs(ShortBuffer buffer, int start, int i, int npw) {
- int from = start + i * npw;
- int end = from + npw;
- int total = 0;
- for (int x = from; x < end; x++) {
- total += Math.abs(buffer.get(x));
- }
- return total / npw;
- }
-
-
- /**
- * Shows waveform of input audio.
- *
- * Copied from version in VoiceSearch's RecognitionActivity.
- *
- * TODO: adjust stroke width based on the size of data.
- * TODO: use dip rather than pixels.
- */
- private void showWave(ShortBuffer waveBuffer, int startPosition, int endPosition) {
- final int w = ((View) mImage.getParent()).getWidth();
- final int h = ((View) mImage.getParent()).getHeight();
- if (w <= 0 || h <= 0) {
- // view is not visible this time. Skip drawing.
- return;
- }
- final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- final Canvas c = new Canvas(b);
- final Paint paint = new Paint();
- paint.setColor(0xFFFFFFFF); // 0xAARRGGBB
- paint.setAntiAlias(true);
- paint.setStyle(Paint.Style.STROKE);
- paint.setAlpha(80);
-
- final PathEffect effect = new CornerPathEffect(3);
- paint.setPathEffect(effect);
-
- final int numSamples = waveBuffer.remaining();
- int endIndex;
- if (endPosition == 0) {
- endIndex = numSamples;
- } else {
- endIndex = Math.min(endPosition, numSamples);
- }
-
- int startIndex = startPosition - 2000; // include 250ms before speech
- if (startIndex < 0) {
- startIndex = 0;
- }
- final int numSamplePerWave = 200; // 8KHz 25ms = 200 samples
- final float scale = 10.0f / 65536.0f;
-
- final int count = (endIndex - startIndex) / numSamplePerWave;
- final float deltaX = 1.0f * w / count;
- int yMax = h / 2;
- Path path = new Path();
- c.translate(0, yMax);
- float x = 0;
- path.moveTo(x, 0);
- for (int i = 0; i < count; i++) {
- final int avabs = getAverageAbs(waveBuffer, startIndex, i , numSamplePerWave);
- int sign = ( (i & 01) == 0) ? -1 : 1;
- final float y = Math.min(yMax, avabs * h * scale) * sign;
- path.lineTo(x, y);
- x += deltaX;
- path.lineTo(x, y);
- }
- if (deltaX > 4) {
- paint.setStrokeWidth(2);
- } else {
- paint.setStrokeWidth(Math.max(0, (int) (deltaX -.05)));
- }
- c.drawPath(path, paint);
- mImage.setImageBitmap(b);
- }
-
- public void finish() {
- mUiHandler.post(new Runnable() {
- @Override
- public void run() {
- mSoundIndicator.stop();
- }
- });
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java b/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
deleted file mode 100644
index 855a09a1d..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/SettingsUtil.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import android.content.ContentResolver;
-import android.provider.Settings;
-
-/**
- * Utility for retrieving settings from Settings.Secure.
- */
-public class SettingsUtil {
- /**
- * A whitespace-separated list of supported locales for voice input from the keyboard.
- */
- public static final String LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES =
- "latin_ime_voice_input_supported_locales";
-
- /**
- * A whitespace-separated list of recommended app packages for voice input from the
- * keyboard.
- */
- public static final String LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES =
- "latin_ime_voice_input_recommended_packages";
-
- /**
- * The maximum number of unique days to show the swipe hint for voice input.
- */
- public static final String LATIN_IME_VOICE_INPUT_SWIPE_HINT_MAX_DAYS =
- "latin_ime_voice_input_swipe_hint_max_days";
-
- /**
- * The maximum number of times to show the punctuation hint for voice input.
- */
- public static final String LATIN_IME_VOICE_INPUT_PUNCTUATION_HINT_MAX_DISPLAYS =
- "latin_ime_voice_input_punctuation_hint_max_displays";
-
- /**
- * Endpointer parameters for voice input from the keyboard.
- */
- public static final String LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS =
- "latin_ime_speech_minimum_length_millis";
- public static final String LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
- "latin_ime_speech_input_complete_silence_length_millis";
- public static final String LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
- "latin_ime_speech_input_possibly_complete_silence_length_millis";
-
- /**
- * Min and max volume levels that can be displayed on the "speak now" screen.
- */
- public static final String LATIN_IME_MIN_MICROPHONE_LEVEL =
- "latin_ime_min_microphone_level";
- public static final String LATIN_IME_MAX_MICROPHONE_LEVEL =
- "latin_ime_max_microphone_level";
-
- /**
- * The number of sentence-level alternates to request of the server.
- */
- public static final String LATIN_IME_MAX_VOICE_RESULTS = "latin_ime_max_voice_results";
-
- /**
- * Get a string-valued setting.
- *
- * @param cr The content resolver to use
- * @param key The setting to look up
- * @param defaultValue The default value to use if none can be found
- * @return The value of the setting, or defaultValue if it couldn't be found
- */
- public static String getSettingsString(ContentResolver cr, String key, String defaultValue) {
- String result = Settings.Secure.getString(cr, key);
- return (result == null) ? defaultValue : result;
- }
-
- /**
- * Get an int-valued setting.
- *
- * @param cr The content resolver to use
- * @param key The setting to look up
- * @param defaultValue The default value to use if the setting couldn't be found or parsed
- * @return The value of the setting, or defaultValue if it couldn't be found or parsed
- */
- public static int getSettingsInt(ContentResolver cr, String key, int defaultValue) {
- return Settings.Secure.getInt(cr, key, defaultValue);
- }
-
- /**
- * Get a float-valued setting.
- *
- * @param cr The content resolver to use
- * @param key The setting to look up
- * @param defaultValue The default value to use if the setting couldn't be found or parsed
- * @return The value of the setting, or defaultValue if it couldn't be found or parsed
- */
- public static float getSettingsFloat(ContentResolver cr, String key, float defaultValue) {
- return Settings.Secure.getFloat(cr, key, defaultValue);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java b/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java
deleted file mode 100644
index 25b314085..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/SoundIndicator.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2011 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.deprecated.voice;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Handler;
-import android.util.AttributeSet;
-import android.widget.ImageView;
-
-import com.android.inputmethod.latin.R;
-
-/**
- * A widget which shows the volume of audio using a microphone icon
- */
-public class SoundIndicator extends ImageView {
- @SuppressWarnings("unused")
- private static final String TAG = "SoundIndicator";
-
- private static final float UP_SMOOTHING_FACTOR = 0.9f;
- private static final float DOWN_SMOOTHING_FACTOR = 0.4f;
-
- private static final float AUDIO_METER_MIN_DB = 7.0f;
- private static final float AUDIO_METER_DB_RANGE = 20.0f;
-
- private static final long FRAME_DELAY = 50;
-
- private Bitmap mDrawingBuffer;
- private Canvas mBufferCanvas;
- private Bitmap mEdgeBitmap;
- private float mLevel = 0.0f;
- private Drawable mFrontDrawable;
- private Paint mClearPaint;
- private Paint mMultPaint;
- private int mEdgeBitmapOffset;
-
- private Handler mHandler;
-
- private Runnable mDrawFrame = new Runnable() {
- public void run() {
- invalidate();
- mHandler.postDelayed(mDrawFrame, FRAME_DELAY);
- }
- };
-
- public SoundIndicator(Context context) {
- this(context, null);
- }
-
- public SoundIndicator(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mFrontDrawable = getDrawable();
- BitmapDrawable edgeDrawable =
- (BitmapDrawable) context.getResources().getDrawable(R.drawable.vs_popup_mic_edge);
- mEdgeBitmap = edgeDrawable.getBitmap();
- mEdgeBitmapOffset = mEdgeBitmap.getHeight() / 2;
-
- mDrawingBuffer =
- Bitmap.createBitmap(mFrontDrawable.getIntrinsicWidth(),
- mFrontDrawable.getIntrinsicHeight(), Config.ARGB_8888);
-
- mBufferCanvas = new Canvas(mDrawingBuffer);
-
- // Initialize Paints.
- mClearPaint = new Paint();
- mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-
- mMultPaint = new Paint();
- mMultPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
-
- mHandler = new Handler();
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- //super.onDraw(canvas);
-
- float w = getWidth();
- float h = getHeight();
-
- // Clear the buffer canvas
- mBufferCanvas.drawRect(0, 0, w, h, mClearPaint);
-
- // Set its clip so we don't draw the front image all the way to the top
- Rect clip = new Rect(0,
- (int) ((1.0 - mLevel) * (h + mEdgeBitmapOffset)) - mEdgeBitmapOffset,
- (int) w,
- (int) h);
-
- mBufferCanvas.save();
- mBufferCanvas.clipRect(clip);
-
- // Draw the front image
- mFrontDrawable.setBounds(new Rect(0, 0, (int) w, (int) h));
- mFrontDrawable.draw(mBufferCanvas);
-
- mBufferCanvas.restore();
-
- // Draw the edge image on top of the buffer image with a multiply mode
- mBufferCanvas.drawBitmap(mEdgeBitmap, 0, clip.top, mMultPaint);
-
- // Draw the buffer image (on top of the background image)
- canvas.drawBitmap(mDrawingBuffer, 0, 0, null);
- }
-
- /**
- * Sets the sound level
- *
- * @param rmsdB The level of the sound, in dB.
- */
- public void setRmsdB(float rmsdB) {
- float level = ((rmsdB - AUDIO_METER_MIN_DB) / AUDIO_METER_DB_RANGE);
-
- level = Math.min(Math.max(0.0f, level), 1.0f);
-
- // We smooth towards the new level
- if (level > mLevel) {
- mLevel = (level - mLevel) * UP_SMOOTHING_FACTOR + mLevel;
- } else {
- mLevel = (level - mLevel) * DOWN_SMOOTHING_FACTOR + mLevel;
- }
- invalidate();
- }
-
- public void start() {
- mHandler.post(mDrawFrame);
- }
-
- public void stop() {
- mHandler.removeCallbacks(mDrawFrame);
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
deleted file mode 100644
index 8969a2168..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/VoiceInput.java
+++ /dev/null
@@ -1,692 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import com.android.inputmethod.latin.EditingUtils;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Message;
-import android.os.Parcelable;
-import android.speech.RecognitionListener;
-import android.speech.RecognizerIntent;
-import android.speech.SpeechRecognizer;
-import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.inputmethod.InputConnection;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-/**
- * Speech recognition input, including both user interface and a background
- * process to stream audio to the network recognizer. This class supplies a
- * View (getView()), which it updates as recognition occurs. The user of this
- * class is responsible for making the view visible to the user, as well as
- * handling various events returned through UiListener.
- */
-public class VoiceInput implements OnClickListener {
- private static final String TAG = "VoiceInput";
- private static final String EXTRA_RECOGNITION_CONTEXT =
- "android.speech.extras.RECOGNITION_CONTEXT";
- private static final String EXTRA_CALLING_PACKAGE = "calling_package";
- private static final String EXTRA_ALTERNATES = "android.speech.extra.ALTERNATES";
- private static final int MAX_ALT_LIST_LENGTH = 6;
- private static boolean DBG = LatinImeLogger.sDBG;
-
- private static final String DEFAULT_RECOMMENDED_PACKAGES =
- "com.android.mms " +
- "com.google.android.gm " +
- "com.google.android.talk " +
- "com.google.android.apps.googlevoice " +
- "com.android.email " +
- "com.android.browser ";
-
- // WARNING! Before enabling this, fix the problem with calling getExtractedText() in
- // landscape view. It causes Extracted text updates to be rejected due to a token mismatch
- public static boolean ENABLE_WORD_CORRECTIONS = true;
-
- // Dummy word suggestion which means "delete current word"
- public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol
-
- private Whitelist mRecommendedList;
- private Whitelist mBlacklist;
-
- private VoiceInputLogger mLogger;
-
- // Names of a few extras defined in VoiceSearch's RecognitionController
- // Note, the version of voicesearch that shipped in Froyo returns the raw
- // RecognitionClientAlternates protocol buffer under the key "alternates",
- // so a VS market update must be installed on Froyo devices in order to see
- // alternatives.
- private static final String ALTERNATES_BUNDLE = "alternates_bundle";
-
- // This is copied from the VoiceSearch app.
- @SuppressWarnings("unused")
- private static final class AlternatesBundleKeys {
- public static final String ALTERNATES = "alternates";
- public static final String CONFIDENCE = "confidence";
- public static final String LENGTH = "length";
- public static final String MAX_SPAN_LENGTH = "max_span_length";
- public static final String SPANS = "spans";
- public static final String SPAN_KEY_DELIMITER = ":";
- public static final String START = "start";
- public static final String TEXT = "text";
- }
-
- // Names of a few intent extras defined in VoiceSearch's RecognitionService.
- // These let us tweak the endpointer parameters.
- private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS =
- "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS";
- private static final String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
- "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS";
- private static final String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
- "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS";
-
- // The usual endpointer default value for input complete silence length is 0.5 seconds,
- // but that's used for things like voice search. For dictation-like voice input like this,
- // we go with a more liberal value of 1 second. This value will only be used if a value
- // is not provided from Gservices.
- private static final String INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS = "1000";
-
- // Used to record part of that state for logging purposes.
- public static final int DEFAULT = 0;
- public static final int LISTENING = 1;
- public static final int WORKING = 2;
- public static final int ERROR = 3;
-
- private int mAfterVoiceInputDeleteCount = 0;
- private int mAfterVoiceInputInsertCount = 0;
- private int mAfterVoiceInputInsertPunctuationCount = 0;
- private int mAfterVoiceInputCursorPos = 0;
- private int mAfterVoiceInputSelectionSpan = 0;
-
- private int mState = DEFAULT;
-
- private final static int MSG_RESET = 1;
-
- private final UIHandler mHandler = new UIHandler(this);
-
- private static class UIHandler extends StaticInnerHandlerWrapper<VoiceInput> {
- public UIHandler(VoiceInput outerInstance) {
- super(outerInstance);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (msg.what == MSG_RESET) {
- final VoiceInput voiceInput = getOuterInstance();
- voiceInput.mState = DEFAULT;
- voiceInput.mRecognitionView.finish();
- voiceInput.mUiListener.onCancelVoice();
- }
- }
- };
-
- /**
- * Events relating to the recognition UI. You must implement these.
- */
- public interface UiListener {
-
- /**
- * @param recognitionResults a set of transcripts for what the user
- * spoke, sorted by likelihood.
- */
- public void onVoiceResults(
- List<String> recognitionResults,
- Map<String, List<CharSequence>> alternatives);
-
- /**
- * Called when the user cancels speech recognition.
- */
- public void onCancelVoice();
- }
-
- private SpeechRecognizer mSpeechRecognizer;
- private RecognitionListener mRecognitionListener;
- private RecognitionView mRecognitionView;
- private UiListener mUiListener;
- private Context mContext;
-
- /**
- * @param context the service or activity in which we're running.
- * @param uiHandler object to receive events from VoiceInput.
- */
- public VoiceInput(Context context, UiListener uiHandler) {
- mLogger = VoiceInputLogger.getLogger(context);
- mRecognitionListener = new ImeRecognitionListener();
- mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
- mSpeechRecognizer.setRecognitionListener(mRecognitionListener);
- mUiListener = uiHandler;
- mContext = context;
- newView();
-
- String recommendedPackages = SettingsUtil.getSettingsString(
- context.getContentResolver(),
- SettingsUtil.LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES,
- DEFAULT_RECOMMENDED_PACKAGES);
-
- mRecommendedList = new Whitelist();
- for (String recommendedPackage : recommendedPackages.split("\\s+")) {
- mRecommendedList.addApp(recommendedPackage);
- }
-
- mBlacklist = new Whitelist();
- mBlacklist.addApp("com.google.android.setupwizard");
- }
-
- public void setCursorPos(int pos) {
- mAfterVoiceInputCursorPos = pos;
- }
-
- public int getCursorPos() {
- return mAfterVoiceInputCursorPos;
- }
-
- public void setSelectionSpan(int span) {
- mAfterVoiceInputSelectionSpan = span;
- }
-
- public int getSelectionSpan() {
- return mAfterVoiceInputSelectionSpan;
- }
-
- public void incrementTextModificationDeleteCount(int count){
- mAfterVoiceInputDeleteCount += count;
- // Send up intents for other text modification types
- if (mAfterVoiceInputInsertCount > 0) {
- logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
- mAfterVoiceInputInsertCount = 0;
- }
- if (mAfterVoiceInputInsertPunctuationCount > 0) {
- logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
- mAfterVoiceInputInsertPunctuationCount = 0;
- }
-
- }
-
- public void incrementTextModificationInsertCount(int count){
- mAfterVoiceInputInsertCount += count;
- if (mAfterVoiceInputSelectionSpan > 0) {
- // If text was highlighted before inserting the char, count this as
- // a delete.
- mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
- }
- // Send up intents for other text modification types
- if (mAfterVoiceInputDeleteCount > 0) {
- logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
- mAfterVoiceInputDeleteCount = 0;
- }
- if (mAfterVoiceInputInsertPunctuationCount > 0) {
- logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
- mAfterVoiceInputInsertPunctuationCount = 0;
- }
- }
-
- public void incrementTextModificationInsertPunctuationCount(int count){
- mAfterVoiceInputInsertPunctuationCount += count;
- if (mAfterVoiceInputSelectionSpan > 0) {
- // If text was highlighted before inserting the char, count this as
- // a delete.
- mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
- }
- // Send up intents for aggregated non-punctuation insertions
- if (mAfterVoiceInputDeleteCount > 0) {
- logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
- mAfterVoiceInputDeleteCount = 0;
- }
- if (mAfterVoiceInputInsertCount > 0) {
- logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
- mAfterVoiceInputInsertCount = 0;
- }
- }
-
- public void flushAllTextModificationCounters() {
- if (mAfterVoiceInputInsertCount > 0) {
- logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
- mAfterVoiceInputInsertCount = 0;
- }
- if (mAfterVoiceInputDeleteCount > 0) {
- logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
- mAfterVoiceInputDeleteCount = 0;
- }
- if (mAfterVoiceInputInsertPunctuationCount > 0) {
- logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
- mAfterVoiceInputInsertPunctuationCount = 0;
- }
- }
-
- /**
- * The configuration of the IME changed and may have caused the views to be layed out
- * again. Restore the state of the recognition view.
- */
- public void onConfigurationChanged(Configuration configuration) {
- mRecognitionView.restoreState();
- mRecognitionView.getView().dispatchConfigurationChanged(configuration);
- }
-
- /**
- * @return true if field is blacklisted for voice
- */
- public boolean isBlacklistedField(FieldContext context) {
- return mBlacklist.matches(context);
- }
-
- /**
- * Used to decide whether to show voice input hints for this field, etc.
- *
- * @return true if field is recommended for voice
- */
- public boolean isRecommendedField(FieldContext context) {
- return mRecommendedList.matches(context);
- }
-
- /**
- * Start listening for speech from the user. This will grab the microphone
- * and start updating the view provided by getView(). It is the caller's
- * responsibility to ensure that the view is visible to the user at this stage.
- *
- * @param context the same FieldContext supplied to voiceIsEnabled()
- * @param swipe whether this voice input was started by swipe, for logging purposes
- */
- public void startListening(FieldContext context, boolean swipe) {
- if (DBG) {
- Log.d(TAG, "startListening: " + context);
- }
-
- if (mState != DEFAULT) {
- Log.w(TAG, "startListening in the wrong status " + mState);
- }
-
- // If everything works ok, the voice input should be already in the correct state. As this
- // class can be called by third-party, we call reset just to be on the safe side.
- reset();
-
- Locale locale = Locale.getDefault();
- String localeString = locale.getLanguage() + "-" + locale.getCountry();
-
- mLogger.start(localeString, swipe);
-
- mState = LISTENING;
-
- mRecognitionView.showInitializing();
- startListeningAfterInitialization(context);
- }
-
- /**
- * Called only when the recognition manager's initialization completed
- *
- * @param context context with which {@link #startListening(FieldContext, boolean)} was executed
- */
- private void startListeningAfterInitialization(FieldContext context) {
- Intent intent = makeIntent();
- intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
- intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
- intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
- intent.putExtra(EXTRA_ALTERNATES, true);
- intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
- SettingsUtil.getSettingsInt(
- mContext.getContentResolver(),
- SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
- 1));
- // Get endpointer params from Gservices.
- // TODO: Consider caching these values for improved performance on slower devices.
- final ContentResolver cr = mContext.getContentResolver();
- putEndpointerExtra(
- cr,
- intent,
- SettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
- EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
- null /* rely on endpointer default */);
- putEndpointerExtra(
- cr,
- intent,
- SettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
- EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
- INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
- /* our default value is different from the endpointer's */);
- putEndpointerExtra(
- cr,
- intent,
- SettingsUtil.
- LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
- EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
- null /* rely on endpointer default */);
-
- mSpeechRecognizer.startListening(intent);
- }
-
- /**
- * Gets the value of the provided Gservices key, attempts to parse it into a long,
- * and if successful, puts the long value as an extra in the provided intent.
- */
- private void putEndpointerExtra(ContentResolver cr, Intent i,
- String gservicesKey, String intentExtraKey, String defaultValue) {
- long l = -1;
- String s = SettingsUtil.getSettingsString(cr, gservicesKey, defaultValue);
- if (s != null) {
- try {
- l = Long.valueOf(s);
- } catch (NumberFormatException e) {
- Log.e(TAG, "could not parse value for " + gservicesKey + ": " + s);
- }
- }
-
- if (l != -1) i.putExtra(intentExtraKey, l);
- }
-
- public void destroy() {
- mSpeechRecognizer.destroy();
- }
-
- /**
- * Creates a new instance of the view that is returned by {@link #getView()}
- * Clients should use this when a previously returned view is stuck in a
- * layout that is being thrown away and a new one is need to show to the
- * user.
- */
- public void newView() {
- mRecognitionView = new RecognitionView(mContext, this);
- }
-
- /**
- * @return a view that shows the recognition flow--e.g., "Speak now" and
- * "working" dialogs.
- */
- public View getView() {
- return mRecognitionView.getView();
- }
-
- /**
- * Handle the cancel button.
- */
- @Override
- public void onClick(View view) {
- switch(view.getId()) {
- case R.id.button:
- cancel();
- break;
- }
- }
-
- public void logTextModifiedByTypingInsertion(int length) {
- mLogger.textModifiedByTypingInsertion(length);
- }
-
- public void logTextModifiedByTypingInsertionPunctuation(int length) {
- mLogger.textModifiedByTypingInsertionPunctuation(length);
- }
-
- public void logTextModifiedByTypingDeletion(int length) {
- mLogger.textModifiedByTypingDeletion(length);
- }
-
- public void logTextModifiedByChooseSuggestion(String suggestion, int index,
- String wordSeparators, InputConnection ic) {
- String wordToBeReplaced = EditingUtils.getWordAtCursor(ic, wordSeparators);
- // If we enable phrase-based alternatives, only send up the first word
- // in suggestion and wordToBeReplaced.
- mLogger.textModifiedByChooseSuggestion(suggestion.length(), wordToBeReplaced.length(),
- index, wordToBeReplaced, suggestion);
- }
-
- public void logKeyboardWarningDialogShown() {
- mLogger.keyboardWarningDialogShown();
- }
-
- public void logKeyboardWarningDialogDismissed() {
- mLogger.keyboardWarningDialogDismissed();
- }
-
- public void logKeyboardWarningDialogOk() {
- mLogger.keyboardWarningDialogOk();
- }
-
- public void logKeyboardWarningDialogCancel() {
- mLogger.keyboardWarningDialogCancel();
- }
-
- public void logSwipeHintDisplayed() {
- mLogger.swipeHintDisplayed();
- }
-
- public void logPunctuationHintDisplayed() {
- mLogger.punctuationHintDisplayed();
- }
-
- public void logVoiceInputDelivered(int length) {
- mLogger.voiceInputDelivered(length);
- }
-
- public void logInputEnded() {
- mLogger.inputEnded();
- }
-
- public void flushLogs() {
- mLogger.flush();
- }
-
- private static Intent makeIntent() {
- Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
-
- // On Cupcake, use VoiceIMEHelper since VoiceSearch doesn't support.
- // On Donut, always use VoiceSearch, since VoiceIMEHelper and
- // VoiceSearch may conflict.
- if (Build.VERSION.RELEASE.equals("1.5")) {
- intent = intent.setClassName(
- "com.google.android.voiceservice",
- "com.google.android.voiceservice.IMERecognitionService");
- } else {
- intent = intent.setClassName(
- "com.google.android.voicesearch",
- "com.google.android.voicesearch.RecognitionService");
- }
-
- return intent;
- }
-
- /**
- * Reset the current voice recognition.
- */
- public void reset() {
- if (mState != DEFAULT) {
- mState = DEFAULT;
-
- // Remove all pending tasks (e.g., timers to cancel voice input)
- mHandler.removeMessages(MSG_RESET);
-
- mSpeechRecognizer.cancel();
- mRecognitionView.finish();
- }
- }
-
- /**
- * Cancel in-progress speech recognition.
- */
- public void cancel() {
- switch (mState) {
- case LISTENING:
- mLogger.cancelDuringListening();
- break;
- case WORKING:
- mLogger.cancelDuringWorking();
- break;
- case ERROR:
- mLogger.cancelDuringError();
- break;
- }
-
- reset();
- mUiListener.onCancelVoice();
- }
-
- private int getErrorStringId(int errorType, boolean endpointed) {
- switch (errorType) {
- // We use CLIENT_ERROR to signify that voice search is not available on the device.
- case SpeechRecognizer.ERROR_CLIENT:
- return R.string.voice_not_installed;
- case SpeechRecognizer.ERROR_NETWORK:
- return R.string.voice_network_error;
- case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
- return endpointed ?
- R.string.voice_network_error : R.string.voice_too_much_speech;
- case SpeechRecognizer.ERROR_AUDIO:
- return R.string.voice_audio_error;
- case SpeechRecognizer.ERROR_SERVER:
- return R.string.voice_server_error;
- case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
- return R.string.voice_speech_timeout;
- case SpeechRecognizer.ERROR_NO_MATCH:
- return R.string.voice_no_match;
- default: return R.string.voice_error;
- }
- }
-
- private void onError(int errorType, boolean endpointed) {
- Log.i(TAG, "error " + errorType);
- mLogger.error(errorType);
- onError(mContext.getString(getErrorStringId(errorType, endpointed)));
- }
-
- private void onError(String error) {
- mState = ERROR;
- mRecognitionView.showError(error);
- // Wait a couple seconds and then automatically dismiss message.
- mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_RESET), 2000);
- }
-
- private class ImeRecognitionListener implements RecognitionListener {
- // Waveform data
- final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
- int mSpeechStart;
- private boolean mEndpointed = false;
-
- @Override
- public void onReadyForSpeech(Bundle noiseParams) {
- mRecognitionView.showListening();
- }
-
- @Override
- public void onBeginningOfSpeech() {
- mEndpointed = false;
- mSpeechStart = mWaveBuffer.size();
- }
-
- @Override
- public void onRmsChanged(float rmsdB) {
- mRecognitionView.updateVoiceMeter(rmsdB);
- }
-
- @Override
- public void onBufferReceived(byte[] buf) {
- try {
- mWaveBuffer.write(buf);
- } catch (IOException e) {
- // ignore.
- }
- }
-
- @Override
- public void onEndOfSpeech() {
- mEndpointed = true;
- mState = WORKING;
- mRecognitionView.showWorking(mWaveBuffer, mSpeechStart, mWaveBuffer.size());
- }
-
- @Override
- public void onError(int errorType) {
- mState = ERROR;
- VoiceInput.this.onError(errorType, mEndpointed);
- }
-
- @Override
- public void onResults(Bundle resultsBundle) {
- List<String> results = resultsBundle
- .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
- // VS Market update is needed for IME froyo clients to access the alternatesBundle
- // TODO: verify this.
- Bundle alternatesBundle = resultsBundle.getBundle(ALTERNATES_BUNDLE);
- mState = DEFAULT;
-
- final Map<String, List<CharSequence>> alternatives =
- new HashMap<String, List<CharSequence>>();
-
- if (ENABLE_WORD_CORRECTIONS && alternatesBundle != null && results.size() > 0) {
- // Use the top recognition result to map each alternative's start:length to a word.
- String[] words = results.get(0).split(" ");
- Bundle spansBundle = alternatesBundle.getBundle(AlternatesBundleKeys.SPANS);
- for (String key : spansBundle.keySet()) {
- // Get the word for which these alternates correspond to.
- Bundle spanBundle = spansBundle.getBundle(key);
- int start = spanBundle.getInt(AlternatesBundleKeys.START);
- int length = spanBundle.getInt(AlternatesBundleKeys.LENGTH);
- // Only keep single-word based alternatives.
- if (length == 1 && start < words.length) {
- // Get the alternatives associated with the span.
- // If a word appears twice in a recognition result,
- // concatenate the alternatives for the word.
- List<CharSequence> altList = alternatives.get(words[start]);
- if (altList == null) {
- altList = new ArrayList<CharSequence>();
- alternatives.put(words[start], altList);
- }
- Parcelable[] alternatesArr = spanBundle
- .getParcelableArray(AlternatesBundleKeys.ALTERNATES);
- for (int j = 0; j < alternatesArr.length &&
- altList.size() < MAX_ALT_LIST_LENGTH; j++) {
- Bundle alternateBundle = (Bundle) alternatesArr[j];
- String alternate = alternateBundle.getString(AlternatesBundleKeys.TEXT);
- // Don't allow duplicates in the alternates list.
- if (!altList.contains(alternate)) {
- altList.add(alternate);
- }
- }
- }
- }
- }
-
- if (results.size() > 5) {
- results = results.subList(0, 5);
- }
- mUiListener.onVoiceResults(results, alternatives);
- mRecognitionView.finish();
- }
-
- @Override
- public void onPartialResults(final Bundle partialResults) {
- // currently - do nothing
- }
-
- @Override
- public void onEvent(int eventType, Bundle params) {
- // do nothing - reserved for events that might be added in the future
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java b/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
deleted file mode 100644
index 22e8207bf..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/VoiceInputLogger.java
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Copyright (C) 2008 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.deprecated.voice;
-
-import com.android.common.speech.LoggingEvents;
-import com.android.inputmethod.deprecated.compat.VoiceInputLoggerCompatUtils;
-
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * Provides the logging facility for voice input events. This fires broadcasts back to
- * the voice search app which then logs on our behalf.
- *
- * Note that debug console logging does not occur in this class. If you want to
- * see console output of these logging events, there is a boolean switch to turn
- * on on the VoiceSearch side.
- */
-public class VoiceInputLogger {
- @SuppressWarnings("unused")
- private static final String TAG = VoiceInputLogger.class.getSimpleName();
-
- private static VoiceInputLogger sVoiceInputLogger;
-
- private final Context mContext;
-
- // The base intent used to form all broadcast intents to the logger
- // in VoiceSearch.
- private final Intent mBaseIntent;
-
- // This flag is used to indicate when there are voice events that
- // need to be flushed.
- private boolean mHasLoggingInfo = false;
-
- /**
- * Returns the singleton of the logger.
- *
- * @param contextHint a hint context used when creating the logger instance.
- * Ignored if the singleton instance already exists.
- */
- public static synchronized VoiceInputLogger getLogger(Context contextHint) {
- if (sVoiceInputLogger == null) {
- sVoiceInputLogger = new VoiceInputLogger(contextHint);
- }
- return sVoiceInputLogger;
- }
-
- public VoiceInputLogger(Context context) {
- mContext = context;
-
- mBaseIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT);
- mBaseIntent.putExtra(LoggingEvents.EXTRA_APP_NAME, LoggingEvents.VoiceIme.APP_NAME);
- }
-
- private Intent newLoggingBroadcast(int event) {
- Intent i = new Intent(mBaseIntent);
- i.putExtra(LoggingEvents.EXTRA_EVENT, event);
- return i;
- }
-
- public void flush() {
- if (hasLoggingInfo()) {
- Intent i = new Intent(mBaseIntent);
- i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
- mContext.sendBroadcast(i);
- setHasLoggingInfo(false);
- }
- }
-
- public void keyboardWarningDialogShown() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_SHOWN));
- }
-
- public void keyboardWarningDialogDismissed() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_DISMISSED));
- }
-
- public void keyboardWarningDialogOk() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_OK));
- }
-
- public void keyboardWarningDialogCancel() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.KEYBOARD_WARNING_DIALOG_CANCEL));
- }
-
- public void settingsWarningDialogShown() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_SHOWN));
- }
-
- public void settingsWarningDialogDismissed() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_DISMISSED));
- }
-
- public void settingsWarningDialogOk() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_OK));
- }
-
- public void settingsWarningDialogCancel() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.SETTINGS_WARNING_DIALOG_CANCEL));
- }
-
- public void swipeHintDisplayed() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.SWIPE_HINT_DISPLAYED));
- }
-
- public void cancelDuringListening() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_LISTENING));
- }
-
- public void cancelDuringWorking() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_WORKING));
- }
-
- public void cancelDuringError() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.CANCEL_DURING_ERROR));
- }
-
- public void punctuationHintDisplayed() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.PUNCTUATION_HINT_DISPLAYED));
- }
-
- public void error(int code) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.ERROR);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_ERROR_CODE, code);
- mContext.sendBroadcast(i);
- }
-
- public void start(String locale, boolean swipe) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.START);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_LOCALE, locale);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_START_SWIPE, swipe);
- i.putExtra(LoggingEvents.EXTRA_TIMESTAMP, System.currentTimeMillis());
- mContext.sendBroadcast(i);
- }
-
- public void voiceInputDelivered(int length) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.VOICE_INPUT_DELIVERED);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
- mContext.sendBroadcast(i);
- }
-
- public void textModifiedByTypingInsertion(int length) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
- LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_INSERTION);
- mContext.sendBroadcast(i);
- }
-
- public void textModifiedByTypingInsertionPunctuation(int length) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
- LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_INSERTION_PUNCTUATION);
- mContext.sendBroadcast(i);
- }
-
- public void textModifiedByTypingDeletion(int length) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, length);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
- LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_TYPING_DELETION);
-
- mContext.sendBroadcast(i);
- }
-
-
- public void textModifiedByChooseSuggestion(int suggestionLength, int replacedPhraseLength,
- int index, String before, String after) {
- setHasLoggingInfo(true);
- Intent i = newLoggingBroadcast(LoggingEvents.VoiceIme.TEXT_MODIFIED);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_LENGTH, suggestionLength);
- i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_TEXT_REPLACED_LENGTH, replacedPhraseLength);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_TEXT_MODIFIED_TYPE,
- LoggingEvents.VoiceIme.TEXT_MODIFIED_TYPE_CHOOSE_SUGGESTION);
- i.putExtra(LoggingEvents.VoiceIme.EXTRA_N_BEST_CHOOSE_INDEX, index);
- i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_BEFORE_N_BEST_CHOOSE, before);
- i.putExtra(VoiceInputLoggerCompatUtils.EXTRA_AFTER_N_BEST_CHOOSE, after);
- mContext.sendBroadcast(i);
- }
-
- public void inputEnded() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(LoggingEvents.VoiceIme.INPUT_ENDED));
- }
-
- public void voiceInputSettingEnabled() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_ENABLED));
- }
-
- public void voiceInputSettingDisabled() {
- setHasLoggingInfo(true);
- mContext.sendBroadcast(newLoggingBroadcast(
- LoggingEvents.VoiceIme.VOICE_INPUT_SETTING_DISABLED));
- }
-
- private void setHasLoggingInfo(boolean hasLoggingInfo) {
- mHasLoggingInfo = hasLoggingInfo;
- // If applications that call UserHappinessSignals.userAcceptedImeText
- // make that call after VoiceInputLogger.flush() calls this method with false, we
- // will lose those happiness signals. For example, consider the gmail sequence:
- // 1. compose message
- // 2. speak message into message field
- // 3. type subject into subject field
- // 4. press send
- // We will NOT get the signal that the user accepted the voice inputted message text
- // because when the user tapped on the subject field, the ime's flush will be triggered
- // and the hasLoggingInfo will be then set to false. So by the time the user hits send
- // we have essentially forgotten about any voice input.
- // However the following (more common) use case is properly logged
- // 1. compose message
- // 2. type subject in subject field
- // 3. speak message in message field
- // 4. press send
- VoiceInputLoggerCompatUtils.setHasVoiceLoggingInfoCompat(hasLoggingInfo);
- }
-
- private boolean hasLoggingInfo(){
- return mHasLoggingInfo;
- }
-
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java b/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
deleted file mode 100644
index 8ed279f42..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/WaveformImage.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2008-2009 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.deprecated.voice;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-
-import java.io.ByteArrayOutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.ShortBuffer;
-
-/**
- * Utility class to draw a waveform into a bitmap, given a byte array
- * that represents the waveform as a sequence of 16-bit integers.
- * Adapted from RecognitionActivity.java.
- */
-public class WaveformImage {
- private static final int SAMPLING_RATE = 8000;
-
- private WaveformImage() {
- // Intentional empty constructor.
- }
-
- public static Bitmap drawWaveform(
- ByteArrayOutputStream waveBuffer, int w, int h, int start, int end) {
- final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- final Canvas c = new Canvas(b);
- final Paint paint = new Paint();
- paint.setColor(0xFFFFFFFF); // 0xRRGGBBAA
- paint.setAntiAlias(true);
- paint.setStrokeWidth(0);
-
- final ShortBuffer buf = ByteBuffer
- .wrap(waveBuffer.toByteArray())
- .order(ByteOrder.nativeOrder())
- .asShortBuffer();
- buf.position(0);
-
- final int numSamples = waveBuffer.size() / 2;
- final int delay = (SAMPLING_RATE * 100 / 1000);
- int endIndex = end / 2 + delay;
- if (end == 0 || endIndex >= numSamples) {
- endIndex = numSamples;
- }
- int index = start / 2 - delay;
- if (index < 0) {
- index = 0;
- }
- final int size = endIndex - index;
- int numSamplePerPixel = 32;
- int delta = size / (numSamplePerPixel * w);
- if (delta == 0) {
- numSamplePerPixel = size / w;
- delta = 1;
- }
-
- final float scale = 3.5f / 65536.0f;
- // do one less column to make sure we won't read past
- // the buffer.
- try {
- for (int i = 0; i < w - 1 ; i++) {
- final float x = i;
- for (int j = 0; j < numSamplePerPixel; j++) {
- final short s = buf.get(index);
- final float y = (h / 2) - (s * h * scale);
- c.drawPoint(x, y, paint);
- index += delta;
- }
- }
- } catch (IndexOutOfBoundsException e) {
- // this can happen, but we don't care
- }
-
- return b;
- }
-}
diff --git a/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java b/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
deleted file mode 100644
index 6c5f52ae2..000000000
--- a/java/src/com/android/inputmethod/deprecated/voice/Whitelist.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2009 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.deprecated.voice;
-
-import android.os.Bundle;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A set of text fields where speech has been explicitly enabled.
- */
-public class Whitelist {
- private List<Bundle> mConditions;
-
- public Whitelist() {
- mConditions = new ArrayList<Bundle>();
- }
-
- public Whitelist(List<Bundle> conditions) {
- this.mConditions = conditions;
- }
-
- public void addApp(String app) {
- Bundle bundle = new Bundle();
- bundle.putString("packageName", app);
- mConditions.add(bundle);
- }
-
- /**
- * @return true if the field is a member of the whitelist.
- */
- public boolean matches(FieldContext context) {
- for (Bundle condition : mConditions) {
- if (matches(condition, context.getBundle())) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * @return true of all values in condition are matched by a value
- * in target.
- */
- private boolean matches(Bundle condition, Bundle target) {
- for (String key : condition.keySet()) {
- if (!condition.getString(key).equals(target.getString(key))) {
- return false;
- }
- }
- return true;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 872fbf823..e1e1ca9cf 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -16,385 +16,574 @@
package com.android.inputmethod.keyboard;
+import static com.android.inputmethod.keyboard.Keyboard.CODE_OUTPUT_TEXT;
+import static com.android.inputmethod.keyboard.Keyboard.CODE_SHIFT;
+import static com.android.inputmethod.keyboard.Keyboard.CODE_SWITCH_ALPHA_SYMBOL;
+import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED;
+import static com.android.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED;
+
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
+import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
+import android.util.Log;
import android.util.Xml;
-import com.android.inputmethod.keyboard.internal.KeyStyles;
+import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
-import com.android.inputmethod.keyboard.internal.KeyboardParser;
-import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException;
-import com.android.inputmethod.keyboard.internal.PopupCharactersParser;
-import com.android.inputmethod.keyboard.internal.Row;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.StringUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
-import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
/**
* Class for describing the position and characteristics of a single key in the keyboard.
*/
public class Key {
+ private static final String TAG = Key.class.getSimpleName();
+
/**
* The key code (unicode or custom code) that this key generates.
*/
public final int mCode;
+ public final int mAltCode;
/** Label to display */
- public final CharSequence mLabel;
+ public final String mLabel;
/** Hint label to display on the key in conjunction with the label */
- public final CharSequence mHintLabel;
- /** Option of the label */
- public final int mLabelOption;
- public static final int LABEL_OPTION_ALIGN_LEFT = 0x01;
- public static final int LABEL_OPTION_ALIGN_RIGHT = 0x02;
- public static final int LABEL_OPTION_ALIGN_BOTTOM = 0x04;
- public static final int LABEL_OPTION_ALIGN_LEFT_OF_CENTER = 0x08;
- private static final int LABEL_OPTION_LARGE_LETTER = 0x10;
- private static final int LABEL_OPTION_FONT_NORMAL = 0x20;
- private static final int LABEL_OPTION_FONT_MONO_SPACE = 0x40;
- private static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x80;
- private static final int LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO = 0x100;
- private static final int LABEL_OPTION_HAS_POPUP_HINT = 0x200;
- private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x400;
- private static final int LABEL_OPTION_HAS_HINT_LABEL = 0x800;
+ public final String mHintLabel;
+ /** Flags of the label */
+ private final int mLabelFlags;
+ private static final int LABEL_FLAGS_ALIGN_LEFT = 0x01;
+ private static final int LABEL_FLAGS_ALIGN_RIGHT = 0x02;
+ private static final int LABEL_FLAGS_ALIGN_LEFT_OF_CENTER = 0x08;
+ private static final int LABEL_FLAGS_FONT_NORMAL = 0x10;
+ private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20;
+ // Start of key text ratio enum values
+ private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0;
+ private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40;
+ private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80;
+ private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0;
+ private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO = 0x100;
+ private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140;
+ // End of key text ratio mask enum values
+ private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200;
+ private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400;
+ private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800;
+ private static final int LABEL_FLAGS_WITH_ICON_LEFT = 0x1000;
+ private static final int LABEL_FLAGS_WITH_ICON_RIGHT = 0x2000;
+ private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000;
+ private static final int LABEL_FLAGS_PRESERVE_CASE = 0x8000;
+ private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x10000;
+ private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x20000;
+ private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
+ private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
/** Icon to display instead of a label. Icon takes precedence over a label */
- private Drawable mIcon;
+ private final int mIconId;
+ /** Icon for disabled state */
+ private final int mDisabledIconId;
/** Preview version of the icon, for the preview popup */
- private Drawable mPreviewIcon;
+ private final int mPreviewIconId;
/** Width of the key, not including the gap */
public final int mWidth;
/** Height of the key, not including the gap */
public final int mHeight;
/** The horizontal gap around this key */
- public final int mGap;
+ public final int mHorizontalGap;
+ /** The vertical gap below this key */
+ public final int mVerticalGap;
/** The visual insets */
public final int mVisualInsetsLeft;
public final int mVisualInsetsRight;
- /** Whether this key is sticky, i.e., a toggle key */
- public final boolean mSticky;
/** X coordinate of the key in the keyboard layout */
public final int mX;
/** Y coordinate of the key in the keyboard layout */
public final int mY;
+ /** Hit bounding box of the key */
+ public final Rect mHitBox = new Rect();
+
/** Text to output when pressed. This can be multiple characters, like ".com" */
public final CharSequence mOutputText;
- /** Popup characters */
- public final CharSequence[] mPopupCharacters;
- /** Popup keyboard maximum column number */
- public final int mMaxPopupColumn;
-
- /**
- * Flags that specify the anchoring to edges of the keyboard for detecting touch events
- * that are just out of the boundary of the key. This is a bit mask of
- * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
- * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
- */
- public final int mEdgeFlags;
- /** Whether this is a functional key which has different key top than normal key */
- public final boolean mFunctional;
- /** Whether this key repeats itself when held down */
- public final boolean mRepeatable;
-
- /** The Keyboard that this key belongs to */
- private final Keyboard mKeyboard;
+ /** More keys */
+ public final MoreKeySpec[] mMoreKeys;
+ /** More keys column number and flags */
+ private final int mMoreKeysColumnAndFlags;
+ private static final int MORE_KEYS_COLUMN_MASK = 0x000000ff;
+ private static final int MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER = 0x80000000;
+ private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000;
+ private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000;
+ private static final int MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY = 0x10000000;
+ private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!";
+ private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
+ private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!";
+ private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!";
+ private static final String MORE_KEYS_EMBEDDED_MORE_KEY = "!embeddedMoreKey!";
+
+ /** Background type that represents different key background visual than normal one. */
+ public final int mBackgroundType;
+ public static final int BACKGROUND_TYPE_NORMAL = 0;
+ public static final int BACKGROUND_TYPE_FUNCTIONAL = 1;
+ public static final int BACKGROUND_TYPE_ACTION = 2;
+ public static final int BACKGROUND_TYPE_STICKY_OFF = 3;
+ public static final int BACKGROUND_TYPE_STICKY_ON = 4;
+
+ private final int mActionFlags;
+ private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
+ private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02;
+ private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
+ private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
+
+ private final int mHashCode;
/** The current pressed state of this key */
private boolean mPressed;
- /** If this is a sticky key, is its highlight on? */
- private boolean mHighlightOn;
/** Key is enabled and responds on press */
private boolean mEnabled = true;
- // keyWidth constants
- private static final int KEYWIDTH_FILL_RIGHT = 0;
- private static final int KEYWIDTH_FILL_BOTH = -1;
-
- private final static int[] KEY_STATE_NORMAL_ON = {
- android.R.attr.state_checkable,
- android.R.attr.state_checked
- };
-
- private final static int[] KEY_STATE_PRESSED_ON = {
- android.R.attr.state_pressed,
- android.R.attr.state_checkable,
- android.R.attr.state_checked
- };
-
- private final static int[] KEY_STATE_NORMAL_OFF = {
- android.R.attr.state_checkable
- };
-
- private final static int[] KEY_STATE_PRESSED_OFF = {
- android.R.attr.state_pressed,
- android.R.attr.state_checkable
- };
-
- private final static int[] KEY_STATE_NORMAL = {
- };
-
- private final static int[] KEY_STATE_PRESSED = {
- android.R.attr.state_pressed
- };
-
- // functional normal state (with properties)
- private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
- android.R.attr.state_single
- };
-
- // functional pressed state (with properties)
- private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
- android.R.attr.state_single,
- android.R.attr.state_pressed
- };
+ /**
+ * This constructor is being used only for keys in more keys keyboard.
+ */
+ public Key(Keyboard.Params params, MoreKeySpec moreKeySpec, int x, int y, int width, int height,
+ int labelFlags) {
+ this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode,
+ moreKeySpec.mOutputText, x, y, width, height, labelFlags);
+ }
/**
- * This constructor is being used only for key in popup mini keyboard.
+ * This constructor is being used only for key in popup suggestions pane.
*/
- public Key(Resources res, Keyboard keyboard, CharSequence popupCharacter, int x, int y,
- int width, int height, int edgeFlags) {
- mKeyboard = keyboard;
- mHeight = height - keyboard.getVerticalGap();
- mGap = keyboard.getHorizontalGap();
+ public Key(Keyboard.Params params, String label, String hintLabel, int iconId,
+ int code, String outputText, int x, int y, int width, int height, int labelFlags) {
+ mHeight = height - params.mVerticalGap;
+ mHorizontalGap = params.mHorizontalGap;
+ mVerticalGap = params.mVerticalGap;
mVisualInsetsLeft = mVisualInsetsRight = 0;
- mWidth = width - mGap;
- mEdgeFlags = edgeFlags;
- mHintLabel = null;
- mLabelOption = 0;
- mFunctional = false;
- mSticky = false;
- mRepeatable = false;
- mPopupCharacters = null;
- mMaxPopupColumn = 0;
- final String popupSpecification = popupCharacter.toString();
- mLabel = PopupCharactersParser.getLabel(popupSpecification);
- mOutputText = PopupCharactersParser.getOutputText(popupSpecification);
- mCode = PopupCharactersParser.getCode(res, popupSpecification);
- mIcon = keyboard.mIconsSet.getIcon(PopupCharactersParser.getIconId(popupSpecification));
+ mWidth = width - mHorizontalGap;
+ mHintLabel = hintLabel;
+ mLabelFlags = labelFlags;
+ mBackgroundType = BACKGROUND_TYPE_NORMAL;
+ mActionFlags = 0;
+ mMoreKeys = null;
+ mMoreKeysColumnAndFlags = 0;
+ mLabel = label;
+ mOutputText = outputText;
+ mCode = code;
+ mEnabled = (code != CODE_UNSPECIFIED);
+ mAltCode = CODE_UNSPECIFIED;
+ mIconId = iconId;
+ mDisabledIconId = ICON_UNDEFINED;
+ mPreviewIconId = ICON_UNDEFINED;
// Horizontal gap is divided equally to both sides of the key.
- mX = x + mGap / 2;
+ mX = x + mHorizontalGap / 2;
mY = y;
+ mHitBox.set(x, y, x + width + 1, y + height);
+
+ mHashCode = computeHashCode(this);
}
/**
* Create a key with the given top-left coordinate and extract its attributes from the XML
* parser.
* @param res resources associated with the caller's context
- * @param row the row that this key belongs to. The row must already be attached to
- * a {@link Keyboard}.
- * @param x the x coordinate of the top-left
- * @param y the y coordinate of the top-left
+ * @param params the keyboard building parameters.
+ * @param row the row that this key belongs to. row's x-coordinate will be the right edge of
+ * this key.
* @param parser the XML parser containing the attributes for this key
- * @param keyStyles active key styles set
+ * @throws XmlPullParserException
*/
- public Key(Resources res, Row row, int x, int y, XmlResourceParser parser,
- KeyStyles keyStyles) {
- mKeyboard = row.getKeyboard();
-
- final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- int keyWidth;
- try {
- mHeight = KeyboardParser.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight,
- mKeyboard.getKeyboardHeight(), row.mDefaultHeight) - row.mVerticalGap;
- mGap = KeyboardParser.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_horizontalGap,
- mKeyboard.getDisplayWidth(), row.mDefaultHorizontalGap);
- keyWidth = KeyboardParser.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyWidth,
- mKeyboard.getDisplayWidth(), row.mDefaultWidth);
- } finally {
- keyboardAttr.recycle();
- }
+ public Key(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
+ XmlPullParser parser) throws XmlPullParserException {
+ final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
+ final int keyHeight = row.mRowHeight;
+ mVerticalGap = params.mVerticalGap;
+ mHeight = keyHeight - mVerticalGap;
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
- try {
- final KeyStyle style;
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
- String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
- style = keyStyles.getKeyStyle(styleName);
- if (style == null)
- throw new ParseException("Unknown key style: " + styleName, parser);
- } else {
- style = keyStyles.getEmptyKeyStyle();
- }
- final int keyboardWidth = mKeyboard.getDisplayWidth();
- int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyXPos, keyboardWidth, x);
- if (keyXPos < 0) {
- // If keyXPos is negative, the actual x-coordinate will be k + keyXPos.
- keyXPos += keyboardWidth;
- if (keyXPos < x) {
- // keyXPos shouldn't be less than x because drawable area for this key starts
- // at x. Or, this key will overlaps the adjacent key on its left hand side.
- keyXPos = x;
- }
- }
- if (keyWidth == KEYWIDTH_FILL_RIGHT) {
- // If keyWidth is zero, the actual key width will be determined to fill out the
- // area up to the right edge of the keyboard.
- keyWidth = keyboardWidth - keyXPos;
- } else if (keyWidth <= KEYWIDTH_FILL_BOTH) {
- // If keyWidth is negative, the actual key width will be determined to fill out the
- // area between the nearest key on the left hand side and the right edge of the
- // keyboard.
- keyXPos = x;
- keyWidth = keyboardWidth - keyXPos;
- }
+ final KeyStyle style = params.mKeyStyles.getKeyStyle(keyAttr, parser);
+ final float keyXPos = row.getKeyX(keyAttr);
+ final float keyWidth = row.getKeyWidth(keyAttr, keyXPos);
+ final int keyYPos = row.getKeyY();
+
+ // Horizontal gap is divided equally to both sides of the key.
+ mX = Math.round(keyXPos + horizontalGap / 2);
+ mY = keyYPos;
+ mWidth = Math.round(keyWidth - horizontalGap);
+ mHorizontalGap = Math.round(horizontalGap);
+ mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
+ keyYPos + keyHeight);
+ // Update row to have current x coordinate.
+ row.setXPos(keyXPos + keyWidth);
+
+ mBackgroundType = style.getInt(keyAttr,
+ R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
+
+ mVisualInsetsLeft = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0));
+ mVisualInsetsRight = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0));
+ mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyIcon));
+ mDisabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyIconDisabled));
+ mPreviewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyIconPreview));
+
+ mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
+ | row.getDefaultKeyLabelFlags();
+ final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId);
+ final Locale locale = params.mId.mLocale;
+ int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
+
+ int moreKeysColumn = style.getInt(keyAttr,
+ R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn);
+ int value;
+ if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
+ moreKeysColumn = value & MORE_KEYS_COLUMN_MASK;
+ }
+ if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
+ moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK);
+ }
+ if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
+ moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS;
+ }
+ if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
+ moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
+ }
+ if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_EMBEDDED_MORE_KEY)) {
+ moreKeysColumn |= MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY;
+ }
+ mMoreKeysColumnAndFlags = moreKeysColumn;
- // Horizontal gap is divided equally to both sides of the key.
- mX = keyXPos + mGap / 2;
- mY = y;
- mWidth = keyWidth - mGap;
+ final String[] additionalMoreKeys;
+ if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) {
+ additionalMoreKeys = null;
+ } else {
+ additionalMoreKeys = style.getStringArray(keyAttr,
+ R.styleable.Keyboard_Key_additionalMoreKeys);
+ }
+ moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
+ if (moreKeys != null) {
+ actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
+ mMoreKeys = new MoreKeySpec[moreKeys.length];
+ for (int i = 0; i < moreKeys.length; i++) {
+ mMoreKeys[i] = new MoreKeySpec(
+ moreKeys[i], needsToUpperCase, locale, params.mCodesSet);
+ }
+ } else {
+ mMoreKeys = null;
+ }
+ mActionFlags = actionFlags;
- final CharSequence[] popupCharacters = style.getTextArray(keyAttr,
- R.styleable.Keyboard_Key_popupCharacters);
- if (res.getBoolean(R.bool.config_digit_popup_characters_enabled)) {
- mPopupCharacters = popupCharacters;
+ if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
+ mLabel = params.mId.mCustomActionLabel;
+ } else {
+ mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale);
+ }
+ if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
+ mHintLabel = null;
+ } else {
+ mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale);
+ }
+ String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale);
+ final int code = KeySpecParser.parseCode(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED);
+ // Choose the first letter of the label as primary code if not specified.
+ if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
+ && !TextUtils.isEmpty(mLabel)) {
+ if (StringUtils.codePointCount(mLabel) == 1) {
+ // Use the first letter of the hint label if shiftedLetterActivated flag is
+ // specified.
+ if (hasShiftedLetterHint() && isShiftedLetterActivated()
+ && !TextUtils.isEmpty(mHintLabel)) {
+ mCode = mHintLabel.codePointAt(0);
+ } else {
+ mCode = mLabel.codePointAt(0);
+ }
} else {
- mPopupCharacters = filterOutDigitPopupCharacters(popupCharacters);
+ // In some locale and case, the character might be represented by multiple code
+ // points, such as upper case Eszett of German alphabet.
+ outputText = mLabel;
+ mCode = CODE_OUTPUT_TEXT;
}
- mMaxPopupColumn = style.getInt(keyboardAttr,
- R.styleable.Keyboard_Key_maxPopupKeyboardColumn,
- mKeyboard.getMaxPopupKeyboardColumn());
-
- mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false);
- mFunctional = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional, false);
- mSticky = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky, false);
- mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true);
- mEdgeFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyEdgeFlags, 0)
- | row.mRowEdgeFlags;
-
- final KeyboardIconsSet iconsSet = mKeyboard.mIconsSet;
- mVisualInsetsLeft = KeyboardParser.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_visualInsetsLeft, mKeyboard.getDisplayHeight(), 0);
- mVisualInsetsRight = KeyboardParser.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_visualInsetsRight, mKeyboard.getDisplayHeight(), 0);
- mPreviewIcon = iconsSet.getIcon(style.getInt(
- keyAttr, R.styleable.Keyboard_Key_keyIconPreview,
- KeyboardIconsSet.ICON_UNDEFINED));
- Keyboard.setDefaultBounds(mPreviewIcon);
- mIcon = iconsSet.getIcon(style.getInt(
- keyAttr, R.styleable.Keyboard_Key_keyIcon,
- KeyboardIconsSet.ICON_UNDEFINED));
- Keyboard.setDefaultBounds(mIcon);
- mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
-
- mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
- mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0);
- mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
- // Choose the first letter of the label as primary code if not
- // specified.
- final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
- Keyboard.CODE_UNSPECIFIED);
- if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) {
- mCode = mLabel.charAt(0);
- } else if (code != Keyboard.CODE_UNSPECIFIED) {
- mCode = code;
+ } else if (code == CODE_UNSPECIFIED && outputText != null) {
+ if (StringUtils.codePointCount(outputText) == 1) {
+ mCode = outputText.codePointAt(0);
+ outputText = null;
} else {
- mCode = Keyboard.CODE_DUMMY;
+ mCode = CODE_OUTPUT_TEXT;
}
+ } else {
+ mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
+ }
+ mOutputText = outputText;
+ mAltCode = KeySpecParser.toUpperCaseOfCodeForLocale(
+ KeySpecParser.parseCode(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
+ needsToUpperCase, locale);
+ mHashCode = computeHashCode(this);
- final Drawable shiftedIcon = iconsSet.getIcon(style.getInt(
- keyAttr, R.styleable.Keyboard_Key_keyIconShifted,
- KeyboardIconsSet.ICON_UNDEFINED));
- if (shiftedIcon != null)
- mKeyboard.getShiftedIcons().put(this, shiftedIcon);
- } finally {
- keyAttr.recycle();
+ keyAttr.recycle();
+
+ if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) {
+ Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this);
+ }
+ }
+
+ private static boolean needsToUpperCase(int labelFlags, int keyboardElementId) {
+ if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
+ switch (keyboardElementId) {
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static int computeHashCode(Key key) {
+ return Arrays.hashCode(new Object[] {
+ key.mX,
+ key.mY,
+ key.mWidth,
+ key.mHeight,
+ key.mCode,
+ key.mLabel,
+ key.mHintLabel,
+ key.mIconId,
+ key.mBackgroundType,
+ Arrays.hashCode(key.mMoreKeys),
+ key.mOutputText,
+ key.mActionFlags,
+ key.mLabelFlags,
+ // Key can be distinguishable without the following members.
+ // key.mAltCode,
+ // key.mDisabledIconId,
+ // key.mPreviewIconId,
+ // key.mHorizontalGap,
+ // key.mVerticalGap,
+ // key.mVisualInsetLeft,
+ // key.mVisualInsetRight,
+ // key.mMaxMoreKeysColumn,
+ });
+ }
+
+ private boolean equals(Key o) {
+ if (this == o) return true;
+ return o.mX == mX
+ && o.mY == mY
+ && o.mWidth == mWidth
+ && o.mHeight == mHeight
+ && o.mCode == mCode
+ && TextUtils.equals(o.mLabel, mLabel)
+ && TextUtils.equals(o.mHintLabel, mHintLabel)
+ && o.mIconId == mIconId
+ && o.mBackgroundType == mBackgroundType
+ && Arrays.equals(o.mMoreKeys, mMoreKeys)
+ && TextUtils.equals(o.mOutputText, mOutputText)
+ && o.mActionFlags == mActionFlags
+ && o.mLabelFlags == mLabelFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Key && equals((Key)o);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%s %d,%d %dx%d %s/%s/%s",
+ Keyboard.printableCode(mCode), mLabel, mX, mY, mWidth, mHeight, mHintLabel,
+ KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
+ }
+
+ private static String backgroundName(int backgroundType) {
+ switch (backgroundType) {
+ case BACKGROUND_TYPE_NORMAL: return "normal";
+ case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
+ case BACKGROUND_TYPE_ACTION: return "action";
+ case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff";
+ case BACKGROUND_TYPE_STICKY_ON: return "stickyOn";
+ default: return null;
}
}
+ public void markAsLeftEdge(Keyboard.Params params) {
+ mHitBox.left = params.mHorizontalEdgesPadding;
+ }
+
+ public void markAsRightEdge(Keyboard.Params params) {
+ mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding;
+ }
+
+ public void markAsTopEdge(Keyboard.Params params) {
+ mHitBox.top = params.mTopPadding;
+ }
+
+ public void markAsBottomEdge(Keyboard.Params params) {
+ mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
+ }
+
+ public final boolean isSpacer() {
+ return this instanceof Spacer;
+ }
+
+ public boolean isShift() {
+ return mCode == CODE_SHIFT;
+ }
+
+ public boolean isModifier() {
+ return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
+ }
+
+ public boolean isRepeatable() {
+ return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
+ }
+
+ public boolean noKeyPreview() {
+ return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
+ }
+
+ public boolean altCodeWhileTyping() {
+ return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
+ }
+
+ public boolean isLongPressEnabled() {
+ // We need not start long press timer on the key which has activated shifted letter.
+ return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
+ && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
+ }
+
public Typeface selectTypeface(Typeface defaultTypeface) {
// TODO: Handle "bold" here too?
- if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) {
+ if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
return Typeface.DEFAULT;
- } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) {
+ } else if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) {
return Typeface.MONOSPACE;
} else {
return defaultTypeface;
}
}
- public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) {
- if (mLabel.length() > 1
- && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO
- | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) {
- return label;
- } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) {
- return hintLabel;
- } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) {
- return largeLetter;
- } else {
- return letter;
+ public int selectTextSize(int letterSize, int largeLetterSize, int labelSize,
+ int largeLabelSize, int hintLabelSize) {
+ switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
+ case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
+ return letterSize;
+ case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
+ return largeLetterSize;
+ case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
+ return labelSize;
+ case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO:
+ return largeLabelSize;
+ case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
+ return hintLabelSize;
+ default: // No follow key ratio flag specified.
+ return StringUtils.codePointCount(mLabel) == 1 ? letterSize : labelSize;
}
}
+ public boolean isAlignLeft() {
+ return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0;
+ }
+
+ public boolean isAlignRight() {
+ return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0;
+ }
+
+ public boolean isAlignLeftOfCenter() {
+ return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0;
+ }
+
public boolean hasPopupHint() {
- return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0;
+ return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
}
- public boolean hasUppercaseLetter() {
- return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0;
+ public boolean hasShiftedLetterHint() {
+ return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0;
}
public boolean hasHintLabel() {
- return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0;
- }
-
- private static boolean isDigitPopupCharacter(CharSequence label) {
- return label != null && label.length() == 1 && Character.isDigit(label.charAt(0));
- }
-
- private static CharSequence[] filterOutDigitPopupCharacters(CharSequence[] popupCharacters) {
- if (popupCharacters == null || popupCharacters.length < 1)
- return null;
- if (popupCharacters.length == 1 && isDigitPopupCharacter(
- PopupCharactersParser.getLabel(popupCharacters[0].toString())))
- return null;
- ArrayList<CharSequence> filtered = null;
- for (int i = 0; i < popupCharacters.length; i++) {
- final CharSequence popupSpec = popupCharacters[i];
- if (isDigitPopupCharacter(PopupCharactersParser.getLabel(popupSpec.toString()))) {
- if (filtered == null) {
- filtered = new ArrayList<CharSequence>();
- for (int j = 0; j < i; j++)
- filtered.add(popupCharacters[j]);
- }
- } else if (filtered != null) {
- filtered.add(popupSpec);
- }
- }
- if (filtered == null)
- return popupCharacters;
- if (filtered.size() == 0)
- return null;
- return filtered.toArray(new CharSequence[filtered.size()]);
+ return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
+ }
+
+ public boolean hasLabelWithIconLeft() {
+ return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0;
+ }
+
+ public boolean hasLabelWithIconRight() {
+ return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0;
+ }
+
+ public boolean needsXScale() {
+ return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
+ }
+
+ public boolean isShiftedLetterActivated() {
+ return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0;
+ }
+
+ public int getMoreKeysColumn() {
+ return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK;
+ }
+
+ public boolean isFixedColumnOrderMoreKeys() {
+ return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0;
+ }
+
+ public boolean hasLabelsInMoreKeys() {
+ return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
}
- public Drawable getIcon() {
- return mIcon;
+ public int getMoreKeyLabelFlags() {
+ return hasLabelsInMoreKeys()
+ ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
+ : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
}
- public Drawable getPreviewIcon() {
- return mPreviewIcon;
+ public boolean needsDividersInMoreKeys() {
+ return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
}
- public void setIcon(Drawable icon) {
- mIcon = icon;
+ public boolean hasEmbeddedMoreKey() {
+ return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY) != 0;
+ }
+
+ public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
+ final int iconId = mEnabled ? mIconId : mDisabledIconId;
+ final Drawable icon = iconSet.getIconDrawable(iconId);
+ if (icon != null) {
+ icon.setAlpha(alpha);
+ }
+ return icon;
}
- public void setPreviewIcon(Drawable icon) {
- mPreviewIcon = icon;
+ public Drawable getPreviewIcon(KeyboardIconsSet iconSet) {
+ return mPreviewIconId != ICON_UNDEFINED
+ ? iconSet.getIconDrawable(mPreviewIconId)
+ : iconSet.getIconDrawable(mIconId);
}
/**
@@ -415,10 +604,6 @@ public class Key {
mPressed = false;
}
- public void setHighlightOn(boolean highlightOn) {
- mHighlightOn = highlightOn;
- }
-
public boolean isEnabled() {
return mEnabled;
}
@@ -431,22 +616,12 @@ public class Key {
* Detects if a point falls on this key.
* @param x the x-coordinate of the point
* @param y the y-coordinate of the point
- * @return whether or not the point falls on the key. If the key is attached to an edge, it will
- * assume that all points between the key and the edge are considered to be on the key.
+ * @return whether or not the point falls on the key. If the key is attached to an edge, it
+ * will assume that all points between the key and the edge are considered to be on the key.
+ * @see #markAsLeftEdge(Keyboard.Params) etc.
*/
public boolean isOnKey(int x, int y) {
- final int flags = mEdgeFlags;
- final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
- final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
- final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
- final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
- final int left = mX - mGap / 2;
- final int right = left + mWidth + mGap;
- final int top = mY;
- final int bottom = top + mHeight + mKeyboard.getVerticalGap();
- // In order to mitigate rounding errors, we use (left <= x <= right) here.
- return (x >= left || leftEdge) && (x <= right || rightEdge)
- && (y >= top || topEdge) && (y <= bottom || bottomEdge);
+ return mHitBox.contains(x, y);
}
/**
@@ -467,42 +642,87 @@ public class Key {
return dx * dx + dy * dy;
}
+ private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_ON = {
+ android.R.attr.state_checkable,
+ android.R.attr.state_checked
+ };
+
+ private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_ON = {
+ android.R.attr.state_pressed,
+ android.R.attr.state_checkable,
+ android.R.attr.state_checked
+ };
+
+ private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_OFF = {
+ android.R.attr.state_checkable
+ };
+
+ private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_OFF = {
+ android.R.attr.state_pressed,
+ android.R.attr.state_checkable
+ };
+
+ private final static int[] KEY_STATE_NORMAL = {
+ };
+
+ private final static int[] KEY_STATE_PRESSED = {
+ android.R.attr.state_pressed
+ };
+
+ // functional normal state (with properties)
+ private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
+ android.R.attr.state_single
+ };
+
+ // functional pressed state (with properties)
+ private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
+ android.R.attr.state_single,
+ android.R.attr.state_pressed
+ };
+
+ // action normal state (with properties)
+ private static final int[] KEY_STATE_ACTIVE_NORMAL = {
+ android.R.attr.state_active
+ };
+
+ // action pressed state (with properties)
+ private static final int[] KEY_STATE_ACTIVE_PRESSED = {
+ android.R.attr.state_active,
+ android.R.attr.state_pressed
+ };
+
/**
* Returns the drawable state for the key, based on the current state and type of the key.
* @return the drawable state of the key.
* @see android.graphics.drawable.StateListDrawable#setState(int[])
*/
public int[] getCurrentDrawableState() {
- final boolean pressed = mPressed;
- if (!mSticky && mFunctional) {
- if (pressed) {
- return KEY_STATE_FUNCTIONAL_PRESSED;
- } else {
- return KEY_STATE_FUNCTIONAL_NORMAL;
- }
+ switch (mBackgroundType) {
+ case BACKGROUND_TYPE_FUNCTIONAL:
+ return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
+ case BACKGROUND_TYPE_ACTION:
+ return mPressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL;
+ case BACKGROUND_TYPE_STICKY_OFF:
+ return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
+ case BACKGROUND_TYPE_STICKY_ON:
+ return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
+ default: /* BACKGROUND_TYPE_NORMAL */
+ return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
}
+ }
- int[] states = KEY_STATE_NORMAL;
+ public static class Spacer extends Key {
+ public Spacer(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
+ XmlPullParser parser) throws XmlPullParserException {
+ super(res, params, row, parser);
+ }
- if (mHighlightOn) {
- if (pressed) {
- states = KEY_STATE_PRESSED_ON;
- } else {
- states = KEY_STATE_NORMAL_ON;
- }
- } else {
- if (mSticky) {
- if (pressed) {
- states = KEY_STATE_PRESSED_OFF;
- } else {
- states = KEY_STATE_NORMAL_OFF;
- }
- } else {
- if (pressed) {
- states = KEY_STATE_PRESSED;
- }
- }
+ /**
+ * This constructor is being used only for divider in more keys keyboard.
+ */
+ protected Spacer(Keyboard.Params params, int x, int y, int width, int height) {
+ super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED,
+ null, x, y, width, height, 0);
}
- return states;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 7add43a6d..13e909c7e 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -16,28 +16,26 @@
package com.android.inputmethod.keyboard;
-import android.util.Log;
-
-import java.util.Arrays;
-import java.util.List;
public class KeyDetector {
- private static final String TAG = KeyDetector.class.getSimpleName();
- private static final boolean DEBUG = false;
-
public static final int NOT_A_CODE = -1;
- public static final int NOT_A_KEY = -1;
+
+ private final int mKeyHysteresisDistanceSquared;
private Keyboard mKeyboard;
private int mCorrectionX;
private int mCorrectionY;
private boolean mProximityCorrectOn;
- private int mProximityThresholdSquare;
- // working area
- private static final int MAX_NEARBY_KEYS = 12;
- private final int[] mDistances = new int[MAX_NEARBY_KEYS];
- private final int[] mIndices = new int[MAX_NEARBY_KEYS];
+ /**
+ * This class handles key detection.
+ *
+ * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the
+ * movement will not been handled as meaningful movement. The unit is pixel.
+ */
+ public KeyDetector(float keyHysteresisDistance) {
+ mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
+ }
public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
if (keyboard == null)
@@ -47,19 +45,22 @@ public class KeyDetector {
mKeyboard = keyboard;
}
- protected int getTouchX(int x) {
+ public int getKeyHysteresisDistanceSquared() {
+ return mKeyHysteresisDistanceSquared;
+ }
+
+ public int getTouchX(int x) {
return x + mCorrectionX;
}
- protected int getTouchY(int y) {
+ public int getTouchY(int y) {
return y + mCorrectionY;
}
- protected List<Key> getKeys() {
+ public Keyboard getKeyboard() {
if (mKeyboard == null)
throw new IllegalStateException("keyboard isn't set");
- // mKeyboard is guaranteed not to be null at setKeybaord() method if mKeys is not null
- return mKeyboard.getKeys();
+ return mKeyboard;
}
public void setProximityCorrectionEnabled(boolean enabled) {
@@ -70,133 +71,49 @@ public class KeyDetector {
return mProximityCorrectOn;
}
- public void setProximityThreshold(int threshold) {
- mProximityThresholdSquare = threshold * threshold;
- }
-
- /**
- * Computes maximum size of the array that can contain all nearby key indices returned by
- * {@link #getKeyIndexAndNearbyCodes}.
- *
- * @return Returns maximum size of the array that can contain all nearby key indices returned
- * by {@link #getKeyIndexAndNearbyCodes}.
- */
- protected int getMaxNearbyKeys() {
- return MAX_NEARBY_KEYS;
+ public boolean alwaysAllowsSlidingInput() {
+ return false;
}
/**
- * Allocates array that can hold all key indices returned by {@link #getKeyIndexAndNearbyCodes}
- * method. The maximum size of the array should be computed by {@link #getMaxNearbyKeys}.
- *
- * @return Allocates and returns an array that can hold all key indices returned by
- * {@link #getKeyIndexAndNearbyCodes} method. All elements in the returned array are
- * initialized by {@link #NOT_A_CODE} value.
- */
- public int[] newCodeArray() {
- int[] codes = new int[getMaxNearbyKeys()];
- Arrays.fill(codes, NOT_A_CODE);
- return codes;
- }
-
- private void initializeNearbyKeys() {
- Arrays.fill(mDistances, Integer.MAX_VALUE);
- Arrays.fill(mIndices, NOT_A_KEY);
- }
-
- /**
- * Insert the key into nearby keys buffer and sort nearby keys by ascending order of distance.
- * If the distance of two keys are the same, the key which the point is on should be considered
- * as a closer one.
- *
- * @param keyIndex index of the key.
- * @param distance distance between the key's edge and user touched point.
- * @param isOnKey true if the point is on the key.
- * @return order of the key in the nearby buffer, 0 if it is the nearest key.
- */
- private int sortNearbyKeys(int keyIndex, int distance, boolean isOnKey) {
- final int[] distances = mDistances;
- final int[] indices = mIndices;
- for (int insertPos = 0; insertPos < distances.length; insertPos++) {
- final int comparingDistance = distances[insertPos];
- if (distance < comparingDistance || (distance == comparingDistance && isOnKey)) {
- final int nextPos = insertPos + 1;
- if (nextPos < distances.length) {
- System.arraycopy(distances, insertPos, distances, nextPos,
- distances.length - nextPos);
- System.arraycopy(indices, insertPos, indices, nextPos,
- indices.length - nextPos);
- }
- distances[insertPos] = distance;
- indices[insertPos] = keyIndex;
- return insertPos;
- }
- }
- return distances.length;
- }
-
- private void getNearbyKeyCodes(final int[] allCodes) {
- final List<Key> keys = getKeys();
- final int[] indices = mIndices;
-
- // allCodes[0] should always have the key code even if it is a non-letter key.
- if (indices[0] == NOT_A_KEY) {
- allCodes[0] = NOT_A_CODE;
- return;
- }
-
- int numCodes = 0;
- for (int j = 0; j < indices.length && numCodes < allCodes.length; j++) {
- final int index = indices[j];
- if (index == NOT_A_KEY)
- break;
- final int code = keys.get(index).mCode;
- // filter out a non-letter key from nearby keys
- if (code < Keyboard.CODE_SPACE)
- continue;
- allCodes[numCodes++] = code;
- }
- }
-
- /**
- * Finds all possible nearby key indices around a touch event point and returns the nearest key
- * index. The algorithm to determine the nearby keys depends on the threshold set by
- * {@link #setProximityThreshold(int)} and the mode set by
- * {@link #setProximityCorrectionEnabled(boolean)}.
+ * Detect the key whose hitbox the touch point is in.
*
* @param x The x-coordinate of a touch point
* @param y The y-coordinate of a touch point
- * @param allCodes All nearby key code except functional key are returned in this array
- * @return The nearest key index
+ * @return the key that the touch point hits.
*/
- public int getKeyIndexAndNearbyCodes(int x, int y, final int[] allCodes) {
- final List<Key> keys = getKeys();
+ public Key detectHitKey(int x, int y) {
final int touchX = getTouchX(x);
final int touchY = getTouchY(y);
- initializeNearbyKeys();
- int primaryIndex = NOT_A_KEY;
- for (final int index : mKeyboard.getNearestKeys(touchX, touchY)) {
- final Key key = keys.get(index);
+ int minDistance = Integer.MAX_VALUE;
+ Key primaryKey = null;
+ for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) {
final boolean isOnKey = key.isOnKey(touchX, touchY);
final int distance = key.squaredDistanceToEdge(touchX, touchY);
- if (isOnKey || (mProximityCorrectOn && distance < mProximityThresholdSquare)) {
- final int insertedPosition = sortNearbyKeys(index, distance, isOnKey);
- if (insertedPosition == 0 && isOnKey)
- primaryIndex = index;
+ // To take care of hitbox overlaps, we compare mCode here too.
+ if (primaryKey == null || distance < minDistance
+ || (distance == minDistance && isOnKey && key.mCode > primaryKey.mCode)) {
+ minDistance = distance;
+ primaryKey = key;
}
}
+ return primaryKey;
+ }
- if (allCodes != null && allCodes.length > 0) {
- getNearbyKeyCodes(allCodes);
- if (DEBUG) {
- Log.d(TAG, "x=" + x + " y=" + y
- + " primary="
- + (primaryIndex == NOT_A_KEY ? "none" : keys.get(primaryIndex).mCode)
- + " codes=" + Arrays.toString(allCodes));
- }
- }
+ public static String printableCode(Key key) {
+ return key != null ? Keyboard.printableCode(key.mCode) : "none";
+ }
- return primaryIndex;
+ public static String printableCodes(int[] codes) {
+ final StringBuilder sb = new StringBuilder();
+ boolean addDelimiter = false;
+ for (final int code : codes) {
+ if (code == NOT_A_CODE) break;
+ if (addDelimiter) sb.append(", ");
+ sb.append(Keyboard.printableCode(code));
+ addDelimiter = true;
+ }
+ return "[" + sb + "]";
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 20327c5b2..6fc630d05 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -18,22 +18,35 @@ package com.android.inputmethod.keyboard;
import android.content.Context;
import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+import com.android.inputmethod.keyboard.internal.KeyStyles;
+import com.android.inputmethod.keyboard.internal.KeyboardCodesSet;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
-import com.android.inputmethod.keyboard.internal.KeyboardParser;
-import com.android.inputmethod.keyboard.internal.KeyboardShiftState;
+import com.android.inputmethod.keyboard.internal.KeyboardTextsSet;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeLocale;
+import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.XmlParseUtils;
+import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
+import java.util.Locale;
/**
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
@@ -56,12 +69,9 @@ import java.util.Map;
public class Keyboard {
private static final String TAG = Keyboard.class.getSimpleName();
- public static final int EDGE_LEFT = 0x01;
- public static final int EDGE_RIGHT = 0x02;
- public static final int EDGE_TOP = 0x04;
- public static final int EDGE_BOTTOM = 0x08;
-
- /** Some common keys code. These should be aligned with values/keycodes.xml */
+ /** Some common keys code. Must be positive.
+ * These should be aligned with values/keycodes.xml
+ */
public static final int CODE_ENTER = '\n';
public static final int CODE_TAB = '\t';
public static final int CODE_SPACE = ' ';
@@ -69,393 +79,1275 @@ public class Keyboard {
public static final int CODE_DASH = '-';
public static final int CODE_SINGLE_QUOTE = '\'';
public static final int CODE_DOUBLE_QUOTE = '"';
-
- /** Special keys code. These should be aligned with values/keycodes.xml */
- public static final int CODE_DUMMY = 0;
+ // TODO: Check how this should work for right-to-left languages. It seems to stand
+ // that for rtl languages, a closing parenthesis is a left parenthesis. Is this
+ // managed by the font? Or is it a different char?
+ public static final int CODE_CLOSING_PARENTHESIS = ')';
+ public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
+ public static final int CODE_CLOSING_CURLY_BRACKET = '}';
+ public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
+ private static final int MINIMUM_LETTER_CODE = CODE_TAB;
+
+ /** Special keys code. Must be negative.
+ * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
+ * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
+ */
public static final int CODE_SHIFT = -1;
public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
- public static final int CODE_CAPSLOCK = -3;
- public static final int CODE_CANCEL = -4;
- public static final int CODE_DELETE = -5;
- public static final int CODE_SETTINGS = -6;
- public static final int CODE_SETTINGS_LONGPRESS = -7;
- public static final int CODE_SHORTCUT = -8;
+ public static final int CODE_OUTPUT_TEXT = -3;
+ public static final int CODE_DELETE = -4;
+ public static final int CODE_SETTINGS = -5;
+ public static final int CODE_SHORTCUT = -6;
+ public static final int CODE_ACTION_ENTER = -7;
+ public static final int CODE_ACTION_NEXT = -8;
+ public static final int CODE_ACTION_PREVIOUS = -9;
+ public static final int CODE_LANGUAGE_SWITCH = -10;
+ public static final int CODE_RESEARCH = -11;
// Code value representing the code is not specified.
- public static final int CODE_UNSPECIFIED = -99;
+ public static final int CODE_UNSPECIFIED = -12;
- /** Horizontal gap default for all rows */
- private int mDefaultHorizontalGap;
-
- /** Default key width */
- private int mDefaultWidth;
+ public final KeyboardId mId;
+ public final int mThemeId;
- /** Default key height */
- private int mDefaultHeight;
+ /** Total height of the keyboard, including the padding and keys */
+ public final int mOccupiedHeight;
+ /** Total width of the keyboard, including the padding and keys */
+ public final int mOccupiedWidth;
+ /** The padding above the keyboard */
+ public final int mTopPadding;
/** Default gap between rows */
- private int mDefaultVerticalGap;
+ public final int mVerticalGap;
- /** Popup keyboard template */
- private int mPopupKeyboardResId;
+ public final int mMostCommonKeyHeight;
+ public final int mMostCommonKeyWidth;
- /** Maximum column for popup keyboard */
- private int mMaxPopupColumn;
+ /** More keys keyboard template */
+ public final int mMoreKeysTemplate;
- /** List of shift keys in this keyboard and its icons and state */
- private final List<Key> mShiftKeys = new ArrayList<Key>();
- private final HashMap<Key, Drawable> mShiftedIcons = new HashMap<Key, Drawable>();
- private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>();
- private final HashSet<Key> mShiftLockEnabled = new HashSet<Key>();
- private final KeyboardShiftState mShiftState = new KeyboardShiftState();
+ /** Maximum column for more keys keyboard */
+ public final int mMaxMoreKeysKeyboardColumn;
- /** Total height of the keyboard, including the padding and keys */
- private int mTotalHeight;
+ /** Array of keys and icons in this keyboard */
+ public final Key[] mKeys;
+ public final Key[] mShiftKeys;
+ public final Key[] mAltCodeKeysWhileTyping;
+ public final KeyboardIconsSet mIconsSet;
- /**
- * Total width (minimum width) of the keyboard, including left side gaps and keys, but not any
- * gaps on the right side.
- */
- private int mMinWidth;
+ private final HashMap<Integer, Key> mKeyCache = new HashMap<Integer, Key>();
- /** List of keys in this keyboard */
- private final List<Key> mKeys = new ArrayList<Key>();
+ private final ProximityInfo mProximityInfo;
+ private final boolean mProximityCharsCorrectionEnabled;
+
+ public Keyboard(Params params) {
+ mId = params.mId;
+ mThemeId = params.mThemeId;
+ mOccupiedHeight = params.mOccupiedHeight;
+ mOccupiedWidth = params.mOccupiedWidth;
+ mMostCommonKeyHeight = params.mMostCommonKeyHeight;
+ mMostCommonKeyWidth = params.mMostCommonKeyWidth;
+ mMoreKeysTemplate = params.mMoreKeysTemplate;
+ mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn;
+
+ mTopPadding = params.mTopPadding;
+ mVerticalGap = params.mVerticalGap;
+
+ mKeys = params.mKeys.toArray(new Key[params.mKeys.size()]);
+ mShiftKeys = params.mShiftKeys.toArray(new Key[params.mShiftKeys.size()]);
+ mAltCodeKeysWhileTyping = params.mAltCodeKeysWhileTyping.toArray(
+ new Key[params.mAltCodeKeysWhileTyping.size()]);
+ mIconsSet = params.mIconsSet;
+
+ mProximityInfo = new ProximityInfo(params.mId.mLocale.toString(),
+ params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight,
+ mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection);
+ mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
+ }
- /** Width of the screen available to fit the keyboard */
- private final int mDisplayWidth;
+ public boolean hasProximityCharsCorrection(int code) {
+ if (!mProximityCharsCorrectionEnabled) {
+ return false;
+ }
+ // Note: The native code has the main keyboard layout only at this moment.
+ // TODO: Figure out how to handle proximity characters information of all layouts.
+ final boolean canAssumeNativeHasProximityCharsInfoOfAllKeys = (
+ mId.mElementId == KeyboardId.ELEMENT_ALPHABET
+ || mId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED);
+ return canAssumeNativeHasProximityCharsInfoOfAllKeys || Character.isLetter(code);
+ }
- /** Height of the screen */
- private final int mDisplayHeight;
+ public ProximityInfo getProximityInfo() {
+ return mProximityInfo;
+ }
- /** Height of keyboard */
- private int mKeyboardHeight;
+ public Key getKey(int code) {
+ if (code == CODE_UNSPECIFIED) {
+ return null;
+ }
+ final Integer keyCode = code;
+ if (mKeyCache.containsKey(keyCode)) {
+ return mKeyCache.get(keyCode);
+ }
- private int mMostCommonKeyWidth = 0;
+ for (final Key key : mKeys) {
+ if (key.mCode == code) {
+ mKeyCache.put(keyCode, key);
+ return key;
+ }
+ }
+ mKeyCache.put(keyCode, null);
+ return null;
+ }
- public final KeyboardId mId;
+ public boolean hasKey(Key aKey) {
+ if (mKeyCache.containsKey(aKey)) {
+ return true;
+ }
- public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
+ for (final Key key : mKeys) {
+ if (key == aKey) {
+ mKeyCache.put(key.mCode, key);
+ return true;
+ }
+ }
+ return false;
+ }
- // Variables for pre-computing nearest keys.
+ public static boolean isLetterCode(int code) {
+ return code >= MINIMUM_LETTER_CODE;
+ }
- // TODO: Change GRID_WIDTH and GRID_HEIGHT to private.
- public final int GRID_WIDTH;
- public final int GRID_HEIGHT;
- private final int GRID_SIZE;
- private int mCellWidth;
- private int mCellHeight;
- private int[][] mGridNeighbors;
- private int mProximityThreshold;
- private static int[] EMPTY_INT_ARRAY = new int[0];
- /** Number of key widths from current touch point to search for nearest keys. */
- private static float SEARCH_DISTANCE = 1.2f;
+ public static class Params {
+ public KeyboardId mId;
+ public int mThemeId;
+
+ /** Total height and width of the keyboard, including the paddings and keys */
+ public int mOccupiedHeight;
+ public int mOccupiedWidth;
+
+ /** Base height and width of the keyboard used to calculate rows' or keys' heights and
+ * widths
+ */
+ public int mBaseHeight;
+ public int mBaseWidth;
+
+ public int mTopPadding;
+ public int mBottomPadding;
+ public int mHorizontalEdgesPadding;
+ public int mHorizontalCenterPadding;
+
+ public int mDefaultRowHeight;
+ public int mDefaultKeyWidth;
+ public int mHorizontalGap;
+ public int mVerticalGap;
+
+ public int mMoreKeysTemplate;
+ public int mMaxMoreKeysKeyboardColumn;
+
+ public int GRID_WIDTH;
+ public int GRID_HEIGHT;
+
+ public final HashSet<Key> mKeys = new HashSet<Key>();
+ public final ArrayList<Key> mShiftKeys = new ArrayList<Key>();
+ public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<Key>();
+ public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
+ public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
+ public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+ public final KeyStyles mKeyStyles = new KeyStyles(mTextsSet);
+
+ public KeyboardLayoutSet.KeysCache mKeysCache;
+
+ public int mMostCommonKeyHeight = 0;
+ public int mMostCommonKeyWidth = 0;
+
+ public boolean mProximityCharsCorrectionEnabled;
+
+ public final TouchPositionCorrection mTouchPositionCorrection =
+ new TouchPositionCorrection();
+
+ public static class TouchPositionCorrection {
+ private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
+
+ public boolean mEnabled;
+ public float[] mXs;
+ public float[] mYs;
+ public float[] mRadii;
+
+ public void load(String[] data) {
+ final int dataLength = data.length;
+ if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
+ if (LatinImeLogger.sDBG)
+ throw new RuntimeException(
+ "the size of touch position correction data is invalid");
+ return;
+ }
- private final ProximityInfo mProximityInfo;
+ final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ mXs = new float[length];
+ mYs = new float[length];
+ mRadii = new float[length];
+ try {
+ for (int i = 0; i < dataLength; ++i) {
+ final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ final float value = Float.parseFloat(data[i]);
+ if (type == 0) {
+ mXs[index] = value;
+ } else if (type == 1) {
+ mYs[index] = value;
+ } else {
+ mRadii[index] = value;
+ }
+ }
+ } catch (NumberFormatException e) {
+ if (LatinImeLogger.sDBG) {
+ throw new RuntimeException(
+ "the number format for touch position correction data is invalid");
+ }
+ mXs = null;
+ mYs = null;
+ mRadii = null;
+ }
+ }
- /**
- * Creates a keyboard from the given xml key layout file.
- * @param context the application or service context
- * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
- * @param id keyboard identifier
- * @param width keyboard width
- */
+ // TODO: Remove this method.
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
- public Keyboard(Context context, int xmlLayoutResId, KeyboardId id, int width) {
- final Resources res = context.getResources();
- GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
- GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
- GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
-
- final int horizontalEdgesPadding = (int)res.getDimension(
- R.dimen.keyboard_horizontal_edges_padding);
- mDisplayWidth = width - horizontalEdgesPadding * 2;
- // TODO: Adjust the height by referring to the height of area available for drawing as well.
- mDisplayHeight = res.getDisplayMetrics().heightPixels;
-
- mDefaultHorizontalGap = 0;
- setKeyWidth(mDisplayWidth / 10);
- mDefaultVerticalGap = 0;
- mDefaultHeight = mDefaultWidth;
- mId = id;
- mProximityInfo = new ProximityInfo(GRID_WIDTH, GRID_HEIGHT);
- loadKeyboard(context, xmlLayoutResId);
- }
+ public boolean isValid() {
+ return mEnabled && mXs != null && mYs != null && mRadii != null
+ && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
+ }
+ }
- public int getProximityInfo() {
- return mProximityInfo.getNativeProximityInfo(this);
- }
+ protected void clearKeys() {
+ mKeys.clear();
+ mShiftKeys.clear();
+ clearHistogram();
+ }
- public List<Key> getKeys() {
- return mKeys;
- }
+ public void onAddKey(Key newKey) {
+ final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
+ final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0;
+ if (!zeroWidthSpacer) {
+ mKeys.add(key);
+ updateHistogram(key);
+ }
+ if (key.mCode == Keyboard.CODE_SHIFT) {
+ mShiftKeys.add(key);
+ }
+ if (key.altCodeWhileTyping()) {
+ mAltCodeKeysWhileTyping.add(key);
+ }
+ }
- public int getHorizontalGap() {
- return mDefaultHorizontalGap;
- }
+ private int mMaxHeightCount = 0;
+ private int mMaxWidthCount = 0;
+ private final HashMap<Integer, Integer> mHeightHistogram = new HashMap<Integer, Integer>();
+ private final HashMap<Integer, Integer> mWidthHistogram = new HashMap<Integer, Integer>();
- public void setHorizontalGap(int gap) {
- mDefaultHorizontalGap = gap;
- }
+ private void clearHistogram() {
+ mMostCommonKeyHeight = 0;
+ mMaxHeightCount = 0;
+ mHeightHistogram.clear();
- public int getVerticalGap() {
- return mDefaultVerticalGap;
- }
+ mMaxWidthCount = 0;
+ mMostCommonKeyWidth = 0;
+ mWidthHistogram.clear();
+ }
- public void setVerticalGap(int gap) {
- mDefaultVerticalGap = gap;
- }
+ private static int updateHistogramCounter(HashMap<Integer, Integer> histogram,
+ Integer key) {
+ final int count = (histogram.containsKey(key) ? histogram.get(key) : 0) + 1;
+ histogram.put(key, count);
+ return count;
+ }
- public int getRowHeight() {
- return mDefaultHeight;
- }
+ private void updateHistogram(Key key) {
+ final Integer height = key.mHeight + key.mVerticalGap;
+ final int heightCount = updateHistogramCounter(mHeightHistogram, height);
+ if (heightCount > mMaxHeightCount) {
+ mMaxHeightCount = heightCount;
+ mMostCommonKeyHeight = height;
+ }
- public void setRowHeight(int height) {
- mDefaultHeight = height;
+ final Integer width = key.mWidth + key.mHorizontalGap;
+ final int widthCount = updateHistogramCounter(mWidthHistogram, width);
+ if (widthCount > mMaxWidthCount) {
+ mMaxWidthCount = widthCount;
+ mMostCommonKeyWidth = width;
+ }
+ }
}
- public int getKeyWidth() {
- return mDefaultWidth;
+ /**
+ * Returns the array of the keys that are closest to the given point.
+ * @param x the x-coordinate of the point
+ * @param y the y-coordinate of the point
+ * @return the array of the nearest keys to the given point. If the given
+ * point is out of range, then an array of size zero is returned.
+ */
+ public Key[] getNearestKeys(int x, int y) {
+ // Avoid dead pixels at edges of the keyboard
+ final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
+ final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
+ return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
}
- public void setKeyWidth(int width) {
- mDefaultWidth = width;
- final int threshold = (int) (width * SEARCH_DISTANCE);
- mProximityThreshold = threshold * threshold;
+ public static String printableCode(int code) {
+ switch (code) {
+ case CODE_SHIFT: return "shift";
+ case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
+ case CODE_OUTPUT_TEXT: return "text";
+ case CODE_DELETE: return "delete";
+ case CODE_SETTINGS: return "settings";
+ case CODE_SHORTCUT: return "shortcut";
+ case CODE_ACTION_ENTER: return "actionEnter";
+ case CODE_ACTION_NEXT: return "actionNext";
+ case CODE_ACTION_PREVIOUS: return "actionPrevious";
+ case CODE_LANGUAGE_SWITCH: return "languageSwitch";
+ case CODE_UNSPECIFIED: return "unspec";
+ case CODE_TAB: return "tab";
+ case CODE_ENTER: return "enter";
+ default:
+ if (code <= 0) Log.w(TAG, "Unknown non-positive key code=" + code);
+ if (code < CODE_SPACE) return String.format("'\\u%02x'", code);
+ if (code < 0x100) return String.format("'%c'", code);
+ return String.format("'\\u%04x'", code);
+ }
}
- /**
- * Returns the total height of the keyboard
- * @return the total height of the keyboard
+ /**
+ * Keyboard Building helper.
+ *
+ * This class parses Keyboard XML file and eventually build a Keyboard.
+ * The Keyboard XML file looks like:
+ * <pre>
+ * &lt;!-- xml/keyboard.xml --&gt;
+ * &lt;Keyboard keyboard_attributes*&gt;
+ * &lt;!-- Keyboard Content --&gt;
+ * &lt;Row row_attributes*&gt;
+ * &lt;!-- Row Content --&gt;
+ * &lt;Key key_attributes* /&gt;
+ * &lt;Spacer horizontalGap="32.0dp" /&gt;
+ * &lt;include keyboardLayout="@xml/other_keys"&gt;
+ * ...
+ * &lt;/Row&gt;
+ * &lt;include keyboardLayout="@xml/other_rows"&gt;
+ * ...
+ * &lt;/Keyboard&gt;
+ * </pre>
+ * The XML file which is included in other file must have &lt;merge&gt; as root element,
+ * such as:
+ * <pre>
+ * &lt;!-- xml/other_keys.xml --&gt;
+ * &lt;merge&gt;
+ * &lt;Key key_attributes* /&gt;
+ * ...
+ * &lt;/merge&gt;
+ * </pre>
+ * and
+ * <pre>
+ * &lt;!-- xml/other_rows.xml --&gt;
+ * &lt;merge&gt;
+ * &lt;Row row_attributes*&gt;
+ * &lt;Key key_attributes* /&gt;
+ * &lt;/Row&gt;
+ * ...
+ * &lt;/merge&gt;
+ * </pre>
+ * You can also use switch-case-default tags to select Rows and Keys.
+ * <pre>
+ * &lt;switch&gt;
+ * &lt;case case_attribute*&gt;
+ * &lt;!-- Any valid tags at switch position --&gt;
+ * &lt;/case&gt;
+ * ...
+ * &lt;default&gt;
+ * &lt;!-- Any valid tags at switch position --&gt;
+ * &lt;/default&gt;
+ * &lt;/switch&gt;
+ * </pre>
+ * You can declare Key style and specify styles within Key tags.
+ * <pre>
+ * &lt;switch&gt;
+ * &lt;case mode="email"&gt;
+ * &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel=".com"
+ * /&gt;
+ * &lt;/case&gt;
+ * &lt;case mode="url"&gt;
+ * &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel="http://"
+ * /&gt;
+ * &lt;/case&gt;
+ * &lt;/switch&gt;
+ * ...
+ * &lt;Key keyStyle="shift-key" ... /&gt;
+ * </pre>
*/
- public int getHeight() {
- return mTotalHeight;
- }
- public void setHeight(int height) {
- mTotalHeight = height;
- }
+ public static class Builder<KP extends Params> {
+ private static final String BUILDER_TAG = "Keyboard.Builder";
+ private static final boolean DEBUG = false;
+
+ // Keyboard XML Tags
+ private static final String TAG_KEYBOARD = "Keyboard";
+ private static final String TAG_ROW = "Row";
+ private static final String TAG_KEY = "Key";
+ private static final String TAG_SPACER = "Spacer";
+ private static final String TAG_INCLUDE = "include";
+ private static final String TAG_MERGE = "merge";
+ private static final String TAG_SWITCH = "switch";
+ private static final String TAG_CASE = "case";
+ private static final String TAG_DEFAULT = "default";
+ public static final String TAG_KEY_STYLE = "key-style";
+
+ private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
+ private static final int DEFAULT_KEYBOARD_ROWS = 4;
+
+ protected final KP mParams;
+ protected final Context mContext;
+ protected final Resources mResources;
+ private final DisplayMetrics mDisplayMetrics;
+
+ private int mCurrentY = 0;
+ private Row mCurrentRow = null;
+ private boolean mLeftEdge;
+ private boolean mTopEdge;
+ private Key mRightEdgeKey = null;
+
+ /**
+ * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
+ * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
+ * defines.
+ */
+ public static class Row {
+ // keyWidth enum constants
+ private static final int KEYWIDTH_NOT_ENUM = 0;
+ private static final int KEYWIDTH_FILL_RIGHT = -1;
+
+ private final Params mParams;
+ /** Default width of a key in this row. */
+ private float mDefaultKeyWidth;
+ /** Default height of a key in this row. */
+ public final int mRowHeight;
+ /** Default keyLabelFlags in this row. */
+ private int mDefaultKeyLabelFlags;
+ /** Default backgroundType for this row */
+ private int mDefaultBackgroundType;
+
+ private final int mCurrentY;
+ // Will be updated by {@link Key}'s constructor.
+ private float mCurrentX;
+
+ public Row(Resources res, Params params, XmlPullParser parser, int y) {
+ mParams = params;
+ TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight,
+ params.mBaseHeight, params.mDefaultRowHeight);
+ keyboardAttr.recycle();
+ TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth,
+ params.mBaseWidth, params.mDefaultKeyWidth);
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ Key.BACKGROUND_TYPE_NORMAL);
+ keyAttr.recycle();
+
+ // TODO: Initialize this with <Row> attribute as backgroundType is done.
+ mDefaultKeyLabelFlags = 0;
+ mCurrentY = y;
+ mCurrentX = 0.0f;
+ }
- public int getMinWidth() {
- return mMinWidth;
- }
+ public float getDefaultKeyWidth() {
+ return mDefaultKeyWidth;
+ }
- public void setMinWidth(int minWidth) {
- mMinWidth = minWidth;
- }
+ public void setDefaultKeyWidth(float defaultKeyWidth) {
+ mDefaultKeyWidth = defaultKeyWidth;
+ }
- public int getDisplayHeight() {
- return mDisplayHeight;
- }
+ public int getDefaultKeyLabelFlags() {
+ return mDefaultKeyLabelFlags;
+ }
- public int getDisplayWidth() {
- return mDisplayWidth;
- }
+ public void setDefaultKeyLabelFlags(int keyLabelFlags) {
+ mDefaultKeyLabelFlags = keyLabelFlags;
+ }
- public int getKeyboardHeight() {
- return mKeyboardHeight;
- }
+ public int getDefaultBackgroundType() {
+ return mDefaultBackgroundType;
+ }
- public void setKeyboardHeight(int height) {
- mKeyboardHeight = height;
- }
+ public void setDefaultBackgroundType(int backgroundType) {
+ mDefaultBackgroundType = backgroundType;
+ }
- public int getPopupKeyboardResId() {
- return mPopupKeyboardResId;
- }
+ public void setXPos(float keyXPos) {
+ mCurrentX = keyXPos;
+ }
- public void setPopupKeyboardResId(int resId) {
- mPopupKeyboardResId = resId;
- }
+ public void advanceXPos(float width) {
+ mCurrentX += width;
+ }
- public int getMaxPopupKeyboardColumn() {
- return mMaxPopupColumn;
- }
+ public int getKeyY() {
+ return mCurrentY;
+ }
- public void setMaxPopupKeyboardColumn(int column) {
- mMaxPopupColumn = column;
- }
+ public float getKeyX(TypedArray keyAttr) {
+ final int widthType = Builder.getEnumValue(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
+
+ final int keyboardRightEdge = mParams.mOccupiedWidth
+ - mParams.mHorizontalEdgesPadding;
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+ final float keyXPos = Builder.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
+ if (keyXPos < 0) {
+ // If keyXPos is negative, the actual x-coordinate will be
+ // keyboardWidth + keyXPos.
+ // keyXPos shouldn't be less than mCurrentX because drawable area for this
+ // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
+ // its left hand side.
+ return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
+ } else {
+ return keyXPos + mParams.mHorizontalEdgesPadding;
+ }
+ }
+ return mCurrentX;
+ }
- public List<Key> getShiftKeys() {
- return mShiftKeys;
- }
+ public float getKeyWidth(TypedArray keyAttr) {
+ return getKeyWidth(keyAttr, mCurrentX);
+ }
- public Map<Key, Drawable> getShiftedIcons() {
- return mShiftedIcons;
- }
+ public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
+ final int widthType = Builder.getEnumValue(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
+ switch (widthType) {
+ case KEYWIDTH_FILL_RIGHT:
+ final int keyboardRightEdge =
+ mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
+ // If keyWidth is fillRight, the actual key width will be determined to fill
+ // out the area up to the right edge of the keyboard.
+ return keyboardRightEdge - keyXPos;
+ default: // KEYWIDTH_NOT_ENUM
+ return Builder.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth,
+ mParams.mBaseWidth, mDefaultKeyWidth);
+ }
+ }
+ }
+
+ public Builder(Context context, KP params) {
+ mContext = context;
+ final Resources res = context.getResources();
+ mResources = res;
+ mDisplayMetrics = res.getDisplayMetrics();
- public void enableShiftLock() {
- for (final Key key : getShiftKeys()) {
- mShiftLockEnabled.add(key);
- mNormalShiftIcons.put(key, key.getIcon());
+ mParams = params;
+
+ params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
+ params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
}
- }
- public boolean isShiftLockEnabled(Key key) {
- return mShiftLockEnabled.contains(key);
- }
+ public void setAutoGenerate(KeyboardLayoutSet.KeysCache keysCache) {
+ mParams.mKeysCache = keysCache;
+ }
+
+ public Builder<KP> load(int xmlId, KeyboardId id) {
+ mParams.mId = id;
+ final XmlResourceParser parser = mResources.getXml(xmlId);
+ try {
+ parseKeyboard(parser);
+ } catch (XmlPullParserException e) {
+ Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+ throw new IllegalArgumentException(e);
+ } catch (IOException e) {
+ Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+ return this;
+ }
- public boolean setShiftLocked(boolean newShiftLockState) {
- final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
- for (final Key key : getShiftKeys()) {
- key.setHighlightOn(newShiftLockState);
- key.setIcon(newShiftLockState ? shiftedIcons.get(key) : mNormalShiftIcons.get(key));
+ // TODO: Remove this method.
+ public void setTouchPositionCorrectionEnabled(boolean enabled) {
+ mParams.mTouchPositionCorrection.setEnabled(enabled);
}
- mShiftState.setShiftLocked(newShiftLockState);
- return true;
- }
- public boolean isShiftLocked() {
- return mShiftState.isShiftLocked();
- }
+ public void setProximityCharsCorrectionEnabled(boolean enabled) {
+ mParams.mProximityCharsCorrectionEnabled = enabled;
+ }
+
+ public Keyboard build() {
+ return new Keyboard(mParams);
+ }
+
+ private int mIndent;
+ private static final String SPACES = " ";
+
+ private static String spaces(int count) {
+ return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
+ }
+
+ private void startTag(String format, Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+ }
+
+ private void endTag(String format, Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
+ }
- public boolean setShifted(boolean newShiftState) {
- final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
- for (final Key key : getShiftKeys()) {
- if (!newShiftState && !mShiftState.isShiftLocked()) {
- key.setIcon(mNormalShiftIcons.get(key));
- } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) {
- key.setIcon(shiftedIcons.get(key));
+ private void startEndTag(String format, Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+ mIndent--;
+ }
+
+ private void parseKeyboard(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD.equals(tag)) {
+ parseKeyboardAttributes(parser);
+ startKeyboard();
+ parseKeyboardContent(parser, false);
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
+ }
+ }
}
}
- return mShiftState.setShifted(newShiftState);
- }
- public boolean isShiftedOrShiftLocked() {
- return mShiftState.isShiftedOrShiftLocked();
- }
+ private void parseKeyboardAttributes(XmlPullParser parser) {
+ final int displayWidth = mDisplayMetrics.widthPixels;
+ final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
+ Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
+ R.style.Keyboard);
+ final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ try {
+ final int displayHeight = mDisplayMetrics.heightPixels;
+ final String keyboardHeightString = Utils.getDeviceOverrideValue(
+ mResources, R.array.keyboard_heights, null);
+ final float keyboardHeight;
+ if (keyboardHeightString != null) {
+ keyboardHeight = Float.parseFloat(keyboardHeightString)
+ * mDisplayMetrics.density;
+ } else {
+ keyboardHeight = keyboardAttr.getDimension(
+ R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
+ }
+ final float maxKeyboardHeight = getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
+ float minKeyboardHeight = getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
+ if (minKeyboardHeight < 0) {
+ // Specified fraction was negative, so it should be calculated against display
+ // width.
+ minKeyboardHeight = -getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
+ }
+ final Params params = mParams;
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ params.mOccupiedHeight = (int)Math.max(
+ Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ params.mOccupiedWidth = params.mId.mWidth;
+ params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
+ params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
+ params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
+ mParams.mOccupiedWidth, 0);
+
+ params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
+ - params.mHorizontalCenterPadding;
+ params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
+ params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
+ params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
+ params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
+ params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
+ - params.mBottomPadding + params.mVerticalGap;
+ params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight, params.mBaseHeight,
+ params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
+
+ params.mMoreKeysTemplate = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_moreKeysTemplate, 0);
+ params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
+ R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
+
+ params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
+ params.mIconsSet.loadIcons(keyboardAttr);
+ final String language = params.mId.mLocale.getLanguage();
+ params.mCodesSet.setLanguage(language);
+ params.mTextsSet.setLanguage(language);
+ final RunInLocale<Void> job = new RunInLocale<Void>() {
+ @Override
+ protected Void job(Resources res) {
+ params.mTextsSet.loadStringResources(mContext);
+ return null;
+ }
+ };
+ // Null means the current system locale.
+ final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
+ ? null : params.mId.mLocale;
+ job.runInLocale(mResources, locale);
+
+ final int resourceId = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_touchPositionCorrectionData, 0);
+ params.mTouchPositionCorrection.setEnabled(resourceId != 0);
+ if (resourceId != 0) {
+ final String[] data = mResources.getStringArray(resourceId);
+ params.mTouchPositionCorrection.load(data);
+ }
+ } finally {
+ keyAttr.recycle();
+ keyboardAttr.recycle();
+ }
+ }
- public void setAutomaticTemporaryUpperCase() {
- setShifted(true);
- mShiftState.setAutomaticTemporaryUpperCase();
- }
+ private void parseKeyboardContent(XmlPullParser parser, boolean skip)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_ROW.equals(tag)) {
+ Row row = parseRowAttributes(parser);
+ if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
+ if (!skip) {
+ startRow(row);
+ }
+ parseRowContent(parser, row, skip);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeKeyboardContent(parser, skip);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchKeyboardContent(parser, skip);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (DEBUG) endTag("</%s>", tag);
+ if (TAG_KEYBOARD.equals(tag)) {
+ endKeyboard();
+ break;
+ } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+ || TAG_MERGE.equals(tag)) {
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW);
+ }
+ }
+ }
+ }
- public boolean isAutomaticTemporaryUpperCase() {
- return isAlphaKeyboard() && mShiftState.isAutomaticTemporaryUpperCase();
- }
+ private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException {
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ try {
+ if (a.hasValue(R.styleable.Keyboard_horizontalGap))
+ throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
+ if (a.hasValue(R.styleable.Keyboard_verticalGap))
+ throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
+ return new Row(mResources, mParams, parser, mCurrentY);
+ } finally {
+ a.recycle();
+ }
+ }
- public boolean isManualTemporaryUpperCase() {
- return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCase();
- }
+ private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEY.equals(tag)) {
+ parseKey(parser, row, skip);
+ } else if (TAG_SPACER.equals(tag)) {
+ parseSpacer(parser, row, skip);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeRowContent(parser, row, skip);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchRowContent(parser, row, skip);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (DEBUG) endTag("</%s>", tag);
+ if (TAG_ROW.equals(tag)) {
+ if (!skip) {
+ endRow(row);
+ }
+ break;
+ } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
+ || TAG_MERGE.equals(tag)) {
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
+ }
+ }
+ }
+ }
- public boolean isManualTemporaryUpperCaseFromAuto() {
- return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCaseFromAuto();
- }
+ private void parseKey(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
+ } else {
+ final Key key = new Key(mResources, mParams, row, parser);
+ if (DEBUG) {
+ startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
+ (key.isEnabled() ? "" : " disabled"), key,
+ Arrays.toString(key.mMoreKeys));
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ endKey(key);
+ }
+ }
- public KeyboardShiftState getKeyboardShiftState() {
- return mShiftState;
- }
+ private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
+ XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+ if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
+ } else {
+ final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
+ if (DEBUG) startEndTag("<%s />", TAG_SPACER);
+ XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+ endKey(spacer);
+ }
+ }
- public boolean isAlphaKeyboard() {
- return mId != null && mId.isAlphabetKeyboard();
- }
+ private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
+ throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, null, skip);
+ }
- public boolean isPhoneKeyboard() {
- return mId != null && mId.isPhoneKeyboard();
- }
+ private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, row, skip);
+ }
- public boolean isNumberKeyboard() {
- return mId != null && mId.isNumberKeyboard();
- }
+ private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
+ XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+ if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
+ } else {
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
+ R.styleable.Keyboard_Include);
+ final TypedArray keyAttr = mResources.obtainAttributes(attr,
+ R.styleable.Keyboard_Key);
+ int keyboardLayout = 0;
+ float savedDefaultKeyWidth = 0;
+ int savedDefaultKeyLabelFlags = 0;
+ int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
+ try {
+ XmlParseUtils.checkAttributeExists(keyboardAttr,
+ R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
+ TAG_INCLUDE, parser);
+ keyboardLayout = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_Include_keyboardLayout, 0);
+ if (row != null) {
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+ // Override current x coordinate.
+ row.setXPos(row.getKeyX(keyAttr));
+ }
+ // TODO: Remove this if-clause and do the same as backgroundType below.
+ savedDefaultKeyWidth = row.getDefaultKeyWidth();
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
+ // Override default key width.
+ row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
+ }
+ savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
+ // Bitwise-or default keyLabelFlag if exists.
+ row.setDefaultKeyLabelFlags(keyAttr.getInt(
+ R.styleable.Keyboard_Key_keyLabelFlags, 0)
+ | savedDefaultKeyLabelFlags);
+ savedDefaultBackgroundType = row.getDefaultBackgroundType();
+ // Override default backgroundType if exists.
+ row.setDefaultBackgroundType(keyAttr.getInt(
+ R.styleable.Keyboard_Key_backgroundType,
+ savedDefaultBackgroundType));
+ }
+ } finally {
+ keyboardAttr.recycle();
+ keyAttr.recycle();
+ }
- // TODO: Move this function to ProximityInfo and make this private.
- public void computeNearestNeighbors() {
- // Round-up so we don't have any pixels outside the grid
- mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
- mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
- mGridNeighbors = new int[GRID_SIZE][];
- final int[] indices = new int[mKeys.size()];
- final int gridWidth = GRID_WIDTH * mCellWidth;
- final int gridHeight = GRID_HEIGHT * mCellHeight;
- final int threshold = mProximityThreshold;
- for (int x = 0; x < gridWidth; x += mCellWidth) {
- for (int y = 0; y < gridHeight; y += mCellHeight) {
- final int centerX = x + mCellWidth / 2;
- final int centerY = y + mCellHeight / 2;
- int count = 0;
- for (int i = 0; i < mKeys.size(); i++) {
- final Key key = mKeys.get(i);
- if (key.squaredDistanceToEdge(centerX, centerY) < threshold)
- indices[count++] = i;
+ XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+ if (DEBUG) {
+ startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
+ mResources.getResourceEntryName(keyboardLayout));
+ }
+ final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
+ try {
+ parseMerge(parserForInclude, row, skip);
+ } finally {
+ if (row != null) {
+ // Restore default keyWidth, keyLabelFlags, and backgroundType.
+ row.setDefaultKeyWidth(savedDefaultKeyWidth);
+ row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
+ row.setDefaultBackgroundType(savedDefaultBackgroundType);
+ }
+ parserForInclude.close();
}
- final int[] cell = new int[count];
- System.arraycopy(indices, 0, cell, 0, count);
- mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
}
}
- mProximityInfo.setProximityInfo(mGridNeighbors, getMinWidth(), getHeight(), mKeys);
- }
- /**
- * Returns the indices of the keys that are closest to the given point.
- * @param x the x-coordinate of the point
- * @param y the y-coordinate of the point
- * @return the array of integer indices for the nearest keys to the given point. If the given
- * point is out of range, then an array of size zero is returned.
- */
- public int[] getNearestKeys(int x, int y) {
- if (mGridNeighbors == null) computeNearestNeighbors();
- if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
- int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
- if (index < GRID_SIZE) {
- return mGridNeighbors[index];
+ private void parseMerge(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s>", TAG_MERGE);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_MERGE.equals(tag)) {
+ if (row == null) {
+ parseKeyboardContent(parser, skip);
+ } else {
+ parseRowContent(parser, row, skip);
+ }
+ break;
+ } else {
+ throw new XmlParseUtils.ParseException(
+ "Included keyboard layout must have <merge> root element", parser);
+ }
+ }
}
}
- return EMPTY_INT_ARRAY;
- }
- /**
- * Compute the most common key width in order to use it as proximity key detection threshold.
- *
- * @return The most common key width in the keyboard
- */
- public int getMostCommonKeyWidth() {
- if (mMostCommonKeyWidth == 0) {
- final HashMap<Integer, Integer> histogram = new HashMap<Integer, Integer>();
- int maxCount = 0;
- int mostCommonWidth = 0;
- for (final Key key : mKeys) {
- final Integer width = key.mWidth + key.mGap;
- Integer count = histogram.get(width);
- if (count == null)
- count = 0;
- histogram.put(width, ++count);
- if (count > maxCount) {
- maxCount = count;
- mostCommonWidth = width;
+ private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
+ throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, null, skip);
+ }
+
+ private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, row, skip);
+ }
+
+ private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
+ boolean selected = false;
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_CASE.equals(tag)) {
+ selected |= parseCase(parser, row, selected ? true : skip);
+ } else if (TAG_DEFAULT.equals(tag)) {
+ selected |= parseDefault(parser, row, selected ? true : skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_SWITCH.equals(tag)) {
+ if (DEBUG) endTag("</%s>", TAG_SWITCH);
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
+ }
}
}
- mMostCommonKeyWidth = mostCommonWidth;
}
- return mMostCommonKeyWidth;
- }
- /**
- * Return true if spacebar needs showing preview even when "popup on keypress" is off.
- * @param keyIndex index of the pressing key
- * @return true if spacebar needs showing preview
- */
- public boolean needSpacebarPreview(int keyIndex) {
- return false;
- }
+ private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ final boolean selected = parseCaseCondition(parser);
+ if (row == null) {
+ // Processing Rows.
+ parseKeyboardContent(parser, selected ? skip : true);
+ } else {
+ // Processing Keys.
+ parseRowContent(parser, row, selected ? skip : true);
+ }
+ return selected;
+ }
+
+ private boolean parseCaseCondition(XmlPullParser parser) {
+ final KeyboardId id = mParams.mId;
+ if (id == null)
+ return true;
+
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Case);
+ try {
+ final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
+ R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
+ KeyboardId.elementIdToName(id.mElementId));
+ final boolean modeMatched = matchTypedValue(a,
+ R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
+ final boolean navigateNextMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
+ final boolean navigatePreviousMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
+ final boolean passwordInputMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
+ final boolean clobberSettingsKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
+ final boolean shortcutKeyEnabledMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
+ final boolean hasShortcutKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
+ final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ id.mLanguageSwitchKeyEnabled);
+ final boolean isMultiLineMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
+ final boolean imeActionMatched = matchInteger(a,
+ R.styleable.Keyboard_Case_imeAction, id.imeAction());
+ final boolean localeCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
+ final boolean languageCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
+ final boolean countryCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
+ final boolean selected = keyboardLayoutSetElementMatched && modeMatched
+ && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
+ && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
+ && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched
+ && isMultiLineMatched && imeActionMatched && localeCodeMatched
+ && languageCodeMatched && countryCodeMatched;
+
+ if (DEBUG) {
+ startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
+ textAttr(a.getString(
+ R.styleable.Keyboard_Case_keyboardLayoutSetElement),
+ "keyboardLayoutSetElement"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
+ "imeAction"),
+ booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
+ "navigateNext"),
+ booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
+ "navigatePrevious"),
+ booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+ "clobberSettingsKey"),
+ booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
+ "passwordInput"),
+ booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
+ "shortcutKeyEnabled"),
+ booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
+ "hasShortcutKey"),
+ booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ "languageSwitchKeyEnabled"),
+ booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
+ "isMultiLine"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
+ "localeCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
+ "languageCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
+ "countryCode"),
+ selected ? "" : " skipped");
+ }
- private void loadKeyboard(Context context, int xmlLayoutResId) {
- try {
- KeyboardParser parser = new KeyboardParser(this, context);
- parser.parseKeyboard(xmlLayoutResId);
- // mMinWidth is the width of this keyboard which is maximum width of row.
- mMinWidth = parser.getMaxRowWidth();
- mTotalHeight = parser.getTotalHeight();
- } catch (XmlPullParserException e) {
- Log.w(TAG, "keyboard XML parse error: " + e);
- throw new IllegalArgumentException(e);
- } catch (IOException e) {
- Log.w(TAG, "keyboard XML parse error: " + e);
- throw new RuntimeException(e);
+ return selected;
+ } finally {
+ a.recycle();
+ }
+ }
+
+ private static boolean matchInteger(TypedArray a, int index, int value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index) || a.getInt(index, 0) == value;
+ }
+
+ private static boolean matchBoolean(TypedArray a, int index, boolean value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index) || a.getBoolean(index, false) == value;
+ }
+
+ private static boolean matchString(TypedArray a, int index, String value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index)
+ || stringArrayContains(a.getString(index).split("\\|"), value);
+ }
+
+ private static boolean matchTypedValue(TypedArray a, int index, int intValue,
+ String strValue) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ final TypedValue v = a.peekValue(index);
+ if (v == null)
+ return true;
+
+ if (isIntegerValue(v)) {
+ return intValue == a.getInt(index, 0);
+ } else if (isStringValue(v)) {
+ return stringArrayContains(a.getString(index).split("\\|"), strValue);
+ }
+ return false;
+ }
+
+ private static boolean stringArrayContains(String[] array, String value) {
+ for (final String elem : array) {
+ if (elem.equals(value))
+ return true;
+ }
+ return false;
+ }
+
+ private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s>", TAG_DEFAULT);
+ if (row == null) {
+ parseKeyboardContent(parser, skip);
+ } else {
+ parseRowContent(parser, row, skip);
+ }
+ return true;
+ }
+
+ private void parseKeyStyle(XmlPullParser parser, boolean skip)
+ throws XmlPullParserException, IOException {
+ TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_KeyStyle);
+ TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ try {
+ if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
+ throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
+ + "/> needs styleName attribute", parser);
+ if (DEBUG) {
+ startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
+ keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
+ skip ? " skipped" : "");
+ }
+ if (!skip)
+ mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
+ } finally {
+ keyStyleAttr.recycle();
+ keyAttrs.recycle();
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
+ }
+
+ private void startKeyboard() {
+ mCurrentY += mParams.mTopPadding;
+ mTopEdge = true;
+ }
+
+ private void startRow(Row row) {
+ addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ mCurrentRow = row;
+ mLeftEdge = true;
+ mRightEdgeKey = null;
}
- }
- public static void setDefaultBounds(Drawable drawable) {
- if (drawable != null)
- drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
- drawable.getIntrinsicHeight());
+ private void endRow(Row row) {
+ if (mCurrentRow == null)
+ throw new InflateException("orphan end row tag");
+ if (mRightEdgeKey != null) {
+ mRightEdgeKey.markAsRightEdge(mParams);
+ mRightEdgeKey = null;
+ }
+ addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ mCurrentY += row.mRowHeight;
+ mCurrentRow = null;
+ mTopEdge = false;
+ }
+
+ private void endKey(Key key) {
+ mParams.onAddKey(key);
+ if (mLeftEdge) {
+ key.markAsLeftEdge(mParams);
+ mLeftEdge = false;
+ }
+ if (mTopEdge) {
+ key.markAsTopEdge(mParams);
+ }
+ mRightEdgeKey = key;
+ }
+
+ private void endKeyboard() {
+ // nothing to do here.
+ }
+
+ private void addEdgeSpace(float width, Row row) {
+ row.advanceXPos(width);
+ mLeftEdge = false;
+ mRightEdgeKey = null;
+ }
+
+ public static float getDimensionOrFraction(TypedArray a, int index, int base,
+ float defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null)
+ return defValue;
+ if (isFractionValue(value)) {
+ return a.getFraction(index, base, base, defValue);
+ } else if (isDimensionValue(value)) {
+ return a.getDimension(index, defValue);
+ }
+ return defValue;
+ }
+
+ public static int getEnumValue(TypedArray a, int index, int defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null)
+ return defValue;
+ if (isIntegerValue(value)) {
+ return a.getInt(index, defValue);
+ }
+ return defValue;
+ }
+
+ private static boolean isFractionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_FRACTION;
+ }
+
+ private static boolean isDimensionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_DIMENSION;
+ }
+
+ private static boolean isIntegerValue(TypedValue v) {
+ return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
+ }
+
+ private static boolean isStringValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_STRING;
+ }
+
+ private static String textAttr(String value, String name) {
+ return value != null ? String.format(" %s=%s", name, value) : "";
+ }
+
+ private static String booleanAttr(TypedArray a, int index, String name) {
+ return a.hasValue(index)
+ ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
+ }
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index 7e67d6f6b..275aacf36 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -24,10 +24,8 @@ public interface KeyboardActionListener {
*
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
* the value will be zero.
- * @param withSliding true if pressing has occurred because the user slid finger from other key
- * to this key without releasing the finger.
*/
- public void onPress(int primaryCode, boolean withSliding);
+ public void onPressKey(int primaryCode);
/**
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -37,27 +35,26 @@ public interface KeyboardActionListener {
* @param withSliding true if releasing has occurred because the user slid finger from the key
* to other key without releasing the finger.
*/
- public void onRelease(int primaryCode, boolean withSliding);
+ public void onReleaseKey(int primaryCode, boolean withSliding);
/**
* Send a key code to the listener.
*
* @param primaryCode this is the code of the key that was pressed
- * @param keyCodes the codes for all the possible alternative keys with the primary code being
- * the first. If the primary key code is a single character such as an alphabet or
- * number or symbol, the alternatives will include other characters that may be on
- * the same key or adjacent keys. These codes are useful to correct for accidental
- * presses of a key adjacent to the intended key.
* @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
- * {@link PointerTracker#onTouchEvent} or so, the value should be
- * {@link #NOT_A_TOUCH_COORDINATE}.
+ * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
+ * If it's called on insertion from the suggestion strip, it should be
+ * {@link #SUGGESTION_STRIP_COORDINATE}.
* @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
- * {@link PointerTracker#onTouchEvent} or so, the value should be
- * {@link #NOT_A_TOUCH_COORDINATE}.
+ * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
+ * If it's called on insertion from the suggestion strip, it should be
+ * {@link #SUGGESTION_STRIP_COORDINATE}.
*/
- public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y);
+ public void onCodeInput(int primaryCode, int x, int y);
public static final int NOT_A_TOUCH_COORDINATE = -1;
+ public static final int SUGGESTION_STRIP_COORDINATE = -2;
+ public static final int SPELL_CHECKER_COORDINATE = -3;
/**
* Sends a sequence of characters to the listener.
@@ -72,7 +69,25 @@ public interface KeyboardActionListener {
public void onCancelInput();
/**
- * Called when the user quickly moves the finger from up to down.
+ * Send a non-"code input" custom request to the listener.
+ * @return true if the request has been consumed, false otherwise.
*/
- public void onSwipeDown();
+ public boolean onCustomRequest(int requestCode);
+
+ public static class Adapter implements KeyboardActionListener {
+ @Override
+ public void onPressKey(int primaryCode) {}
+ @Override
+ public void onReleaseKey(int primaryCode, boolean withSliding) {}
+ @Override
+ public void onCodeInput(int primaryCode, int x, int y) {}
+ @Override
+ public void onTextInput(CharSequence text) {}
+ @Override
+ public void onCancelInput() {}
+ @Override
+ public boolean onCustomRequest(int requestCode) {
+ return false;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 9c63c198c..233716acf 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -16,18 +16,22 @@
package com.android.inputmethod.keyboard;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+
+import android.text.InputType;
+import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.compat.InputTypeCompatUtils;
-import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.InputTypeUtils;
+import com.android.inputmethod.latin.SubtypeLocale;
import java.util.Arrays;
import java.util.Locale;
/**
- * Represents the parameters necessary to construct a new LatinKeyboard,
- * which also serve as a unique identifier for each keyboard type.
+ * Unique identifier for each keyboard type.
*/
public class KeyboardId {
public static final int MODE_TEXT = 0;
@@ -36,118 +40,134 @@ public class KeyboardId {
public static final int MODE_IM = 3;
public static final int MODE_PHONE = 4;
public static final int MODE_NUMBER = 5;
-
- public static final int F2KEY_MODE_NONE = 0;
- public static final int F2KEY_MODE_SETTINGS = 1;
- public static final int F2KEY_MODE_SHORTCUT_IME = 2;
- public static final int F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS = 3;
-
+ public static final int MODE_DATE = 6;
+ public static final int MODE_TIME = 7;
+ public static final int MODE_DATETIME = 8;
+
+ public static final int ELEMENT_ALPHABET = 0;
+ public static final int ELEMENT_ALPHABET_MANUAL_SHIFTED = 1;
+ public static final int ELEMENT_ALPHABET_AUTOMATIC_SHIFTED = 2;
+ public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3;
+ public static final int ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED = 4;
+ public static final int ELEMENT_SYMBOLS = 5;
+ public static final int ELEMENT_SYMBOLS_SHIFTED = 6;
+ public static final int ELEMENT_PHONE = 7;
+ public static final int ELEMENT_PHONE_SYMBOLS = 8;
+ public static final int ELEMENT_NUMBER = 9;
+
+ private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
+
+ public final InputMethodSubtype mSubtype;
public final Locale mLocale;
public final int mOrientation;
public final int mWidth;
public final int mMode;
- public final int mXmlId;
- public final boolean mNavigateAction;
- public final boolean mPasswordInput;
- // TODO: Clean up these booleans and modes.
- public final boolean mHasSettingsKey;
- public final int mF2KeyMode;
+ public final int mElementId;
+ private final EditorInfo mEditorInfo;
public final boolean mClobberSettingsKey;
- public final boolean mVoiceKeyEnabled;
- public final boolean mHasVoiceKey;
- public final int mImeAction;
- public final boolean mEnableShiftLock;
-
- public final String mXmlName;
- public final EditorInfo mAttribute;
+ public final boolean mShortcutKeyEnabled;
+ public final boolean mHasShortcutKey;
+ public final boolean mLanguageSwitchKeyEnabled;
+ public final String mCustomActionLabel;
private final int mHashCode;
- public KeyboardId(String xmlName, int xmlId, Locale locale, int orientation, int width,
- int mode, EditorInfo attribute, boolean hasSettingsKey, int f2KeyMode,
- boolean clobberSettingsKey, boolean voiceKeyEnabled, boolean hasVoiceKey,
- boolean enableShiftLock) {
- final int inputType = (attribute != null) ? attribute.inputType : 0;
- final int imeOptions = (attribute != null) ? attribute.imeOptions : 0;
- this.mLocale = locale;
- this.mOrientation = orientation;
- this.mWidth = width;
- this.mMode = mode;
- this.mXmlId = xmlId;
- // Note: Turn off checking navigation flag to show TAB key for now.
- this.mNavigateAction = InputTypeCompatUtils.isWebInputType(inputType);
-// || EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
-// || EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions);
- this.mPasswordInput = InputTypeCompatUtils.isPasswordInputType(inputType)
- || InputTypeCompatUtils.isVisiblePasswordInputType(inputType);
- this.mHasSettingsKey = hasSettingsKey;
- this.mF2KeyMode = f2KeyMode;
- this.mClobberSettingsKey = clobberSettingsKey;
- this.mVoiceKeyEnabled = voiceKeyEnabled;
- this.mHasVoiceKey = hasVoiceKey;
- // We are interested only in {@link EditorInfo#IME_MASK_ACTION} enum value and
- // {@link EditorInfo#IME_FLAG_NO_ENTER_ACTION}.
- this.mImeAction = imeOptions & (
- EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION);
- this.mEnableShiftLock = enableShiftLock;
-
- this.mXmlName = xmlName;
- this.mAttribute = attribute;
-
- this.mHashCode = Arrays.hashCode(new Object[] {
- locale,
- orientation,
- width,
- mode,
- xmlId,
- mNavigateAction,
- mPasswordInput,
- hasSettingsKey,
- f2KeyMode,
- clobberSettingsKey,
- voiceKeyEnabled,
- hasVoiceKey,
- mImeAction,
- enableShiftLock,
+ public KeyboardId(int elementId, InputMethodSubtype subtype, int orientation, int width,
+ int mode, EditorInfo editorInfo, boolean clobberSettingsKey, boolean shortcutKeyEnabled,
+ boolean hasShortcutKey, boolean languageSwitchKeyEnabled) {
+ mSubtype = subtype;
+ mLocale = SubtypeLocale.getSubtypeLocale(subtype);
+ mOrientation = orientation;
+ mWidth = width;
+ mMode = mode;
+ mElementId = elementId;
+ mEditorInfo = editorInfo;
+ mClobberSettingsKey = clobberSettingsKey;
+ mShortcutKeyEnabled = shortcutKeyEnabled;
+ mHasShortcutKey = hasShortcutKey;
+ mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
+ mCustomActionLabel = (editorInfo.actionLabel != null)
+ ? editorInfo.actionLabel.toString() : null;
+
+ mHashCode = computeHashCode(this);
+ }
+
+ private static int computeHashCode(KeyboardId id) {
+ return Arrays.hashCode(new Object[] {
+ id.mOrientation,
+ id.mElementId,
+ id.mMode,
+ id.mWidth,
+ id.passwordInput(),
+ id.mClobberSettingsKey,
+ id.mShortcutKeyEnabled,
+ id.mHasShortcutKey,
+ id.mLanguageSwitchKeyEnabled,
+ id.isMultiLine(),
+ id.imeAction(),
+ id.mCustomActionLabel,
+ id.navigateNext(),
+ id.navigatePrevious(),
+ id.mSubtype
});
}
- public KeyboardId cloneWithNewLayout(String xmlName, int xmlId) {
- return new KeyboardId(xmlName, xmlId, mLocale, mOrientation, mWidth, mMode, mAttribute,
- mHasSettingsKey, mF2KeyMode, mClobberSettingsKey, mVoiceKeyEnabled, mHasVoiceKey,
- mEnableShiftLock);
+ private boolean equals(KeyboardId other) {
+ if (other == this)
+ return true;
+ return other.mOrientation == mOrientation
+ && other.mElementId == mElementId
+ && other.mMode == mMode
+ && other.mWidth == mWidth
+ && other.passwordInput() == passwordInput()
+ && other.mClobberSettingsKey == mClobberSettingsKey
+ && other.mShortcutKeyEnabled == mShortcutKeyEnabled
+ && other.mHasShortcutKey == mHasShortcutKey
+ && other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled
+ && other.isMultiLine() == isMultiLine()
+ && other.imeAction() == imeAction()
+ && TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel)
+ && other.navigateNext() == navigateNext()
+ && other.navigatePrevious() == navigatePrevious()
+ && other.mSubtype.equals(mSubtype);
}
- public KeyboardId cloneWithNewGeometry(int width) {
- if (mWidth == width)
- return this;
- return new KeyboardId(mXmlName, mXmlId, mLocale, mOrientation, width, mMode, mAttribute,
- mHasSettingsKey, mF2KeyMode, mClobberSettingsKey, mVoiceKeyEnabled, mHasVoiceKey,
- mEnableShiftLock);
+ public boolean isAlphabetKeyboard() {
+ return mElementId < ELEMENT_SYMBOLS;
}
- public int getXmlId() {
- return mXmlId;
+ public boolean navigateNext() {
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0;
}
- public boolean isAlphabetKeyboard() {
- return mXmlId == R.xml.kbd_qwerty;
+ public boolean navigatePrevious() {
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0;
}
- public boolean isSymbolsKeyboard() {
- return mXmlId == R.xml.kbd_symbols || mXmlId == R.xml.kbd_symbols_shift;
+ public boolean passwordInput() {
+ final int inputType = mEditorInfo.inputType;
+ return InputTypeUtils.isPasswordInputType(inputType)
+ || InputTypeUtils.isVisiblePasswordInputType(inputType);
}
- public boolean isPhoneKeyboard() {
- return mMode == MODE_PHONE;
+ public boolean isMultiLine() {
+ return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
- public boolean isPhoneSymbolsKeyboard() {
- return mXmlId == R.xml.kbd_phone_symbols;
+ public int imeAction() {
+ final int actionId = mEditorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ if ((mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
+ return EditorInfo.IME_ACTION_NONE;
+ } else if (mEditorInfo.actionLabel != null) {
+ return IME_ACTION_CUSTOM_LABEL;
+ } else {
+ return actionId;
+ }
}
- public boolean isNumberKeyboard() {
- return mMode == MODE_NUMBER;
+ public int imeActionId() {
+ final int actionId = imeAction();
+ return actionId == IME_ACTION_CUSTOM_LABEL ? mEditorInfo.actionId : actionId;
}
@Override
@@ -155,23 +175,6 @@ public class KeyboardId {
return other instanceof KeyboardId && equals((KeyboardId) other);
}
- boolean equals(KeyboardId other) {
- return other.mLocale.equals(this.mLocale)
- && other.mOrientation == this.mOrientation
- && other.mWidth == this.mWidth
- && other.mMode == this.mMode
- && other.mXmlId == this.mXmlId
- && other.mNavigateAction == this.mNavigateAction
- && other.mPasswordInput == this.mPasswordInput
- && other.mHasSettingsKey == this.mHasSettingsKey
- && other.mF2KeyMode == this.mF2KeyMode
- && other.mClobberSettingsKey == this.mClobberSettingsKey
- && other.mVoiceKeyEnabled == this.mVoiceKeyEnabled
- && other.mHasVoiceKey == this.mHasVoiceKey
- && other.mImeAction == this.mImeAction
- && other.mEnableShiftLock == this.mEnableShiftLock;
- }
-
@Override
public int hashCode() {
return mHashCode;
@@ -179,23 +182,48 @@ public class KeyboardId {
@Override
public String toString() {
- return String.format("[%s.xml %s %s%d %s %s %s%s%s%s%s%s%s%s]",
- mXmlName,
+ return String.format("[%s %s:%s %s%d %s %s %s%s%s%s%s%s%s%s]",
+ elementIdToName(mElementId),
mLocale,
+ mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
(mOrientation == 1 ? "port" : "land"), mWidth,
modeName(mMode),
- EditorInfoCompatUtils.imeOptionsName(mImeAction),
- f2KeyModeName(mF2KeyMode),
+ imeAction(),
+ (navigateNext() ? "navigateNext" : ""),
+ (navigatePrevious() ? "navigatePrevious" : ""),
(mClobberSettingsKey ? " clobberSettingsKey" : ""),
- (mNavigateAction ? " navigateAction" : ""),
- (mPasswordInput ? " passwordInput" : ""),
- (mHasSettingsKey ? " hasSettingsKey" : ""),
- (mVoiceKeyEnabled ? " voiceKeyEnabled" : ""),
- (mHasVoiceKey ? " hasVoiceKey" : ""),
- (mEnableShiftLock ? " enableShiftLock" : "")
+ (passwordInput() ? " passwordInput" : ""),
+ (mShortcutKeyEnabled ? " shortcutKeyEnabled" : ""),
+ (mHasShortcutKey ? " hasShortcutKey" : ""),
+ (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""),
+ (isMultiLine() ? "isMultiLine" : "")
);
}
+ public static boolean equivalentEditorInfoForKeyboard(EditorInfo a, EditorInfo b) {
+ if (a == null && b == null) return true;
+ if (a == null || b == null) return false;
+ return a.inputType == b.inputType
+ && a.imeOptions == b.imeOptions
+ && TextUtils.equals(a.privateImeOptions, b.privateImeOptions);
+ }
+
+ public static String elementIdToName(int elementId) {
+ switch (elementId) {
+ case ELEMENT_ALPHABET: return "alphabet";
+ case ELEMENT_ALPHABET_MANUAL_SHIFTED: return "alphabetManualShifted";
+ case ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: return "alphabetAutomaticShifted";
+ case ELEMENT_ALPHABET_SHIFT_LOCKED: return "alphabetShiftLocked";
+ case ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: return "alphabetShiftLockShifted";
+ case ELEMENT_SYMBOLS: return "symbols";
+ case ELEMENT_SYMBOLS_SHIFTED: return "symbolsShifted";
+ case ELEMENT_PHONE: return "phone";
+ case ELEMENT_PHONE_SYMBOLS: return "phoneSymbols";
+ case ELEMENT_NUMBER: return "number";
+ default: return null;
+ }
+ }
+
public static String modeName(int mode) {
switch (mode) {
case MODE_TEXT: return "text";
@@ -204,17 +232,15 @@ public class KeyboardId {
case MODE_IM: return "im";
case MODE_PHONE: return "phone";
case MODE_NUMBER: return "number";
+ case MODE_DATE: return "date";
+ case MODE_TIME: return "time";
+ case MODE_DATETIME: return "datetime";
default: return null;
}
}
- public static String f2KeyModeName(int f2KeyMode) {
- switch (f2KeyMode) {
- case F2KEY_MODE_NONE: return "none";
- case F2KEY_MODE_SETTINGS: return "settings";
- case F2KEY_MODE_SHORTCUT_IME: return "shortcutIme";
- case F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS: return "shortcutImeOrSettings";
- default: return null;
- }
+ public static String actionName(int actionId) {
+ return (actionId == IME_ACTION_CUSTOM_LABEL) ? "actionCustomLabel"
+ : EditorInfoCompatUtils.imeActionName(actionId);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
new file mode 100644
index 000000000..8c7246855
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
+import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
+import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.text.InputType;
+import android.util.Log;
+import android.util.Xml;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams;
+import com.android.inputmethod.latin.InputAttributes;
+import com.android.inputmethod.latin.InputTypeUtils;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeLocale;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.XmlParseUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+
+/**
+ * This class represents a set of keyboard layouts. Each of them represents a different keyboard
+ * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same
+ * {@link KeyboardLayoutSet} are related to each other.
+ * A {@link KeyboardLayoutSet} needs to be created for each
+ * {@link android.view.inputmethod.EditorInfo}.
+ */
+public class KeyboardLayoutSet {
+ private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
+ private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
+
+ private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
+ private static final String TAG_ELEMENT = "Element";
+
+ private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
+
+ private final Context mContext;
+ private final Params mParams;
+
+ private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
+ new HashMap<KeyboardId, SoftReference<Keyboard>>();
+ private static final KeysCache sKeysCache = new KeysCache();
+
+ public static class KeyboardLayoutSetException extends RuntimeException {
+ public final KeyboardId mKeyboardId;
+
+ public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) {
+ super(cause);
+ mKeyboardId = keyboardId;
+ }
+ }
+
+ public static class KeysCache {
+ private final HashMap<Key, Key> mMap;
+
+ public KeysCache() {
+ mMap = new HashMap<Key, Key>();
+ }
+
+ public void clear() {
+ mMap.clear();
+ }
+
+ public Key get(Key key) {
+ final Key existingKey = mMap.get(key);
+ if (existingKey != null) {
+ // Reuse the existing element that equals to "key" without adding "key" to the map.
+ return existingKey;
+ }
+ mMap.put(key, key);
+ return key;
+ }
+ }
+
+ static class Params {
+ String mKeyboardLayoutSetName;
+ int mMode;
+ EditorInfo mEditorInfo;
+ boolean mTouchPositionCorrectionEnabled;
+ boolean mVoiceKeyEnabled;
+ boolean mVoiceKeyOnMain;
+ boolean mNoSettingsKey;
+ boolean mLanguageSwitchKeyEnabled;
+ InputMethodSubtype mSubtype;
+ int mOrientation;
+ int mWidth;
+ // KeyboardLayoutSet element id to element's parameters map.
+ final HashMap<Integer, ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
+ new HashMap<Integer, ElementParams>();
+
+ static class ElementParams {
+ int mKeyboardXmlId;
+ boolean mProximityCharsCorrectionEnabled;
+ }
+ }
+
+ public static void clearKeyboardCache() {
+ sKeyboardCache.clear();
+ sKeysCache.clear();
+ }
+
+ private KeyboardLayoutSet(Context context, Params params) {
+ mContext = context;
+ mParams = params;
+ }
+
+ public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) {
+ final int keyboardLayoutSetElementId;
+ switch (mParams.mMode) {
+ case KeyboardId.MODE_PHONE:
+ if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
+ } else {
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
+ }
+ break;
+ case KeyboardId.MODE_NUMBER:
+ case KeyboardId.MODE_DATE:
+ case KeyboardId.MODE_TIME:
+ case KeyboardId.MODE_DATETIME:
+ keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
+ break;
+ default:
+ keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
+ break;
+ }
+
+ ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
+ keyboardLayoutSetElementId);
+ if (elementParams == null) {
+ elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
+ KeyboardId.ELEMENT_ALPHABET);
+ }
+ final KeyboardId id = getKeyboardId(keyboardLayoutSetElementId);
+ try {
+ return getKeyboard(elementParams, id);
+ } catch (RuntimeException e) {
+ throw new KeyboardLayoutSetException(e, id);
+ }
+ }
+
+ private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId id) {
+ final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
+ Keyboard keyboard = (ref == null) ? null : ref.get();
+ if (keyboard == null) {
+ final Keyboard.Builder<Keyboard.Params> builder =
+ new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params());
+ if (id.isAlphabetKeyboard()) {
+ builder.setAutoGenerate(sKeysCache);
+ }
+ final int keyboardXmlId = elementParams.mKeyboardXmlId;
+ builder.load(keyboardXmlId, id);
+ builder.setTouchPositionCorrectionEnabled(mParams.mTouchPositionCorrectionEnabled);
+ builder.setProximityCharsCorrectionEnabled(
+ elementParams.mProximityCharsCorrectionEnabled);
+ keyboard = builder.build();
+ sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
+
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
+ + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
+ }
+ } else if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
+ }
+
+ return keyboard;
+ }
+
+ // Note: The keyboard for each locale, shift state, and mode are represented as
+ // KeyboardLayoutSet element id that is a key in keyboard_set.xml. Also that file specifies
+ // which XML layout should be used for each keyboard. The KeyboardId is an internal key for
+ // Keyboard object.
+ private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) {
+ final Params params = mParams;
+ final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS
+ || keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED);
+ final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype);
+ final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage;
+ final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain);
+ return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mOrientation,
+ params.mWidth, params.mMode, params.mEditorInfo, params.mNoSettingsKey,
+ voiceKeyEnabled, hasShortcutKey, params.mLanguageSwitchKeyEnabled);
+ }
+
+ public static class Builder {
+ private final Context mContext;
+ private final String mPackageName;
+ private final Resources mResources;
+ private final EditorInfo mEditorInfo;
+
+ private final Params mParams = new Params();
+
+ private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
+
+ public Builder(Context context, EditorInfo editorInfo) {
+ mContext = context;
+ mPackageName = context.getPackageName();
+ mResources = context.getResources();
+ mEditorInfo = editorInfo;
+ final Params params = mParams;
+
+ params.mMode = getKeyboardMode(editorInfo);
+ params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
+ params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
+ mPackageName, NO_SETTINGS_KEY, mEditorInfo);
+ }
+
+ public Builder setScreenGeometry(int orientation, int widthPixels) {
+ mParams.mOrientation = orientation;
+ mParams.mWidth = widthPixels;
+ return this;
+ }
+
+ public Builder setSubtype(InputMethodSubtype subtype) {
+ final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
+ @SuppressWarnings("deprecation")
+ final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
+ mPackageName, FORCE_ASCII, mEditorInfo);
+ final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
+ mParams.mEditorInfo.imeOptions)
+ || deprecatedForceAscii;
+ final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
+ ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
+ : subtype;
+ mParams.mSubtype = keyboardSubtype;
+ mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
+ + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype);
+ return this;
+ }
+
+ public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain,
+ boolean languageSwitchKeyEnabled) {
+ @SuppressWarnings("deprecation")
+ final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
+ null, NO_MICROPHONE_COMPAT, mEditorInfo);
+ final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
+ mPackageName, NO_MICROPHONE, mEditorInfo)
+ || deprecatedNoMicrophone;
+ mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
+ mParams.mVoiceKeyOnMain = voiceKeyOnMain;
+ mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled;
+ return this;
+ }
+
+ public void setTouchPositionCorrectionEnabled(boolean enabled) {
+ mParams.mTouchPositionCorrectionEnabled = enabled;
+ }
+
+ public KeyboardLayoutSet build() {
+ if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
+ throw new RuntimeException("Screen geometry is not specified");
+ if (mParams.mSubtype == null)
+ throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
+ final String packageName = mResources.getResourcePackageName(
+ R.xml.keyboard_layout_set_qwerty);
+ final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName;
+ final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
+ try {
+ parseKeyboardLayoutSet(mResources, xmlId);
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName);
+ }
+ return new KeyboardLayoutSet(mContext, mParams);
+ }
+
+ private void parseKeyboardLayoutSet(Resources res, int resId)
+ throws XmlPullParserException, IOException {
+ final XmlResourceParser parser = res.getXml(resId);
+ try {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD_SET.equals(tag)) {
+ parseKeyboardLayoutSetContent(parser);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET);
+ }
+ }
+ }
+ } finally {
+ parser.close();
+ }
+ }
+
+ private void parseKeyboardLayoutSetContent(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_ELEMENT.equals(tag)) {
+ parseKeyboardLayoutSetElement(parser);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD_SET);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD_SET.equals(tag)) {
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEYBOARD_SET);
+ }
+ }
+ }
+ }
+
+ private void parseKeyboardLayoutSetElement(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.KeyboardLayoutSet_Element);
+ try {
+ XmlParseUtils.checkAttributeExists(a,
+ R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
+ TAG_ELEMENT, parser);
+ XmlParseUtils.checkAttributeExists(a,
+ R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
+ TAG_ELEMENT, parser);
+ XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
+
+ final ElementParams elementParams = new ElementParams();
+ final int elementName = a.getInt(
+ R.styleable.KeyboardLayoutSet_Element_elementName, 0);
+ elementParams.mKeyboardXmlId = a.getResourceId(
+ R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
+ elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
+ R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
+ false);
+ mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ private static int getKeyboardMode(EditorInfo editorInfo) {
+ if (editorInfo == null)
+ return KeyboardId.MODE_TEXT;
+
+ final int inputType = editorInfo.inputType;
+ final int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+ switch (inputType & InputType.TYPE_MASK_CLASS) {
+ case InputType.TYPE_CLASS_NUMBER:
+ return KeyboardId.MODE_NUMBER;
+ case InputType.TYPE_CLASS_DATETIME:
+ switch (variation) {
+ case InputType.TYPE_DATETIME_VARIATION_DATE:
+ return KeyboardId.MODE_DATE;
+ case InputType.TYPE_DATETIME_VARIATION_TIME:
+ return KeyboardId.MODE_TIME;
+ default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
+ return KeyboardId.MODE_DATETIME;
+ }
+ case InputType.TYPE_CLASS_PHONE:
+ return KeyboardId.MODE_PHONE;
+ case InputType.TYPE_CLASS_TEXT:
+ if (InputTypeUtils.isEmailVariation(variation)) {
+ return KeyboardId.MODE_EMAIL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
+ return KeyboardId.MODE_URL;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
+ return KeyboardId.MODE_IM;
+ } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+ return KeyboardId.MODE_TEXT;
+ } else {
+ return KeyboardId.MODE_TEXT;
+ }
+ default:
+ return KeyboardId.MODE_TEXT;
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 275e9d1fe..2e4ce199e 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -27,86 +27,64 @@ import android.view.View;
import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.keyboard.internal.ModifierKeyState;
-import com.android.inputmethod.keyboard.internal.ShiftKeyState;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException;
+import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.keyboard.internal.KeyboardState;
+import com.android.inputmethod.latin.DebugSettings;
+import com.android.inputmethod.latin.ImfUtils;
+import com.android.inputmethod.latin.InputView;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.SettingsValues;
import com.android.inputmethod.latin.SubtypeSwitcher;
import com.android.inputmethod.latin.Utils;
-import java.lang.ref.SoftReference;
-import java.util.HashMap;
-import java.util.Locale;
-
-public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener {
+public class KeyboardSwitcher implements KeyboardState.SwitchActions {
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
- private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
- public static final boolean DEBUG_STATE = false;
-
- public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20100902";
- private static final int[] KEYBOARD_THEMES = {
- R.style.KeyboardTheme,
- R.style.KeyboardTheme_HighContrast,
- R.style.KeyboardTheme_Stone,
- R.style.KeyboardTheme_Stone_Bold,
- R.style.KeyboardTheme_Gingerbread,
- R.style.KeyboardTheme_IceCreamSandwich,
+
+ public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
+
+ static class KeyboardTheme {
+ public final String mName;
+ public final int mThemeId;
+ public final int mStyleId;
+
+ public KeyboardTheme(String name, int themeId, int styleId) {
+ mName = name;
+ mThemeId = themeId;
+ mStyleId = styleId;
+ }
+ }
+
+ private static final KeyboardTheme[] KEYBOARD_THEMES = {
+ new KeyboardTheme("Basic", 0, R.style.KeyboardTheme),
+ new KeyboardTheme("HighContrast", 1, R.style.KeyboardTheme_HighContrast),
+ new KeyboardTheme("Stone", 6, R.style.KeyboardTheme_Stone),
+ new KeyboardTheme("Stne.Bold", 7, R.style.KeyboardTheme_Stone_Bold),
+ new KeyboardTheme("GingerBread", 8, R.style.KeyboardTheme_Gingerbread),
+ new KeyboardTheme("IceCreamSandwich", 5, R.style.KeyboardTheme_IceCreamSandwich),
};
private SubtypeSwitcher mSubtypeSwitcher;
private SharedPreferences mPrefs;
+ private boolean mForceNonDistinctMultitouch;
- private View mCurrentInputView;
+ private InputView mCurrentInputView;
private LatinKeyboardView mKeyboardView;
- private LatinIME mInputMethodService;
-
- // TODO: Combine these key state objects with auto mode switch state.
- private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
- private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
+ private LatinIME mLatinIME;
+ private Resources mResources;
- private KeyboardId mSymbolsId;
- private KeyboardId mSymbolsShiftedId;
+ private KeyboardState mState;
- private KeyboardId mCurrentId;
- private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache =
- new HashMap<KeyboardId, SoftReference<LatinKeyboard>>();
+ private KeyboardLayoutSet mKeyboardLayoutSet;
- private EditorInfo mAttribute;
- private boolean mIsSymbols;
/** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of
* what user actually typed. */
private boolean mIsAutoCorrectionActive;
- private boolean mVoiceKeyEnabled;
- private boolean mVoiceButtonOnPrimary;
-
- // TODO: Encapsulate these state handling to separate class and combine with ShiftKeyState
- // and ModifierKeyState.
- private static final int SWITCH_STATE_ALPHA = 0;
- private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
- private static final int SWITCH_STATE_SYMBOL = 2;
- // The following states are used only on the distinct multi-touch panel devices.
- private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
- private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
- private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
- private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
- private int mSwitchState = SWITCH_STATE_ALPHA;
-
- // Indicates whether or not we have the settings key in option of settings
- private boolean mSettingsKeyEnabledInSettings;
- private static final int SETTINGS_KEY_MODE_AUTO = R.string.settings_key_mode_auto;
- private static final int SETTINGS_KEY_MODE_ALWAYS_SHOW =
- R.string.settings_key_mode_always_show;
- // NOTE: No need to have SETTINGS_KEY_MODE_ALWAYS_HIDE here because it's not being referred to
- // in the source code now.
- // Default is SETTINGS_KEY_MODE_AUTO.
- private static final int DEFAULT_SETTINGS_KEY_MODE = SETTINGS_KEY_MODE_AUTO;
-
- private int mThemeIndex = -1;
+
+ private KeyboardTheme mKeyboardTheme = KEYBOARD_THEMES[0];
private Context mThemeContext;
- private int mKeyboardWidth;
private static final KeyboardSwitcher sInstance = new KeyboardSwitcher();
@@ -118,612 +96,251 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
// Intentional empty constructor for singleton.
}
- public static void init(LatinIME ims, SharedPreferences prefs) {
- sInstance.mInputMethodService = ims;
- sInstance.mPrefs = prefs;
- sInstance.mSubtypeSwitcher = SubtypeSwitcher.getInstance();
- sInstance.setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs));
- prefs.registerOnSharedPreferenceChangeListener(sInstance);
+ public static void init(LatinIME latinIme, SharedPreferences prefs) {
+ sInstance.initInternal(latinIme, prefs);
+ }
+
+ private void initInternal(LatinIME latinIme, SharedPreferences prefs) {
+ mLatinIME = latinIme;
+ mResources = latinIme.getResources();
+ mPrefs = prefs;
+ mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ mState = new KeyboardState(this);
+ setContextThemeWrapper(latinIme, getKeyboardTheme(latinIme, prefs));
+ mForceNonDistinctMultitouch = prefs.getBoolean(
+ DebugSettings.FORCE_NON_DISTINCT_MULTITOUCH_KEY, false);
}
- private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) {
- final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id);
- final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId);
+ private static KeyboardTheme getKeyboardTheme(Context context, SharedPreferences prefs) {
+ final String defaultIndex = context.getString(R.string.config_default_keyboard_theme_index);
+ final String themeIndex = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultIndex);
try {
- final int themeIndex = Integer.valueOf(themeId);
- if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length)
- return themeIndex;
+ final int index = Integer.valueOf(themeIndex);
+ if (index >= 0 && index < KEYBOARD_THEMES.length) {
+ return KEYBOARD_THEMES[index];
+ }
} catch (NumberFormatException e) {
// Format error, keyboard theme is default to 0.
}
- Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0");
- return 0;
+ Log.w(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to 0");
+ return KEYBOARD_THEMES[0];
}
- private void setContextThemeWrapper(Context context, int themeIndex) {
- if (mThemeIndex != themeIndex) {
- mThemeIndex = themeIndex;
- mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]);
- mKeyboardCache.clear();
+ private void setContextThemeWrapper(Context context, KeyboardTheme keyboardTheme) {
+ if (mKeyboardTheme.mThemeId != keyboardTheme.mThemeId) {
+ mKeyboardTheme = keyboardTheme;
+ mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId);
+ KeyboardLayoutSet.clearKeyboardCache();
}
}
- public void loadKeyboard(EditorInfo attribute, boolean voiceKeyEnabled,
- boolean voiceButtonOnPrimary) {
- mSwitchState = SWITCH_STATE_ALPHA;
+ public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) {
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+ mThemeContext, editorInfo);
+ builder.setScreenGeometry(mThemeContext.getResources().getConfiguration().orientation,
+ mThemeContext.getResources().getDisplayMetrics().widthPixels);
+ builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
+ builder.setOptions(
+ settingsValues.isVoiceKeyEnabled(editorInfo),
+ settingsValues.isVoiceKeyOnMain(),
+ settingsValues.isLanguageSwitchKeyEnabled(mThemeContext));
+ mKeyboardLayoutSet = builder.build();
try {
- loadKeyboardInternal(attribute, voiceKeyEnabled, voiceButtonOnPrimary, false);
- } catch (RuntimeException e) {
- // Get KeyboardId to record which keyboard has been failed to load.
- final KeyboardId id = getKeyboardId(attribute, false);
- Log.w(TAG, "loading keyboard failed: " + id, e);
- LatinImeLogger.logOnException(id.toString(), e);
+ mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols));
+ } catch (KeyboardLayoutSetException e) {
+ Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
+ LatinImeLogger.logOnException(e.mKeyboardId.toString(), e.getCause());
+ return;
}
}
- private void loadKeyboardInternal(EditorInfo attribute, boolean voiceButtonEnabled,
- boolean voiceButtonOnPrimary, boolean isSymbols) {
- if (mKeyboardView == null) return;
-
- mAttribute = attribute;
- mVoiceKeyEnabled = voiceButtonEnabled;
- mVoiceButtonOnPrimary = voiceButtonOnPrimary;
- mIsSymbols = isSymbols;
- // Update the settings key state because number of enabled IMEs could have been changed
- mSettingsKeyEnabledInSettings = getSettingsKeyMode(mPrefs, mInputMethodService);
- final KeyboardId id = getKeyboardId(attribute, isSymbols);
-
- // Note: This comment is only applied for phone number keyboard layout.
- // On non-xlarge device, "@integer/key_switch_alpha_symbol" key code is used to switch
- // between "phone keyboard" and "phone symbols keyboard". But on xlarge device,
- // "@integer/key_shift" key code is used for that purpose in order to properly display
- // "more" and "locked more" key labels. To achieve these behavior, we should initialize
- // mSymbolsId and mSymbolsShiftedId to "phone keyboard" and "phone symbols keyboard"
- // respectively here for xlarge device's layout switching.
- mSymbolsId = makeSiblingKeyboardId(id, R.xml.kbd_symbols, R.xml.kbd_phone);
- mSymbolsShiftedId = makeSiblingKeyboardId(
- id, R.xml.kbd_symbols_shift, R.xml.kbd_phone_symbols);
+ public void saveKeyboardState() {
+ if (getKeyboard() != null) {
+ mState.onSaveKeyboardState();
+ }
+ }
- setKeyboard(getKeyboard(id));
+ public void onFinishInputView() {
+ mIsAutoCorrectionActive = false;
}
- public void onSizeChanged() {
- final int width = mInputMethodService.getWindow().getWindow().getDecorView().getWidth();
- if (width == 0 || mCurrentId == null)
- return;
- mKeyboardWidth = width;
- // Set keyboard with new width.
- final KeyboardId newId = mCurrentId.cloneWithNewGeometry(width);
- setKeyboard(getKeyboard(newId));
+ public void onHideWindow() {
+ mIsAutoCorrectionActive = false;
}
- private void setKeyboard(final Keyboard newKeyboard) {
+ private void setKeyboard(final Keyboard keyboard) {
final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
- mKeyboardView.setKeyboard(newKeyboard);
- mCurrentId = newKeyboard.mId;
- final Resources res = mInputMethodService.getResources();
+ mKeyboardView.setKeyboard(keyboard);
+ mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
mKeyboardView.setKeyPreviewPopupEnabled(
- Settings.Values.isKeyPreviewPopupEnabled(mPrefs, res),
- Settings.Values.getKeyPreviewPopupDismissDelay(mPrefs, res));
- final boolean localeChanged = (oldKeyboard == null)
- || !newKeyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
- mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged);
- }
-
- private LatinKeyboard getKeyboard(KeyboardId id) {
- final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id);
- LatinKeyboard keyboard = (ref == null) ? null : ref.get();
- if (keyboard == null) {
- final Resources res = mInputMethodService.getResources();
- final Locale savedLocale = Utils.setSystemLocale(res,
- mSubtypeSwitcher.getInputLocale());
-
- keyboard = new LatinKeyboard(mThemeContext, id, id.mWidth);
-
- if (id.mEnableShiftLock) {
- keyboard.enableShiftLock();
- }
-
- mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard));
- if (DEBUG_CACHE)
- Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": "
- + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
-
- Utils.setSystemLocale(res, savedLocale);
- } else if (DEBUG_CACHE) {
- Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT id=" + id);
- }
-
- keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive);
- keyboard.setShifted(false);
- // If the cached keyboard had been switched to another keyboard while the language was
- // displayed on its spacebar, it might have had arbitrary text fade factor. In such case,
- // we should reset the text fade factor. It is also applicable to shortcut key.
- keyboard.setSpacebarTextFadeFactor(0.0f, null);
- keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null);
- keyboard.setSpacebarSlidingLanguageSwitchDiff(0);
- return keyboard;
- }
-
- private boolean hasVoiceKey(boolean isSymbols) {
- return mVoiceKeyEnabled && (isSymbols != mVoiceButtonOnPrimary);
- }
-
- private boolean hasSettingsKey(EditorInfo attribute) {
- return mSettingsKeyEnabledInSettings
- && !Utils.inPrivateImeOptions(mInputMethodService.getPackageName(),
- LatinIME.IME_OPTION_NO_SETTINGS_KEY, attribute);
- }
-
- private KeyboardId getKeyboardId(EditorInfo attribute, boolean isSymbols) {
- final int mode = Utils.getKeyboardMode(attribute);
- final boolean hasVoiceKey = hasVoiceKey(isSymbols);
- final int xmlId;
- final boolean enableShiftLock;
-
- if (isSymbols) {
- if (mode == KeyboardId.MODE_PHONE) {
- xmlId = R.xml.kbd_phone_symbols;
- } else if (mode == KeyboardId.MODE_NUMBER) {
- // Note: MODE_NUMBER keyboard layout has no "switch alpha symbol" key.
- xmlId = R.xml.kbd_number;
- } else {
- xmlId = R.xml.kbd_symbols;
- }
- enableShiftLock = false;
- } else {
- if (mode == KeyboardId.MODE_PHONE) {
- xmlId = R.xml.kbd_phone;
- enableShiftLock = false;
- } else if (mode == KeyboardId.MODE_NUMBER) {
- xmlId = R.xml.kbd_number;
- enableShiftLock = false;
- } else {
- xmlId = R.xml.kbd_qwerty;
- enableShiftLock = true;
- }
+ SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources),
+ SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
+ mKeyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive);
+ mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
+ final boolean subtypeChanged = (oldKeyboard == null)
+ || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
+ final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage(
+ keyboard.mId.mLocale);
+ mKeyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage,
+ ImfUtils.hasMultipleEnabledIMEsOrSubtypes(mLatinIME, true));
+ }
+
+ public Keyboard getKeyboard() {
+ if (mKeyboardView != null) {
+ return mKeyboardView.getKeyboard();
}
- final boolean hasSettingsKey = hasSettingsKey(attribute);
- final int f2KeyMode = getF2KeyMode(mPrefs, mInputMethodService, attribute);
- final boolean clobberSettingsKey = Utils.inPrivateImeOptions(
- mInputMethodService.getPackageName(), LatinIME.IME_OPTION_NO_SETTINGS_KEY,
- attribute);
- final Resources res = mInputMethodService.getResources();
- final int orientation = res.getConfiguration().orientation;
- if (mKeyboardWidth == 0)
- mKeyboardWidth = res.getDisplayMetrics().widthPixels;
- final Locale locale = mSubtypeSwitcher.getInputLocale();
- return new KeyboardId(
- res.getResourceEntryName(xmlId), xmlId, locale, orientation, mKeyboardWidth,
- mode, attribute, hasSettingsKey, f2KeyMode, clobberSettingsKey, mVoiceKeyEnabled,
- hasVoiceKey, enableShiftLock);
- }
-
- private KeyboardId makeSiblingKeyboardId(KeyboardId base, int alphabet, int phone) {
- final int xmlId = base.mMode == KeyboardId.MODE_PHONE ? phone : alphabet;
- final String xmlName = mInputMethodService.getResources().getResourceEntryName(xmlId);
- return base.cloneWithNewLayout(xmlName, xmlId);
- }
-
- public int getKeyboardMode() {
- return mCurrentId != null ? mCurrentId.mMode : KeyboardId.MODE_TEXT;
- }
-
- public boolean isAlphabetMode() {
- return mCurrentId != null && mCurrentId.isAlphabetKeyboard();
+ return null;
}
- public boolean isInputViewShown() {
- return mCurrentInputView != null && mCurrentInputView.isShown();
+ /**
+ * Update keyboard shift state triggered by connected EditText status change.
+ */
+ public void updateShiftState() {
+ mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState());
}
- public boolean isKeyboardAvailable() {
- if (mKeyboardView != null)
- return mKeyboardView.getKeyboard() != null;
- return false;
+ public void onPressKey(int code) {
+ if (isVibrateAndSoundFeedbackRequired()) {
+ mLatinIME.hapticAndAudioFeedback(code);
+ }
+ mState.onPressKey(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
}
- public LatinKeyboard getLatinKeyboard() {
- if (mKeyboardView != null) {
- final Keyboard keyboard = mKeyboardView.getKeyboard();
- if (keyboard instanceof LatinKeyboard)
- return (LatinKeyboard)keyboard;
- }
- return null;
+ public void onReleaseKey(int code, boolean withSliding) {
+ mState.onReleaseKey(code, withSliding);
}
- public boolean isShiftedOrShiftLocked() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null)
- return latinKeyboard.isShiftedOrShiftLocked();
- return false;
+ public void onCancelInput() {
+ mState.onCancelInput(isSinglePointer());
}
- public boolean isShiftLocked() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null)
- return latinKeyboard.isShiftLocked();
- return false;
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET));
}
- public boolean isAutomaticTemporaryUpperCase() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null)
- return latinKeyboard.isAutomaticTemporaryUpperCase();
- return false;
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetManualShiftedKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED));
}
- public boolean isManualTemporaryUpperCase() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null)
- return latinKeyboard.isManualTemporaryUpperCase();
- return false;
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetAutomaticShiftedKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED));
}
- private boolean isManualTemporaryUpperCaseFromAuto() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null)
- return latinKeyboard.isManualTemporaryUpperCaseFromAuto();
- return false;
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetShiftLockedKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED));
}
- private void setManualTemporaryUpperCase(boolean shifted) {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null) {
- // On non-distinct multi touch panel device, we should also turn off the shift locked
- // state when shift key is pressed to go to normal mode.
- // On the other hand, on distinct multi touch panel device, turning off the shift locked
- // state with shift key pressing is handled by onReleaseShift().
- if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) {
- latinKeyboard.setShiftLocked(false);
- }
- if (latinKeyboard.setShifted(shifted)) {
- mKeyboardView.invalidateAllKeys();
- }
- }
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setAlphabetShiftLockShiftedKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED));
}
- private void setShiftLocked(boolean shiftLocked) {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) {
- mKeyboardView.invalidateAllKeys();
- }
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setSymbolsKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS));
}
- /**
- * Toggle keyboard shift state triggered by user touch event.
- */
- public void toggleShift() {
- mInputMethodService.mHandler.cancelUpdateShiftState();
- if (DEBUG_STATE)
- Log.d(TAG, "toggleShift:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + mShiftKeyState);
- if (isAlphabetMode()) {
- setManualTemporaryUpperCase(!isShiftedOrShiftLocked());
- } else {
- toggleShiftInSymbol();
- }
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setSymbolsShiftedKeyboard() {
+ setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED));
}
- public void toggleCapsLock() {
- mInputMethodService.mHandler.cancelUpdateShiftState();
- if (DEBUG_STATE)
- Log.d(TAG, "toggleCapsLock:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + mShiftKeyState);
- if (isAlphabetMode()) {
- if (isShiftLocked()) {
- // Shift key is long pressed while caps lock state, we will toggle back to normal
- // state. And mark as if shift key is released.
- setShiftLocked(false);
- mShiftKeyState.onRelease();
- } else {
- setShiftLocked(true);
- }
- }
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void requestUpdatingShiftState() {
+ mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState());
}
- private void setAutomaticTemporaryUpperCase() {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null) {
- latinKeyboard.setAutomaticTemporaryUpperCase();
- mKeyboardView.invalidateAllKeys();
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void startDoubleTapTimer() {
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ if (keyboardView != null) {
+ final TimerProxy timer = keyboardView.getTimerProxy();
+ timer.startDoubleTapTimer();
}
}
- /**
- * Update keyboard shift state triggered by connected EditText status change.
- */
- public void updateShiftState() {
- final ShiftKeyState shiftKeyState = mShiftKeyState;
- if (DEBUG_STATE)
- Log.d(TAG, "updateShiftState:"
- + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState()
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + shiftKeyState);
- if (isAlphabetMode()) {
- if (!isShiftLocked() && !shiftKeyState.isIgnoring()) {
- if (shiftKeyState.isReleasing() && mInputMethodService.getCurrentAutoCapsState()) {
- // Only when shift key is releasing, automatic temporary upper case will be set.
- setAutomaticTemporaryUpperCase();
- } else {
- setManualTemporaryUpperCase(shiftKeyState.isMomentary());
- }
- }
- } else {
- // In symbol keyboard mode, we should clear shift key state because only alphabet
- // keyboard has shift key.
- shiftKeyState.onRelease();
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void cancelDoubleTapTimer() {
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ if (keyboardView != null) {
+ final TimerProxy timer = keyboardView.getTimerProxy();
+ timer.cancelDoubleTapTimer();
}
}
- public void changeKeyboardMode() {
- if (DEBUG_STATE)
- Log.d(TAG, "changeKeyboardMode:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + mShiftKeyState);
- toggleKeyboardMode();
- if (isShiftLocked() && isAlphabetMode())
- setShiftLocked(true);
- updateShiftState();
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public boolean isInDoubleTapTimeout() {
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ return (keyboardView != null)
+ ? keyboardView.getTimerProxy().isInDoubleTapTimeout() : false;
}
- public void onPressShift(boolean withSliding) {
- if (!isKeyboardAvailable())
- return;
- ShiftKeyState shiftKeyState = mShiftKeyState;
- if (DEBUG_STATE)
- Log.d(TAG, "onPressShift:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
- if (isAlphabetMode()) {
- if (isShiftLocked()) {
- // Shift key is pressed while caps lock state, we will treat this state as shifted
- // caps lock state and mark as if shift key pressed while normal state.
- shiftKeyState.onPress();
- setManualTemporaryUpperCase(true);
- } else if (isAutomaticTemporaryUpperCase()) {
- // Shift key is pressed while automatic temporary upper case, we have to move to
- // manual temporary upper case.
- shiftKeyState.onPress();
- setManualTemporaryUpperCase(true);
- } else if (isShiftedOrShiftLocked()) {
- // In manual upper case state, we just record shift key has been pressing while
- // shifted state.
- shiftKeyState.onPressOnShifted();
- } else {
- // In base layout, chording or manual temporary upper case mode is started.
- shiftKeyState.onPress();
- toggleShift();
- }
- } else {
- // In symbol mode, just toggle symbol and symbol more keyboard.
- shiftKeyState.onPress();
- toggleShift();
- mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void startLongPressTimer(int code) {
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ if (keyboardView != null) {
+ final TimerProxy timer = keyboardView.getTimerProxy();
+ timer.startLongPressTimer(code);
}
}
- public void onReleaseShift(boolean withSliding) {
- if (!isKeyboardAvailable())
- return;
- ShiftKeyState shiftKeyState = mShiftKeyState;
- if (DEBUG_STATE)
- Log.d(TAG, "onReleaseShift:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + shiftKeyState + " sliding=" + withSliding);
- if (isAlphabetMode()) {
- if (shiftKeyState.isMomentary()) {
- // After chording input while normal state.
- toggleShift();
- } else if (isShiftLocked() && !shiftKeyState.isIgnoring() && !withSliding) {
- // Shift has been pressed without chording while caps lock state.
- toggleCapsLock();
- // To be able to turn off caps lock by "double tap" on shift key, we should ignore
- // the second tap of the "double tap" from now for a while because we just have
- // already turned off caps lock above.
- mKeyboardView.startIgnoringDoubleTap();
- } else if (isShiftedOrShiftLocked() && shiftKeyState.isPressingOnShifted()
- && !withSliding) {
- // Shift has been pressed without chording while shifted state.
- toggleShift();
- } else if (isManualTemporaryUpperCaseFromAuto() && shiftKeyState.isPressing()
- && !withSliding) {
- // Shift has been pressed without chording while manual temporary upper case
- // transited from automatic temporary upper case.
- toggleShift();
- }
- } else {
- // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
- // key and another key, then releases the shift key.
- if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
- toggleShift();
- }
- }
- shiftKeyState.onRelease();
- }
-
- public void onPressSymbol() {
- if (DEBUG_STATE)
- Log.d(TAG, "onPressSymbol:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " symbolKeyState=" + mSymbolKeyState);
- changeKeyboardMode();
- mSymbolKeyState.onPress();
- mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
- }
-
- public void onReleaseSymbol() {
- if (DEBUG_STATE)
- Log.d(TAG, "onReleaseSymbol:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " symbolKeyState=" + mSymbolKeyState);
- // Snap back to the previous keyboard mode if the user chords the mode change key and
- // another key, then releases the mode change key.
- if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
- changeKeyboardMode();
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void cancelLongPressTimer() {
+ final LatinKeyboardView keyboardView = getKeyboardView();
+ if (keyboardView != null) {
+ final TimerProxy timer = keyboardView.getTimerProxy();
+ timer.cancelLongPressTimer();
}
- mSymbolKeyState.onRelease();
}
- public void onOtherKeyPressed() {
- if (DEBUG_STATE)
- Log.d(TAG, "onOtherKeyPressed:"
- + " keyboard=" + getLatinKeyboard().getKeyboardShiftState()
- + " shiftKeyState=" + mShiftKeyState
- + " symbolKeyState=" + mSymbolKeyState);
- mShiftKeyState.onOtherKeyPressed();
- mSymbolKeyState.onOtherKeyPressed();
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void hapticAndAudioFeedback(int code) {
+ mLatinIME.hapticAndAudioFeedback(code);
}
- public void onCancelInput() {
- // Snap back to the previous keyboard mode if the user cancels sliding input.
- if (getPointerCount() == 1) {
- if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
- changeKeyboardMode();
- } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
- toggleShift();
- }
- }
- }
-
- private void toggleShiftInSymbol() {
- if (isAlphabetMode())
- return;
- final LatinKeyboard keyboard;
- if (mCurrentId.equals(mSymbolsId) || !mCurrentId.equals(mSymbolsShiftedId)) {
- keyboard = getKeyboard(mSymbolsShiftedId);
- // Symbol shifted keyboard has an ALT key that has a caps lock style indicator. To
- // enable the indicator, we need to call setShiftLocked(true).
- keyboard.setShiftLocked(true);
- } else {
- keyboard = getKeyboard(mSymbolsId);
- // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the
- // indicator, we need to call setShiftLocked(false).
- keyboard.setShiftLocked(false);
- }
- setKeyboard(keyboard);
+ public void onLongPressTimeout(int code) {
+ mState.onLongPressTimeout(code);
}
public boolean isInMomentarySwitchState() {
- return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
- || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
- }
-
- public boolean isVibrateAndSoundFeedbackRequired() {
- return mKeyboardView == null || !mKeyboardView.isInSlidingKeyInput();
+ return mState.isInMomentarySwitchState();
}
- private int getPointerCount() {
- return mKeyboardView == null ? 0 : mKeyboardView.getPointerCount();
+ private boolean isVibrateAndSoundFeedbackRequired() {
+ return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
}
- private void toggleKeyboardMode() {
- loadKeyboardInternal(mAttribute, mVoiceKeyEnabled, mVoiceButtonOnPrimary, !mIsSymbols);
- if (mIsSymbols) {
- mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
- } else {
- mSwitchState = SWITCH_STATE_ALPHA;
- }
+ private boolean isSinglePointer() {
+ return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
}
public boolean hasDistinctMultitouch() {
return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch();
}
- private static boolean isSpaceCharacter(int c) {
- return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
- }
-
- private static boolean isQuoteCharacter(int c) {
- // Apostrophe, quotation mark.
- if (c == Keyboard.CODE_SINGLE_QUOTE || c == Keyboard.CODE_DOUBLE_QUOTE)
- return true;
- // \u2018: Left single quotation mark
- // \u2019: Right single quotation mark
- // \u201a: Single low-9 quotation mark
- // \u201b: Single high-reversed-9 quotation mark
- // \u201c: Left double quotation mark
- // \u201d: Right double quotation mark
- // \u201e: Double low-9 quotation mark
- // \u201f: Double high-reversed-9 quotation mark
- if (c >= '\u2018' && c <= '\u201f')
- return true;
- // \u00ab: Left-pointing double angle quotation mark
- // \u00bb: Right-pointing double angle quotation mark
- if (c == '\u00ab' || c == '\u00bb')
- return true;
- return false;
- }
-
/**
- * Updates state machine to figure out when to automatically snap back to the previous mode.
+ * Updates state machine to figure out when to automatically switch back to the previous mode.
*/
- public void onKey(int code) {
- if (DEBUG_STATE)
- Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState
- + " pointers=" + getPointerCount());
- switch (mSwitchState) {
- case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
- // Only distinct multi touch devices can be in this state.
- // On non-distinct multi touch devices, mode change key is handled by
- // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and
- // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts
- // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from
- // {@link #SWITCH_STATE_MOMENTARY}.
- if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
- // Detected only the mode change key has been pressed, and then released.
- if (mIsSymbols) {
- mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
- } else {
- mSwitchState = SWITCH_STATE_ALPHA;
- }
- } else if (getPointerCount() == 1) {
- // Snap back to the previous keyboard mode if the user pressed the mode change key
- // and slid to other key, then released the finger.
- // If the user cancels the sliding input, snapping back to the previous keyboard
- // mode is handled by {@link #onCancelInput}.
- changeKeyboardMode();
- } else {
- // Chording input is being started. The keyboard mode will be snapped back to the
- // previous mode in {@link onReleaseSymbol} when the mode change key is released.
- mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
- }
- break;
- case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
- if (code == Keyboard.CODE_SHIFT) {
- // Detected only the shift key has been pressed on symbol layout, and then released.
- mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
- } else if (getPointerCount() == 1) {
- // Snap back to the previous keyboard mode if the user pressed the shift key on
- // symbol mode and slid to other key, then released the finger.
- toggleShift();
- mSwitchState = SWITCH_STATE_SYMBOL;
- } else {
- // Chording input is being started. The keyboard mode will be snapped back to the
- // previous mode in {@link onReleaseShift} when the shift key is released.
- mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
- }
- break;
- case SWITCH_STATE_SYMBOL_BEGIN:
- if (!isSpaceCharacter(code) && code >= 0) {
- mSwitchState = SWITCH_STATE_SYMBOL;
- }
- // Snap back to alpha keyboard mode immediately if user types a quote character.
- if (isQuoteCharacter(code)) {
- changeKeyboardMode();
- }
- break;
- case SWITCH_STATE_SYMBOL:
- case SWITCH_STATE_CHORDING_SYMBOL:
- // Snap back to alpha keyboard mode if user types one or more non-space/enter
- // characters followed by a space/enter or a quote character.
- if (isSpaceCharacter(code) || isQuoteCharacter(code)) {
- changeKeyboardMode();
- }
- break;
- }
+ public void onCodeInput(int code) {
+ mState.onCodeInput(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
}
public LatinKeyboardView getKeyboardView() {
@@ -731,119 +348,52 @@ public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceCha
}
public View onCreateInputView() {
- return createInputView(mThemeIndex, true);
- }
-
- private View createInputView(final int newThemeIndex, final boolean forceRecreate) {
- if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate)
- return mCurrentInputView;
-
if (mKeyboardView != null) {
mKeyboardView.closing();
}
- final int oldThemeIndex = mThemeIndex;
Utils.GCUtils.getInstance().reset();
boolean tryGC = true;
for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
- setContextThemeWrapper(mInputMethodService, newThemeIndex);
- mCurrentInputView = LayoutInflater.from(mThemeContext).inflate(
+ setContextThemeWrapper(mLatinIME, mKeyboardTheme);
+ mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
R.layout.input_view, null);
tryGC = false;
} catch (OutOfMemoryError e) {
Log.w(TAG, "load keyboard failed: " + e);
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
- oldThemeIndex + "," + newThemeIndex, e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
} catch (InflateException e) {
Log.w(TAG, "load keyboard failed: " + e);
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait(
- oldThemeIndex + "," + newThemeIndex, e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
}
}
mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
- mKeyboardView.setOnKeyboardActionListener(mInputMethodService);
+ mKeyboardView.setKeyboardActionListener(mLatinIME);
+ if (mForceNonDistinctMultitouch) {
+ mKeyboardView.setDistinctMultitouch(false);
+ }
// This always needs to be set since the accessibility state can
// potentially change without the input view being re-created.
- AccessibleKeyboardViewProxy.setView(mKeyboardView);
+ AccessibleKeyboardViewProxy.getInstance().setView(mKeyboardView);
return mCurrentInputView;
}
- private void postSetInputView(final View newInputView) {
- mInputMethodService.mHandler.post(new Runnable() {
- @Override
- public void run() {
- if (newInputView != null) {
- mInputMethodService.setInputView(newInputView);
- }
- mInputMethodService.updateInputViewShown();
- }
- });
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (PREF_KEYBOARD_LAYOUT.equals(key)) {
- final int layoutId = getKeyboardThemeIndex(mInputMethodService, sharedPreferences);
- postSetInputView(createInputView(layoutId, false));
- } else if (Settings.PREF_SETTINGS_KEY.equals(key)) {
- mSettingsKeyEnabledInSettings = getSettingsKeyMode(sharedPreferences,
- mInputMethodService);
- postSetInputView(createInputView(mThemeIndex, true));
+ public void onNetworkStateChanged() {
+ if (mKeyboardView != null) {
+ mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
}
}
public void onAutoCorrectionStateChanged(boolean isAutoCorrection) {
if (mIsAutoCorrectionActive != isAutoCorrection) {
mIsAutoCorrectionActive = isAutoCorrection;
- final LatinKeyboard keyboard = getLatinKeyboard();
- if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) {
- final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection);
- final LatinKeyboardView keyboardView = getKeyboardView();
- if (keyboardView != null)
- keyboardView.invalidateKey(invalidatedKey);
+ if (mKeyboardView != null) {
+ mKeyboardView.updateAutoCorrectionState(isAutoCorrection);
}
}
}
-
- private static boolean getSettingsKeyMode(SharedPreferences prefs, Context context) {
- final Resources res = context.getResources();
- final boolean showSettingsKeyOption = res.getBoolean(
- R.bool.config_enable_show_settings_key_option);
- if (showSettingsKeyOption) {
- final String settingsKeyMode = prefs.getString(Settings.PREF_SETTINGS_KEY,
- res.getString(DEFAULT_SETTINGS_KEY_MODE));
- // We show the settings key when 1) SETTINGS_KEY_MODE_ALWAYS_SHOW or
- // 2) SETTINGS_KEY_MODE_AUTO and there are two or more enabled IMEs on the system
- if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))
- || (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_AUTO))
- && Utils.hasMultipleEnabledIMEsOrSubtypes(
- (InputMethodManagerCompatWrapper.getInstance(context))))) {
- return true;
- }
- return false;
- }
- // If the show settings key option is disabled, we always try showing the settings key.
- return true;
- }
-
- private static int getF2KeyMode(SharedPreferences prefs, Context context,
- EditorInfo attribute) {
- final boolean clobberSettingsKey = Utils.inPrivateImeOptions(
- context.getPackageName(), LatinIME.IME_OPTION_NO_SETTINGS_KEY, attribute);
- final Resources res = context.getResources();
- final String settingsKeyMode = prefs.getString(Settings.PREF_SETTINGS_KEY,
- res.getString(DEFAULT_SETTINGS_KEY_MODE));
- if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_AUTO))) {
- return clobberSettingsKey ? KeyboardId.F2KEY_MODE_SHORTCUT_IME
- : KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS;
- } else if (settingsKeyMode.equals(res.getString(SETTINGS_KEY_MODE_ALWAYS_SHOW))) {
- return clobberSettingsKey ? KeyboardId.F2KEY_MODE_NONE : KeyboardId.F2KEY_MODE_SETTINGS;
- } else { // SETTINGS_KEY_MODE_ALWAYS_HIDE
- return KeyboardId.F2KEY_MODE_SHORTCUT_IME;
- }
- }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 2a1b3fb7f..18e01fb49 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -17,8 +17,6 @@
package com.android.inputmethod.keyboard;
import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -32,45 +30,36 @@ import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.util.AttributeSet;
-import android.util.Log;
import android.util.TypedValue;
-import android.view.GestureDetector;
import android.view.LayoutInflater;
-import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
import android.widget.TextView;
-import com.android.inputmethod.accessibility.AccessibilityUtils;
-import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
-import com.android.inputmethod.compat.FrameLayoutCompatUtils;
-import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder;
-import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
-import com.android.inputmethod.keyboard.internal.SwipeTracker;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.StringUtils;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.WeakHashMap;
+import java.util.HashSet;
/**
- * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key
- * presses and touch movements.
+ * A view that renders a virtual {@link Keyboard}.
*
- * @attr ref R.styleable#KeyboardView_backgroundDimAmount
+ * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
* @attr ref R.styleable#KeyboardView_keyBackground
- * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
* @attr ref R.styleable#KeyboardView_keyLetterRatio
* @attr ref R.styleable#KeyboardView_keyLargeLetterRatio
* @attr ref R.styleable#KeyboardView_keyLabelRatio
* @attr ref R.styleable#KeyboardView_keyHintLetterRatio
- * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio
+ * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintRatio
* @attr ref R.styleable#KeyboardView_keyHintLabelRatio
+ * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
+ * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
+ * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
+ * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
* @attr ref R.styleable#KeyboardView_keyTextStyle
* @attr ref R.styleable#KeyboardView_keyPreviewLayout
* @attr ref R.styleable#KeyboardView_keyPreviewTextRatio
@@ -80,188 +69,91 @@ import java.util.WeakHashMap;
* @attr ref R.styleable#KeyboardView_keyTextColorDisabled
* @attr ref R.styleable#KeyboardView_keyHintLetterColor
* @attr ref R.styleable#KeyboardView_keyHintLabelColor
- * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor
- * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor
- * @attr ref R.styleable#KeyboardView_verticalCorrection
- * @attr ref R.styleable#KeyboardView_popupLayout
+ * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintInactivatedColor
+ * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintActivatedColor
* @attr ref R.styleable#KeyboardView_shadowColor
* @attr ref R.styleable#KeyboardView_shadowRadius
*/
-public class KeyboardView extends View implements PointerTracker.UIProxy {
- private static final String TAG = KeyboardView.class.getSimpleName();
- private static final boolean DEBUG_SHOW_ALIGN = false;
- private static final boolean DEBUG_KEYBOARD_GRID = false;
+public class KeyboardView extends View implements PointerTracker.DrawingProxy {
+ // Miscellaneous constants
+ private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
- private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true;
- private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
+ // XML attributes
+ protected final float mVerticalCorrection;
+ protected final int mMoreKeysLayout;
+ private final int mBackgroundDimAlpha;
- // Timing constants
- private final int mKeyRepeatInterval;
+ // HORIZONTAL ELLIPSIS "...", character for popup hint.
+ private static final String POPUP_HINT_CHAR = "\u2026";
- // Miscellaneous constants
- private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
- private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1;
-
- // XML attribute
- private final int mKeyTextColor;
- private final int mKeyTextInactivatedColor;
- private final Typeface mKeyTextStyle;
- private final float mKeyLetterRatio;
- private final float mKeyLargeLetterRatio;
- private final float mKeyLabelRatio;
- private final float mKeyHintLetterRatio;
- private final float mKeyUppercaseLetterRatio;
- private final float mKeyHintLabelRatio;
- private final int mShadowColor;
- private final float mShadowRadius;
- private final Drawable mKeyBackground;
- private final float mBackgroundDimAmount;
- private final float mKeyHysteresisDistance;
- private final float mVerticalCorrection;
- private final Drawable mPreviewBackground;
- private final Drawable mPreviewSpacebarBackground;
- private final int mPreviewTextColor;
- private final float mPreviewTextRatio;
- private final int mPreviewOffset;
- private final int mPreviewHeight;
- private final int mPopupLayout;
- private final Drawable mKeyPopupHintIcon;
- private final int mKeyHintLetterColor;
- private final int mKeyHintLabelColor;
- private final int mKeyUppercaseLetterInactivatedColor;
- private final int mKeyUppercaseLetterActivatedColor;
+ // Margin between the label and the icon on a key that has both of them.
+ // Specified by the fraction of the key width.
+ // TODO: Use resource parameter for this value.
+ private static final float LABEL_ICON_MARGIN = 0.05f;
+
+ // The maximum key label width in the proportion to the key width.
+ private static final float MAX_LABEL_RATIO = 0.90f;
+
+ private final static int ALPHA_OPAQUE = 255;
// Main keyboard
private Keyboard mKeyboard;
- private int mKeyLetterSize;
- private int mKeyLargeLetterSize;
- private int mKeyLabelSize;
- private int mKeyHintLetterSize;
- private int mKeyUppercaseLetterSize;
- private int mKeyHintLabelSize;
+ protected final KeyDrawParams mKeyDrawParams;
// Key preview
- private final TextView mPreviewText;
- private int mPreviewTextSize;
+ private final int mKeyPreviewLayoutId;
+ protected final KeyPreviewDrawParams mKeyPreviewDrawParams;
private boolean mShowKeyPreviewPopup = true;
- private final int mDelayBeforePreview;
private int mDelayAfterPreview;
private ViewGroup mPreviewPlacer;
- private final int[] mCoordinates = new int[2];
-
- // Mini keyboard
- private PopupWindow mPopupWindow;
- private PopupPanel mPopupMiniKeyboardPanel;
- private final WeakHashMap<Key, PopupPanel> mPopupPanelCache =
- new WeakHashMap<Key, PopupPanel>();
-
- /** Listener for {@link KeyboardActionListener}. */
- private KeyboardActionListener mKeyboardActionListener;
-
- private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
-
- // TODO: Let the PointerTracker class manage this pointer queue
- private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
-
- private final boolean mHasDistinctMultitouch;
- private int mOldPointerCount = 1;
- private int mOldKeyIndex;
-
- protected KeyDetector mKeyDetector = new KeyDetector();
-
- // Swipe gesture detector
- protected GestureDetector mGestureDetector;
- private final SwipeTracker mSwipeTracker = new SwipeTracker();
- private final int mSwipeThreshold;
- private final boolean mDisambiguateSwipe;
// Drawing
- /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
- private boolean mDrawPending;
- /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
- private boolean mKeyboardChanged;
- /** The dirty region in the keyboard bitmap */
- private final Rect mDirtyRect = new Rect();
- /** The key to invalidate. */
- private Key mInvalidatedKey;
- /** The dirty region for single key drawing */
- private final Rect mInvalidatedKeyRect = new Rect();
- /** The keyboard bitmap for faster updates */
+ /** True if the entire keyboard needs to be dimmed. */
+ private boolean mNeedsToDimEntireKeyboard;
+ /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/
+ private boolean mBufferNeedsUpdate;
+ /** True if all keys should be drawn */
+ private boolean mInvalidateAllKeys;
+ /** The keys that should be drawn */
+ private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>();
+ /** The region of invalidated keys */
+ private final Rect mInvalidatedKeysRect = new Rect();
+ /** The keyboard bitmap buffer for faster updates */
private Bitmap mBuffer;
/** The canvas for the above mutable keyboard bitmap */
private Canvas mCanvas;
private final Paint mPaint = new Paint();
- private final Rect mPadding = new Rect();
+ private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
// This map caches key label text height in pixel as value and key label text size as map key.
- private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
+ private static final HashMap<Integer, Float> sTextHeightCache =
+ new HashMap<Integer, Float>();
// This map caches key label text width in pixel as value and key label text size as map key.
- private final HashMap<Integer, Integer> mTextWidthCache = new HashMap<Integer, Integer>();
- // Distance from horizontal center of the key, proportional to key label text height and width.
- private static final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
- private static final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
- private static final String KEY_LABEL_REFERENCE_CHAR = "M";
- private final int mKeyLabelHorizontalPadding;
-
- private final UIHandler mHandler = new UIHandler(this);
-
- public static class UIHandler extends StaticInnerHandlerWrapper<KeyboardView> {
- private static final int MSG_SHOW_KEY_PREVIEW = 1;
- private static final int MSG_DISMISS_KEY_PREVIEW = 2;
- private static final int MSG_REPEAT_KEY = 3;
- private static final int MSG_LONGPRESS_KEY = 4;
- private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
- private static final int MSG_IGNORE_DOUBLE_TAP = 6;
-
- private boolean mInKeyRepeat;
-
- public UIHandler(KeyboardView outerInstance) {
+ private static final HashMap<Integer, Float> sTextWidthCache =
+ new HashMap<Integer, Float>();
+ private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
+ private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
+
+ private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
+
+ public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
+ private static final int MSG_DISMISS_KEY_PREVIEW = 1;
+
+ public DrawingHandler(KeyboardView outerInstance) {
super(outerInstance);
}
@Override
public void handleMessage(Message msg) {
final KeyboardView keyboardView = getOuterInstance();
+ if (keyboardView == null) return;
final PointerTracker tracker = (PointerTracker) msg.obj;
switch (msg.what) {
- case MSG_SHOW_KEY_PREVIEW:
- keyboardView.showKey(msg.arg1, tracker);
- break;
case MSG_DISMISS_KEY_PREVIEW:
- keyboardView.mPreviewText.setVisibility(View.INVISIBLE);
- break;
- case MSG_REPEAT_KEY:
- tracker.onRepeatKey(msg.arg1);
- startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, msg.arg1, tracker);
- break;
- case MSG_LONGPRESS_KEY:
- keyboardView.openMiniKeyboardIfRequired(msg.arg1, tracker);
- break;
- case MSG_LONGPRESS_SHIFT_KEY:
- keyboardView.onLongPressShiftKey(tracker);
+ tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
break;
}
}
- public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
- final KeyboardView keyboardView = getOuterInstance();
- removeMessages(MSG_SHOW_KEY_PREVIEW);
- if (keyboardView.mPreviewText.getVisibility() == VISIBLE || delay == 0) {
- // Show right away, if it's already visible and finger is moving around
- keyboardView.showKey(keyIndex, tracker);
- } else {
- sendMessageDelayed(
- obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
- }
- }
-
- public void cancelShowKeyPreview(PointerTracker tracker) {
- removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
- }
-
- public void cancelAllShowKeyPreviews() {
- removeMessages(MSG_SHOW_KEY_PREVIEW);
- }
-
public void dismissKeyPreview(long delay, PointerTracker tracker) {
sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
}
@@ -274,57 +166,186 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
removeMessages(MSG_DISMISS_KEY_PREVIEW);
}
- public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
- mInKeyRepeat = true;
- sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
- }
-
- public void cancelKeyRepeatTimer() {
- mInKeyRepeat = false;
- removeMessages(MSG_REPEAT_KEY);
- }
-
- public boolean isInKeyRepeat() {
- return mInKeyRepeat;
- }
-
- public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
- cancelLongPressTimers();
- sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
+ public void cancelAllMessages() {
+ cancelAllDismissKeyPreviews();
}
+ }
- public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
- cancelLongPressTimers();
- if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
- sendMessageDelayed(
- obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
+ protected static class KeyDrawParams {
+ // XML attributes
+ public final int mKeyTextColor;
+ public final int mKeyTextInactivatedColor;
+ public final Typeface mKeyTextStyle;
+ public final float mKeyLabelHorizontalPadding;
+ public final float mKeyHintLetterPadding;
+ public final float mKeyPopupHintLetterPadding;
+ public final float mKeyShiftedLetterHintPadding;
+ public final int mShadowColor;
+ public final float mShadowRadius;
+ public final Drawable mKeyBackground;
+ public final int mKeyHintLetterColor;
+ public final int mKeyHintLabelColor;
+ public final int mKeyShiftedLetterHintInactivatedColor;
+ public final int mKeyShiftedLetterHintActivatedColor;
+
+ /* package */ final float mKeyLetterRatio;
+ private final float mKeyLargeLetterRatio;
+ private final float mKeyLabelRatio;
+ private final float mKeyLargeLabelRatio;
+ private final float mKeyHintLetterRatio;
+ private final float mKeyShiftedLetterHintRatio;
+ private final float mKeyHintLabelRatio;
+ private static final float UNDEFINED_RATIO = -1.0f;
+
+ public final Rect mPadding = new Rect();
+ public int mKeyLetterSize;
+ public int mKeyLargeLetterSize;
+ public int mKeyLabelSize;
+ public int mKeyLargeLabelSize;
+ public int mKeyHintLetterSize;
+ public int mKeyShiftedLetterHintSize;
+ public int mKeyHintLabelSize;
+ public int mAnimAlpha;
+
+ public KeyDrawParams(TypedArray a) {
+ mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
+ if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) {
+ mKeyLetterRatio = UNDEFINED_RATIO;
+ mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0);
+ } else {
+ mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
}
+ if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) {
+ mKeyLabelRatio = UNDEFINED_RATIO;
+ mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0);
+ } else {
+ mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
+ }
+ mKeyLargeLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLabelRatio);
+ mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
+ mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
+ mKeyShiftedLetterHintRatio = getRatio(a,
+ R.styleable.KeyboardView_keyShiftedLetterHintRatio);
+ mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
+ mKeyLabelHorizontalPadding = a.getDimension(
+ R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
+ mKeyHintLetterPadding = a.getDimension(
+ R.styleable.KeyboardView_keyHintLetterPadding, 0);
+ mKeyPopupHintLetterPadding = a.getDimension(
+ R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
+ mKeyShiftedLetterHintPadding = a.getDimension(
+ R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
+ mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
+ mKeyTextInactivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
+ mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
+ mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
+ mKeyShiftedLetterHintInactivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyShiftedLetterHintInactivatedColor, 0);
+ mKeyShiftedLetterHintActivatedColor = a.getColor(
+ R.styleable.KeyboardView_keyShiftedLetterHintActivatedColor, 0);
+ mKeyTextStyle = Typeface.defaultFromStyle(
+ a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
+ mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
+ mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
+
+ mKeyBackground.getPadding(mPadding);
}
- public void cancelLongPressTimers() {
- removeMessages(MSG_LONGPRESS_KEY);
- removeMessages(MSG_LONGPRESS_SHIFT_KEY);
+ public void updateKeyHeight(int keyHeight) {
+ if (mKeyLetterRatio >= 0.0f)
+ mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
+ if (mKeyLabelRatio >= 0.0f)
+ mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
+ mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio);
+ mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
+ mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
+ mKeyShiftedLetterHintSize = (int)(keyHeight * mKeyShiftedLetterHintRatio);
+ mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
}
- public void cancelKeyTimers() {
- cancelKeyRepeatTimer();
- cancelLongPressTimers();
- removeMessages(MSG_IGNORE_DOUBLE_TAP);
+ public void blendAlpha(Paint paint) {
+ final int color = paint.getColor();
+ paint.setARGB((paint.getAlpha() * mAnimAlpha) / ALPHA_OPAQUE,
+ Color.red(color), Color.green(color), Color.blue(color));
}
+ }
- public void startIgnoringDoubleTap() {
- sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP),
- ViewConfiguration.getDoubleTapTimeout());
+ /* package */ static class KeyPreviewDrawParams {
+ // XML attributes.
+ public final Drawable mPreviewBackground;
+ public final Drawable mPreviewLeftBackground;
+ public final Drawable mPreviewRightBackground;
+ public final int mPreviewTextColor;
+ public final int mPreviewOffset;
+ public final int mPreviewHeight;
+ public final Typeface mKeyTextStyle;
+ public final int mLingerTimeout;
+
+ private final float mPreviewTextRatio;
+ private final float mKeyLetterRatio;
+
+ // The graphical geometry of the key preview.
+ // <-width->
+ // +-------+ ^
+ // | | |
+ // |preview| height (visible)
+ // | | |
+ // + + ^ v
+ // \ / |offset
+ // +-\ /-+ v
+ // | +-+ |
+ // |parent |
+ // | key|
+ // +-------+
+ // The background of a {@link TextView} being used for a key preview may have invisible
+ // paddings. To align the more keys keyboard panel's visible part with the visible part of
+ // the background, we need to record the width and height of key preview that don't include
+ // invisible paddings.
+ public int mPreviewVisibleWidth;
+ public int mPreviewVisibleHeight;
+ // The key preview may have an arbitrary offset and its background that may have a bottom
+ // padding. To align the more keys keyboard and the key preview we also need to record the
+ // offset between the top edge of parent key and the bottom of the visible part of key
+ // preview background.
+ public int mPreviewVisibleOffset;
+
+ public int mPreviewTextSize;
+ public int mKeyLetterSize;
+ public final int[] mCoordinates = new int[2];
+
+ private static final int PREVIEW_ALPHA = 240;
+
+ public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) {
+ mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
+ mPreviewLeftBackground = a.getDrawable(
+ R.styleable.KeyboardView_keyPreviewLeftBackground);
+ mPreviewRightBackground = a.getDrawable(
+ R.styleable.KeyboardView_keyPreviewRightBackground);
+ setAlpha(mPreviewBackground, PREVIEW_ALPHA);
+ setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA);
+ setAlpha(mPreviewRightBackground, PREVIEW_ALPHA);
+ mPreviewOffset = a.getDimensionPixelOffset(
+ R.styleable.KeyboardView_keyPreviewOffset, 0);
+ mPreviewHeight = a.getDimensionPixelSize(
+ R.styleable.KeyboardView_keyPreviewHeight, 80);
+ mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
+ mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
+ mLingerTimeout = a.getInt(R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
+
+ mKeyLetterRatio = keyDrawParams.mKeyLetterRatio;
+ mKeyTextStyle = keyDrawParams.mKeyTextStyle;
}
- public boolean isIgnoringDoubleTap() {
- return hasMessages(MSG_IGNORE_DOUBLE_TAP);
+ public void updateKeyHeight(int keyHeight) {
+ mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
+ mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
}
- public void cancelAllMessages() {
- cancelKeyTimers();
- cancelAllShowKeyPreviews();
- cancelAllDismissKeyPreviews();
+ private static void setAlpha(Drawable drawable, int alpha) {
+ if (drawable == null)
+ return;
+ drawable.setAlpha(alpha);
}
}
@@ -338,173 +359,28 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
- mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
- mKeyHysteresisDistance = a.getDimensionPixelOffset(
- R.styleable.KeyboardView_keyHysteresisDistance, 0);
- mVerticalCorrection = a.getDimensionPixelOffset(
- R.styleable.KeyboardView_verticalCorrection, 0);
- mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
- final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
- if (previewLayout != 0) {
- mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null);
- mPreviewText.setTextColor(mPreviewTextColor);
- } else {
- mPreviewText = null;
+ mKeyDrawParams = new KeyDrawParams(a);
+ mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
+ mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
+ if (mKeyPreviewLayoutId == 0) {
mShowKeyPreviewPopup = false;
}
- mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
- mPreviewSpacebarBackground = a.getDrawable(
- R.styleable.KeyboardView_keyPreviewSpacebarBackground);
- mPreviewOffset = a.getDimensionPixelOffset(R.styleable.KeyboardView_keyPreviewOffset, 0);
- mPreviewHeight = a.getDimensionPixelSize(R.styleable.KeyboardView_keyPreviewHeight, 80);
- mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
- mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
- mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
- mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
- mKeyUppercaseLetterRatio = getRatio(a,
- R.styleable.KeyboardView_keyUppercaseLetterRatio);
- mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
- mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
- mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
- mKeyTextInactivatedColor = a.getColor(
- R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
- mKeyPopupHintIcon = a.getDrawable(R.styleable.KeyboardView_keyPopupHintIcon);
- mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
- mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
- mKeyUppercaseLetterInactivatedColor = a.getColor(
- R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0);
- mKeyUppercaseLetterActivatedColor = a.getColor(
- R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0);
- mKeyTextStyle = Typeface.defaultFromStyle(
- a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
- mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0);
- mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
- mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
- // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
- mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f);
+ mVerticalCorrection = a.getDimensionPixelOffset(
+ R.styleable.KeyboardView_verticalCorrection, 0);
+ mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0);
+ mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0);
a.recycle();
- final Resources res = getResources();
-
- mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
- mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
- mKeyLabelHorizontalPadding = (int)res.getDimension(
- R.dimen.key_label_horizontal_alignment_padding);
+ mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
mPaint.setAntiAlias(true);
- mPaint.setTextAlign(Align.CENTER);
- mPaint.setAlpha(255);
-
- mKeyBackground.getPadding(mPadding);
-
- mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
- // TODO: Refer to frameworks/base/core/res/res/values/config.xml
- mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
-
- GestureDetector.SimpleOnGestureListener listener =
- new GestureDetector.SimpleOnGestureListener() {
- private boolean mProcessingShiftDoubleTapEvent = false;
-
- @Override
- public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
- float velocityY) {
- final float absX = Math.abs(velocityX);
- final float absY = Math.abs(velocityY);
- float deltaY = me2.getY() - me1.getY();
- int travelY = getHeight() / 2; // Half the keyboard height
- mSwipeTracker.computeCurrentVelocity(1000);
- final float endingVelocityY = mSwipeTracker.getYVelocity();
- if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
- if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
- onSwipeDown();
- return true;
- }
- }
- return false;
- }
-
- @Override
- public boolean onDoubleTap(MotionEvent firstDown) {
- if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
- && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
- final int pointerIndex = firstDown.getActionIndex();
- final int id = firstDown.getPointerId(pointerIndex);
- final PointerTracker tracker = getPointerTracker(id);
- // If the first down event is on shift key.
- if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
- mProcessingShiftDoubleTapEvent = true;
- return true;
- }
- }
- mProcessingShiftDoubleTapEvent = false;
- return false;
- }
-
- @Override
- public boolean onDoubleTapEvent(MotionEvent secondTap) {
- if (mProcessingShiftDoubleTapEvent
- && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
- final MotionEvent secondDown = secondTap;
- final int pointerIndex = secondDown.getActionIndex();
- final int id = secondDown.getPointerId(pointerIndex);
- final PointerTracker tracker = getPointerTracker(id);
- // If the second down event is also on shift key.
- if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
- // Detected a double tap on shift key. If we are in the ignoring double tap
- // mode, it means we have already turned off caps lock in
- // {@link KeyboardSwitcher#onReleaseShift} .
- final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap();
- if (!ignoringDoubleTap)
- onDoubleTapShiftKey(tracker);
- return true;
- }
- // Otherwise these events should not be handled as double tap.
- mProcessingShiftDoubleTapEvent = false;
- }
- return mProcessingShiftDoubleTapEvent;
- }
- };
-
- final boolean ignoreMultitouch = true;
- mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
- mGestureDetector.setIsLongpressEnabled(false);
-
- mHasDistinctMultitouch = context.getPackageManager()
- .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
- mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
}
// Read fraction value in TypedArray as float.
- private static float getRatio(TypedArray a, int index) {
+ /* package */ static float getRatio(TypedArray a, int index) {
return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
}
- public void startIgnoringDoubleTap() {
- if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
- mHandler.startIgnoringDoubleTap();
- }
-
- public void setOnKeyboardActionListener(KeyboardActionListener listener) {
- mKeyboardActionListener = listener;
- for (PointerTracker tracker : mPointerTrackers) {
- tracker.setOnKeyboardActionListener(listener);
- }
- }
-
- /**
- * Returns the {@link KeyboardActionListener} object.
- * @return the listener attached to this keyboard
- */
- protected KeyboardActionListener getOnKeyboardActionListener() {
- return mKeyboardActionListener;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- // TODO: Should notify InputMethodService instead?
- KeyboardSwitcher.getInstance().onSizeChanged();
- }
-
/**
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
* view will re-layout itself to accommodate the keyboard.
@@ -513,33 +389,13 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
* @param keyboard the keyboard to display in this view
*/
public void setKeyboard(Keyboard keyboard) {
- if (mKeyboard != null) {
- dismissAllKeyPreviews();
- }
- // Remove any pending messages, except dismissing preview
- mHandler.cancelKeyTimers();
- mHandler.cancelAllShowKeyPreviews();
mKeyboard = keyboard;
LatinImeLogger.onSetKeyboard(keyboard);
- mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
- -getPaddingTop() + mVerticalCorrection);
- for (PointerTracker tracker : mPointerTrackers) {
- tracker.setKeyboard(keyboard, mKeyHysteresisDistance);
- }
requestLayout();
- mKeyboardChanged = true;
invalidateAllKeys();
- mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth());
- mPopupPanelCache.clear();
- final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap();
- mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
- mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
- mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
- mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
- mKeyUppercaseLetterSize = (int)(
- keyHeight * mKeyUppercaseLetterRatio);
- mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
- mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
+ final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
+ mKeyDrawParams.updateKeyHeight(keyHeight);
+ mKeyPreviewDrawParams.updateKeyHeight(keyHeight);
}
/**
@@ -552,15 +408,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
}
/**
- * Returns whether the device has distinct multi-touch panel.
- * @return true if the device has distinct multi-touch panel.
- */
- @Override
- public boolean hasDistinctMultitouch() {
- return mHasDistinctMultitouch;
- }
-
- /**
* Enables or disables the key feedback popup. This is a popup that shows a magnified
* version of the depressed key. By default the preview is enabled.
* @param previewEnabled whether or not to enable the key feedback preview
@@ -581,51 +428,22 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
return mShowKeyPreviewPopup;
}
- /**
- * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
- * codes for adjacent keys. When disabled, only the primary key code will be
- * reported.
- * @param enabled whether or not the proximity correction is enabled
- */
- public void setProximityCorrectionEnabled(boolean enabled) {
- mKeyDetector.setProximityCorrectionEnabled(enabled);
- }
-
- /**
- * Returns true if proximity correction is enabled.
- */
- public boolean isProximityCorrectionEnabled() {
- return mKeyDetector.isProximityCorrectionEnabled();
- }
-
- protected CharSequence adjustCase(CharSequence label) {
- if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
- && Character.isLowerCase(label.charAt(0))) {
- return label.toString().toUpperCase(mKeyboard.mId.mLocale);
- }
- return label;
- }
-
@Override
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // Round up a little
- if (mKeyboard == null) {
- setMeasuredDimension(
- getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mKeyboard != null) {
+ // The main keyboard expands to the display width.
+ final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(widthMeasureSpec, height);
} else {
- int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
- if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
- width = MeasureSpec.getSize(widthMeasureSpec);
- }
- setMeasuredDimension(
- width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
- if (mDrawPending || mBuffer == null || mKeyboardChanged) {
+ if (mBufferNeedsUpdate || mBuffer == null) {
+ mBufferNeedsUpdate = false;
onBufferDraw();
}
canvas.drawBitmap(mBuffer, 0, 0, null);
@@ -636,273 +454,347 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
final int height = getHeight();
if (width == 0 || height == 0)
return;
- if (mBuffer == null || mKeyboardChanged) {
- mKeyboardChanged = false;
- mDirtyRect.union(0, 0, width, height);
- }
if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
if (mBuffer != null)
mBuffer.recycle();
mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ mInvalidateAllKeys = true;
if (mCanvas != null) {
mCanvas.setBitmap(mBuffer);
} else {
mCanvas = new Canvas(mBuffer);
}
}
- final Canvas canvas = mCanvas;
- canvas.clipRect(mDirtyRect, Op.REPLACE);
- canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
if (mKeyboard == null) return;
- if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
- // Draw a single key.
- onBufferDrawKey(canvas, mInvalidatedKey);
- } else {
+ final Canvas canvas = mCanvas;
+ final Paint paint = mPaint;
+ final KeyDrawParams params = mKeyDrawParams;
+
+ if (mInvalidateAllKeys || mInvalidatedKeys.isEmpty()) {
+ mInvalidatedKeysRect.set(0, 0, width, height);
+ canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
+ canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
// Draw all keys.
- for (final Key key : mKeyboard.getKeys()) {
- onBufferDrawKey(canvas, key);
+ for (final Key key : mKeyboard.mKeys) {
+ onDrawKey(key, canvas, paint, params);
+ }
+ if (mNeedsToDimEntireKeyboard) {
+ drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+ }
+ } else {
+ // Draw invalidated keys.
+ for (final Key key : mInvalidatedKeys) {
+ if (!mKeyboard.hasKey(key)) {
+ continue;
+ }
+ final int x = key.mX + getPaddingLeft();
+ final int y = key.mY + getPaddingTop();
+ mInvalidatedKeysRect.set(x, y, x + key.mWidth, y + key.mHeight);
+ canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
+ canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
+ onDrawKey(key, canvas, paint, params);
+ if (mNeedsToDimEntireKeyboard) {
+ drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+ }
}
}
- // TODO: Move this function to ProximityInfo for getting rid of
- // public declarations for
- // GRID_WIDTH and GRID_HEIGHT
- if (DEBUG_KEYBOARD_GRID) {
- Paint p = new Paint();
- p.setStyle(Paint.Style.STROKE);
- p.setStrokeWidth(1.0f);
- p.setColor(0x800000c0);
- int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1)
- / mKeyboard.GRID_WIDTH;
- int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1)
- / mKeyboard.GRID_HEIGHT;
- for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++)
- canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p);
- for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++)
- canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p);
+ mInvalidatedKeys.clear();
+ mInvalidatedKeysRect.setEmpty();
+ mInvalidateAllKeys = false;
+ }
+
+ public void dimEntireKeyboard(boolean dimmed) {
+ final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
+ mNeedsToDimEntireKeyboard = dimmed;
+ if (needsRedrawing) {
+ invalidateAllKeys();
}
+ }
+
+ private void onDrawKey(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+ final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft();
+ final int keyDrawY = key.mY + getPaddingTop();
+ canvas.translate(keyDrawX, keyDrawY);
- // Overlay a dark rectangle to dim the keyboard
- if (mPopupMiniKeyboardPanel != null) {
- mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
- canvas.drawRect(0, 0, width, height, mPaint);
+ params.mAnimAlpha = ALPHA_OPAQUE;
+ if (!key.isSpacer()) {
+ onDrawKeyBackground(key, canvas, params);
}
+ onDrawKeyTopVisuals(key, canvas, paint, params);
- mInvalidatedKey = null;
- mDrawPending = false;
- mDirtyRect.setEmpty();
+ canvas.translate(-keyDrawX, -keyDrawY);
}
- private void onBufferDrawKey(final Canvas canvas, final Key key) {
- final Paint paint = mPaint;
- final Drawable keyBackground = mKeyBackground;
- final Rect padding = mPadding;
- final int kbdPaddingLeft = getPaddingLeft();
- final int kbdPaddingTop = getPaddingTop();
- final int keyDrawX = key.mX + key.mVisualInsetsLeft;
- final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
- final int centerX = (keyDrawWidth + padding.left - padding.right) / 2;
- final float centerY = (key.mHeight + padding.top - padding.bottom) / 2;
- final int rowHeight = padding.top + key.mHeight;
- final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
+ // Draw key background.
+ protected void onDrawKeyBackground(Key key, Canvas canvas, KeyDrawParams params) {
+ final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight
+ + params.mPadding.left + params.mPadding.right;
+ final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom;
+ final int bgX = -params.mPadding.left;
+ final int bgY = -params.mPadding.top;
+ final int[] drawableState = key.getCurrentDrawableState();
+ final Drawable background = params.mKeyBackground;
+ background.setState(drawableState);
+ final Rect bounds = background.getBounds();
+ if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
+ background.setBounds(0, 0, bgWidth, bgHeight);
+ }
+ canvas.translate(bgX, bgY);
+ background.draw(canvas);
+ if (LatinImeLogger.sVISUALDEBUG) {
+ drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint());
+ }
+ canvas.translate(-bgX, -bgY);
+ }
- canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop);
+ // Draw key top visuals.
+ protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+ final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ final int keyHeight = key.mHeight;
+ final float centerX = keyWidth * 0.5f;
+ final float centerY = keyHeight * 0.5f;
- // Draw key background.
- final int[] drawableState = key.getCurrentDrawableState();
- keyBackground.setState(drawableState);
- final Rect bounds = keyBackground.getBounds();
- if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) {
- keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight);
+ if (LatinImeLogger.sVISUALDEBUG) {
+ drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint());
}
- keyBackground.draw(canvas);
// Draw key label.
- int positionX = centerX;
+ final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha);
+ float positionX = centerX;
if (key.mLabel != null) {
- // Switch the character to uppercase if shift is pressed
- final CharSequence label = key.mLabel == null ? null : adjustCase(key.mLabel);
+ final String label = key.mLabel;
// For characters, use large font. For labels like "Done", use smaller font.
- paint.setTypeface(key.selectTypeface(mKeyTextStyle));
- final int labelSize = key.selectTextSize(mKeyLetterSize, mKeyLargeLetterSize,
- mKeyLabelSize, mKeyHintLabelSize);
+ paint.setTypeface(key.selectTypeface(params.mKeyTextStyle));
+ final int labelSize = key.selectTextSize(params.mKeyLetterSize,
+ params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyLargeLabelSize,
+ params.mKeyHintLabelSize);
paint.setTextSize(labelSize);
- final int labelCharHeight = getLabelCharHeight(paint);
- final int labelCharWidth = getLabelCharWidth(paint);
+ final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
+ final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
// Vertical label text alignment.
- final float baseline;
- // TODO: Generalize the following calculations.
- if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_BOTTOM) != 0) {
- baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR;
- } else { // Align center
- baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER;
- }
+ final float baseline = centerY + labelCharHeight / 2;
// Horizontal label text alignment
- if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
- positionX = padding.left + mKeyLabelHorizontalPadding;
+ float labelWidth = 0;
+ if (key.isAlignLeft()) {
+ positionX = (int)params.mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.LEFT);
- } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
- positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right;
+ } else if (key.isAlignRight()) {
+ positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.RIGHT);
- } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT_OF_CENTER) != 0) {
+ } else if (key.isAlignLeftOfCenter()) {
// TODO: Parameterise this?
positionX = centerX - labelCharWidth * 7 / 4;
paint.setTextAlign(Align.LEFT);
+ } else if (key.hasLabelWithIconLeft() && icon != null) {
+ labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+ + LABEL_ICON_MARGIN * keyWidth;
+ positionX = centerX + labelWidth / 2;
+ paint.setTextAlign(Align.RIGHT);
+ } else if (key.hasLabelWithIconRight() && icon != null) {
+ labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth()
+ + LABEL_ICON_MARGIN * keyWidth;
+ positionX = centerX - labelWidth / 2;
+ paint.setTextAlign(Align.LEFT);
} else {
positionX = centerX;
paint.setTextAlign(Align.CENTER);
}
- if (DEBUG_SHOW_ALIGN) {
- final Paint line = new Paint();
- drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000, line);
- drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, line);
+ if (key.needsXScale()) {
+ paint.setTextScaleX(
+ Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
}
- if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) {
- paint.setColor(mKeyTextInactivatedColor);
- } else {
- paint.setColor(mKeyTextColor);
- }
+ paint.setColor(key.isShiftedLetterActivated()
+ ? params.mKeyTextInactivatedColor : params.mKeyTextColor);
if (key.isEnabled()) {
// Set a drop shadow for the text
- paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
+ paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor);
} else {
// Make label invisible
paint.setColor(Color.TRANSPARENT);
}
+ params.blendAlpha(paint);
canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
- // Turn off drop shadow
+ // Turn off drop shadow and reset x-scale.
paint.setShadowLayer(0, 0, 0, 0);
+ paint.setTextScaleX(1.0f);
+
+ if (icon != null) {
+ final int iconWidth = icon.getIntrinsicWidth();
+ final int iconHeight = icon.getIntrinsicHeight();
+ final int iconY = (keyHeight - iconHeight) / 2;
+ if (key.hasLabelWithIconLeft()) {
+ final int iconX = (int)(centerX - labelWidth / 2);
+ drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
+ } else if (key.hasLabelWithIconRight()) {
+ final int iconX = (int)(centerX + labelWidth / 2 - iconWidth);
+ drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
+ }
+ }
+ if (LatinImeLogger.sVISUALDEBUG) {
+ final Paint line = new Paint();
+ drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line);
+ drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line);
+ }
}
// Draw hint label.
if (key.mHintLabel != null) {
- final CharSequence hint = key.mHintLabel;
+ final String hint = key.mHintLabel;
final int hintColor;
final int hintSize;
- if (key.hasUppercaseLetter()) {
- hintColor = isManualTemporaryUpperCase ? mKeyUppercaseLetterActivatedColor
- : mKeyUppercaseLetterInactivatedColor;
- hintSize = mKeyUppercaseLetterSize;
- } else if (key.hasHintLabel()) {
- hintColor = mKeyHintLabelColor;
- hintSize = mKeyHintLabelSize;
+ if (key.hasHintLabel()) {
+ hintColor = params.mKeyHintLabelColor;
+ hintSize = params.mKeyHintLabelSize;
paint.setTypeface(Typeface.DEFAULT);
- } else {
- hintColor = mKeyHintLetterColor;
- hintSize = mKeyHintLetterSize;
+ } else if (key.hasShiftedLetterHint()) {
+ hintColor = key.isShiftedLetterActivated()
+ ? params.mKeyShiftedLetterHintActivatedColor
+ : params.mKeyShiftedLetterHintInactivatedColor;
+ hintSize = params.mKeyShiftedLetterHintSize;
+ } else { // key.hasHintLetter()
+ hintColor = params.mKeyHintLetterColor;
+ hintSize = params.mKeyHintLetterSize;
}
paint.setColor(hintColor);
+ params.blendAlpha(paint);
paint.setTextSize(hintSize);
- // Note: padding.right for drawX?
final float hintX, hintY;
if (key.hasHintLabel()) {
+ // The hint label is placed just right of the key label. Used mainly on
+ // "phone number" layout.
// TODO: Generalize the following calculations.
- hintX = positionX + getLabelCharWidth(paint) * 2;
- hintY = centerY + getLabelCharHeight(paint) / 2;
- } else {
- hintX = keyDrawWidth - getLabelCharWidth(paint);
- hintY = -paint.ascent() + padding.top;
+ hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2;
+ hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+ paint.setTextAlign(Align.LEFT);
+ } else if (key.hasShiftedLetterHint()) {
+ // The hint label is placed at top-right corner of the key. Used mainly on tablet.
+ hintX = keyWidth - params.mKeyShiftedLetterHintPadding
+ - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+ paint.getFontMetrics(mFontMetrics);
+ hintY = -mFontMetrics.top;
+ paint.setTextAlign(Align.CENTER);
+ } else { // key.hasHintLetter()
+ // The hint letter is placed at top-right corner of the key. Used mainly on phone.
+ hintX = keyWidth - params.mKeyHintLetterPadding
+ - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
+ hintY = -paint.ascent();
+ paint.setTextAlign(Align.CENTER);
}
canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint);
+
+ if (LatinImeLogger.sVISUALDEBUG) {
+ final Paint line = new Paint();
+ drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
+ drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
+ }
}
// Draw key icon.
- final Drawable icon = key.getIcon();
if (key.mLabel == null && icon != null) {
final int iconWidth = icon.getIntrinsicWidth();
final int iconHeight = icon.getIntrinsicHeight();
final int iconX, alignX;
- final int iconY = (key.mHeight + padding.top - padding.bottom - iconHeight) / 2;
- if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_LEFT) != 0) {
- iconX = padding.left + mKeyLabelHorizontalPadding;
+ final int iconY = (keyHeight - iconHeight) / 2;
+ if (key.isAlignLeft()) {
+ iconX = (int)params.mKeyLabelHorizontalPadding;
alignX = iconX;
- } else if ((key.mLabelOption & Key.LABEL_OPTION_ALIGN_RIGHT) != 0) {
- iconX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding - iconWidth;
+ } else if (key.isAlignRight()) {
+ iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth;
alignX = iconX + iconWidth;
} else { // Align center
- iconX = (keyDrawWidth + padding.left - padding.right - iconWidth) / 2;
+ iconX = (keyWidth - iconWidth) / 2;
alignX = iconX + iconWidth / 2;
}
drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight);
- if (DEBUG_SHOW_ALIGN) {
+
+ if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
- drawVerticalLine(canvas, alignX, rowHeight, 0xc0800080, line);
+ drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line);
drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line);
}
}
- // Draw popup hint icon "...".
- // TODO: Draw "..." by text.
- if (key.hasPopupHint()) {
- final int drawableWidth = keyDrawWidth;
- final int drawableHeight = key.mHeight;
- final int drawableX = 0;
- final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
- final Drawable hintIcon = mKeyPopupHintIcon;
- drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight);
- if (DEBUG_SHOW_ALIGN) {
- drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
- 0x80c0c000, new Paint());
- }
+ if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) {
+ drawKeyPopupHint(key, canvas, paint, params);
}
+ }
- canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop);
+ // Draw popup hint "..." at the bottom right corner of the key.
+ protected void drawKeyPopupHint(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+ final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ final int keyHeight = key.mHeight;
+
+ paint.setTypeface(params.mKeyTextStyle);
+ paint.setTextSize(params.mKeyHintLetterSize);
+ paint.setColor(params.mKeyHintLabelColor);
+ paint.setTextAlign(Align.CENTER);
+ final float hintX = keyWidth - params.mKeyHintLetterPadding
+ - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
+ final float hintY = keyHeight - params.mKeyPopupHintLetterPadding;
+ canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
+
+ if (LatinImeLogger.sVISUALDEBUG) {
+ final Paint line = new Paint();
+ drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line);
+ drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line);
+ }
}
- // This method is currently being used only by MiniKeyboardBuilder
- public int getDefaultLabelSizeAndSetPaint(Paint paint) {
- // For characters, use large font. For labels like "Done", use small font.
- final int labelSize = mKeyLabelSize;
- paint.setTextSize(labelSize);
- paint.setTypeface(mKeyTextStyle);
- return labelSize;
+ private static int getCharGeometryCacheKey(char referenceChar, Paint paint) {
+ final int labelSize = (int)paint.getTextSize();
+ final Typeface face = paint.getTypeface();
+ final int codePointOffset = referenceChar << 15;
+ if (face == Typeface.DEFAULT) {
+ return codePointOffset + labelSize;
+ } else if (face == Typeface.DEFAULT_BOLD) {
+ return codePointOffset + labelSize + 0x1000;
+ } else if (face == Typeface.MONOSPACE) {
+ return codePointOffset + labelSize + 0x2000;
+ } else {
+ return codePointOffset + labelSize;
+ }
}
+ // Working variable for the following methods.
private final Rect mTextBounds = new Rect();
- private int getLabelCharHeight(Paint paint) {
- final int labelSize = (int)paint.getTextSize();
- final Integer cachedValue = mTextHeightCache.get(labelSize);
+ private float getCharHeight(char[] referenceChar, Paint paint) {
+ final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+ final Float cachedValue = sTextHeightCache.get(key);
if (cachedValue != null)
return cachedValue;
- paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
- final int height = mTextBounds.height();
- mTextHeightCache.put(labelSize, height);
+ paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
+ final float height = mTextBounds.height();
+ sTextHeightCache.put(key, height);
return height;
}
- private int getLabelCharWidth(Paint paint) {
- final int labelSize = (int)paint.getTextSize();
- final Typeface face = paint.getTypeface();
- final Integer key;
- if (face == Typeface.DEFAULT) {
- key = labelSize;
- } else if (face == Typeface.DEFAULT_BOLD) {
- key = labelSize + 1000;
- } else if (face == Typeface.MONOSPACE) {
- key = labelSize + 2000;
- } else {
- key = labelSize;
- }
-
- final Integer cached = mTextWidthCache.get(key);
- if (cached != null)
- return cached;
+ private float getCharWidth(char[] referenceChar, Paint paint) {
+ final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+ final Float cachedValue = sTextWidthCache.get(key);
+ if (cachedValue != null)
+ return cachedValue;
- paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, mTextBounds);
- final int width = mTextBounds.width();
- mTextWidthCache.put(key, width);
+ paint.getTextBounds(referenceChar, 0, 1, mTextBounds);
+ final float width = mTextBounds.width();
+ sTextWidthCache.put(key, width);
return width;
}
- private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
+ public float getLabelWidth(String label, Paint paint) {
+ paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds);
+ return mTextBounds.width();
+ }
+
+ protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
int height) {
canvas.translate(x, y);
icon.setBounds(0, 0, width, height);
@@ -910,21 +802,22 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
canvas.translate(-x, -y);
}
- private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) {
+ private static void drawHorizontalLine(Canvas canvas, float y, float w, int color,
+ Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(0, y, w, y, paint);
}
- private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) {
+ private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(x, 0, x, h, paint);
}
- private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color,
+ private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color,
Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
@@ -934,108 +827,134 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
canvas.translate(-x, -y);
}
- // TODO: clean up this method.
- private void dismissAllKeyPreviews() {
- for (PointerTracker tracker : mPointerTrackers) {
- tracker.setReleasedKeyGraphics();
- dismissKeyPreview(tracker);
- }
+ // Overlay a dark rectangle to dim.
+ private static void drawDimRectangle(Canvas canvas, Rect rect, int alpha, Paint paint) {
+ paint.setColor(Color.BLACK);
+ paint.setAlpha(alpha);
+ canvas.drawRect(rect, paint);
}
- public void cancelAllMessage() {
- mHandler.cancelAllMessages();
+ public Paint newDefaultLabelPaint() {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
+ paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
+ return paint;
}
+ public void cancelAllMessages() {
+ mDrawingHandler.cancelAllMessages();
+ }
+
+ // Called by {@link PointerTracker} constructor to create a TextView.
@Override
- public void showKeyPreview(int keyIndex, PointerTracker tracker) {
- if (mShowKeyPreviewPopup) {
- mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
- } else if (mKeyboard.needSpacebarPreview(keyIndex)) {
- // Show key preview (in this case, slide language switcher) without any delay.
- showKey(keyIndex, tracker);
+ public TextView inflateKeyPreviewText() {
+ final Context context = getContext();
+ if (mKeyPreviewLayoutId != 0) {
+ return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
+ } else {
+ return new TextView(context);
}
}
@Override
public void dismissKeyPreview(PointerTracker tracker) {
- if (mShowKeyPreviewPopup) {
- mHandler.cancelShowKeyPreview(tracker);
- mHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
- } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) {
- // Dismiss key preview (in this case, slide language switcher) without any delay.
- mPreviewText.setVisibility(View.INVISIBLE);
- }
+ mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
}
private void addKeyPreview(TextView keyPreview) {
if (mPreviewPlacer == null) {
- mPreviewPlacer = FrameLayoutCompatUtils.getPlacer(
- (ViewGroup)getRootView().findViewById(android.R.id.content));
+ mPreviewPlacer = new RelativeLayout(getContext());
+ final ViewGroup windowContentView =
+ (ViewGroup)getRootView().findViewById(android.R.id.content);
+ windowContentView.addView(mPreviewPlacer);
}
- final ViewGroup placer = mPreviewPlacer;
- placer.addView(keyPreview, FrameLayoutCompatUtils.newLayoutParam(placer, 0, 0));
+ mPreviewPlacer.addView(
+ keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0));
}
- // TODO: Introduce minimum duration for displaying key previews
- // TODO: Display up to two key previews when the user presses two keys at the same time
- private void showKey(final int keyIndex, PointerTracker tracker) {
- final TextView previewText = mPreviewText;
+ @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16
+ @Override
+ public void showKeyPreview(PointerTracker tracker) {
+ if (!mShowKeyPreviewPopup) return;
+
+ final TextView previewText = tracker.getKeyPreviewText();
// If the key preview has no parent view yet, add it to the ViewGroup which can place
// key preview absolutely in SoftInputWindow.
if (previewText.getParent() == null) {
addKeyPreview(previewText);
}
- final Key key = tracker.getKey(keyIndex);
- // If keyIndex is invalid or IME is already closed, we must not show key preview.
+ mDrawingHandler.cancelDismissKeyPreview(tracker);
+ final Key key = tracker.getKey();
+ // If key is invalid or IME is already closed, we must not show key preview.
// Trying to show key preview while root window is closed causes
// WindowManager.BadTokenException.
if (key == null)
return;
- mHandler.cancelAllDismissKeyPreviews();
-
- final int keyDrawX = key.mX + key.mVisualInsetsLeft;
- final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
- // What we show as preview should match what we show on key top in onBufferDraw().
- if (key.mLabel != null) {
+ final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
+ final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel;
+ // What we show as preview should match what we show on a key top in onBufferDraw().
+ if (label != null) {
// TODO Should take care of temporaryShiftLabel here.
previewText.setCompoundDrawables(null, null, null, null);
- if (key.mLabel.length() > 1) {
- previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
+ if (StringUtils.codePointCount(label) > 1) {
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
previewText.setTypeface(Typeface.DEFAULT_BOLD);
} else {
- previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSize);
- previewText.setTypeface(mKeyTextStyle);
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize);
+ previewText.setTypeface(params.mKeyTextStyle);
}
- previewText.setText(adjustCase(tracker.getPreviewText(key)));
+ previewText.setText(label);
} else {
- final Drawable previewIcon = key.getPreviewIcon();
previewText.setCompoundDrawables(null, null, null,
- previewIcon != null ? previewIcon : key.getIcon());
+ key.getPreviewIcon(mKeyboard.mIconsSet));
previewText.setText(null);
}
- if (key.mCode == Keyboard.CODE_SPACE) {
- previewText.setBackgroundDrawable(mPreviewSpacebarBackground);
- } else {
- previewText.setBackgroundDrawable(mPreviewBackground);
+ previewText.setBackgroundDrawable(params.mPreviewBackground);
+
+ previewText.measure(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ final int previewWidth = previewText.getMeasuredWidth();
+ final int previewHeight = params.mPreviewHeight;
+ // The width and height of visible part of the key preview background. The content marker
+ // of the background 9-patch have to cover the visible part of the background.
+ params.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
+ - previewText.getPaddingRight();
+ params.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
+ - previewText.getPaddingBottom();
+ // The distance between the top edge of the parent key and the bottom of the visible part
+ // of the key preview background.
+ params.mPreviewVisibleOffset = params.mPreviewOffset - previewText.getPaddingBottom();
+ getLocationInWindow(params.mCoordinates);
+ // The key preview is horizontally aligned with the center of the visible part of the
+ // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
+ // the left/right background is used if such background is specified.
+ int previewX = key.mX + key.mVisualInsetsLeft - (previewWidth - keyDrawWidth) / 2
+ + params.mCoordinates[0];
+ if (previewX < 0) {
+ previewX = 0;
+ if (params.mPreviewLeftBackground != null) {
+ previewText.setBackgroundDrawable(params.mPreviewLeftBackground);
+ }
+ } else if (previewX > getWidth() - previewWidth) {
+ previewX = getWidth() - previewWidth;
+ if (params.mPreviewRightBackground != null) {
+ previewText.setBackgroundDrawable(params.mPreviewRightBackground);
+ }
}
+ // The key preview is placed vertically above the top edge of the parent key with an
+ // arbitrary offset.
+ final int previewY = key.mY - previewHeight + params.mPreviewOffset
+ + params.mCoordinates[1];
+
// Set the preview background state
previewText.getBackground().setState(
- key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
-
- previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
- + previewText.getPaddingLeft() + previewText.getPaddingRight());
- final int previewHeight = mPreviewHeight;
- getLocationInWindow(mCoordinates);
- final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
- final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset;
-
- // Place the key preview.
- // TODO: Adjust position of key previews which touch screen edges
- FrameLayoutCompatUtils.placeViewAt(
+ key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
+ previewText.setTextColor(params.mPreviewTextColor);
+ ViewLayoutUtils.placeViewAt(
previewText, previewX, previewY, previewWidth, previewHeight);
previewText.setVisibility(VISIBLE);
}
@@ -1047,8 +966,9 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
* @see #invalidateKey(Key)
*/
public void invalidateAllKeys() {
- mDirtyRect.union(0, 0, getWidth(), getHeight());
- mDrawPending = true;
+ mInvalidatedKeys.clear();
+ mInvalidateAllKeys = true;
+ mBufferNeedsUpdate = true;
invalidate();
}
@@ -1061,278 +981,27 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
*/
@Override
public void invalidateKey(Key key) {
- if (key == null)
- return;
- mInvalidatedKey = key;
+ if (mInvalidateAllKeys) return;
+ if (key == null) return;
+ mInvalidatedKeys.add(key);
final int x = key.mX + getPaddingLeft();
final int y = key.mY + getPaddingTop();
- mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
- mDirtyRect.union(mInvalidatedKeyRect);
- onBufferDraw();
- invalidate(mInvalidatedKeyRect);
- }
-
- private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
- // Check if we have a popup layout specified first.
- if (mPopupLayout == 0) {
- return false;
- }
-
- final Key parentKey = tracker.getKey(keyIndex);
- if (parentKey == null)
- return false;
- boolean result = onLongPress(parentKey, tracker);
- if (result) {
- dismissAllKeyPreviews();
- tracker.onLongPressed(mPointerQueue);
- }
- return result;
- }
-
- private void onLongPressShiftKey(PointerTracker tracker) {
- tracker.onLongPressed(mPointerQueue);
- mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
- }
-
- private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
- // When shift key is double tapped, the first tap is correctly processed as usual tap. And
- // the second tap is treated as this double tap event, so that we need not mark tracker
- // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
- mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
+ mInvalidatedKeysRect.union(x, y, x + key.mWidth, y + key.mHeight);
+ mBufferNeedsUpdate = true;
+ invalidate(mInvalidatedKeysRect);
}
- // This default implementation returns a popup mini keyboard panel.
- // A derived class may return a language switcher popup panel, for instance.
- protected PopupPanel onCreatePopupPanel(Key parentKey) {
- if (parentKey.mPopupCharacters == null)
- return null;
-
- final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
- if (container == null)
- throw new NullPointerException();
-
- final PopupMiniKeyboardView miniKeyboardView =
- (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
- miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
- @Override
- public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
- mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
- dismissMiniKeyboard();
- }
-
- @Override
- public void onTextInput(CharSequence text) {
- mKeyboardActionListener.onTextInput(text);
- dismissMiniKeyboard();
- }
-
- @Override
- public void onCancelInput() {
- mKeyboardActionListener.onCancelInput();
- dismissMiniKeyboard();
- }
-
- @Override
- public void onSwipeDown() {
- // Nothing to do.
- }
- @Override
- public void onPress(int primaryCode, boolean withSliding) {
- mKeyboardActionListener.onPress(primaryCode, withSliding);
- }
- @Override
- public void onRelease(int primaryCode, boolean withSliding) {
- mKeyboardActionListener.onRelease(primaryCode, withSliding);
- }
- });
-
- final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
- parentKey, mKeyboard).build();
- miniKeyboardView.setKeyboard(keyboard);
-
- container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
-
- return miniKeyboardView;
- }
-
- /**
- * Called when a key is long pressed. By default this will open mini keyboard associated
- * with this key.
- * @param parentKey the key that was long pressed
- * @param tracker the pointer tracker which pressed the parent key
- * @return true if the long press is handled, false otherwise. Subclasses should call the
- * method on the base class if the subclass doesn't wish to handle the call.
- */
- protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
- PopupPanel popupPanel = mPopupPanelCache.get(parentKey);
- if (popupPanel == null) {
- popupPanel = onCreatePopupPanel(parentKey);
- if (popupPanel == null)
- return false;
- mPopupPanelCache.put(parentKey, popupPanel);
- }
- if (mPopupWindow == null) {
- mPopupWindow = new PopupWindow(getContext());
- mPopupWindow.setBackgroundDrawable(null);
- mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation);
- // Allow popup window to be drawn off the screen.
- mPopupWindow.setClippingEnabled(false);
- }
- mPopupMiniKeyboardPanel = popupPanel;
- popupPanel.showPanel(this, parentKey, tracker, mPopupWindow);
-
- invalidateAllKeys();
- return true;
- }
-
- private PointerTracker getPointerTracker(final int id) {
- final ArrayList<PointerTracker> pointers = mPointerTrackers;
- final KeyboardActionListener listener = mKeyboardActionListener;
-
- // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
- for (int i = pointers.size(); i <= id; i++) {
- final PointerTracker tracker =
- new PointerTracker(i, this, mHandler, mKeyDetector, this);
- if (mKeyboard != null)
- tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance);
- if (listener != null)
- tracker.setOnKeyboardActionListener(listener);
- pointers.add(tracker);
- }
-
- return pointers.get(id);
- }
-
- public boolean isInSlidingKeyInput() {
- if (mPopupMiniKeyboardPanel != null) {
- return mPopupMiniKeyboardPanel.isInSlidingKeyInput();
- } else {
- return mPointerQueue.isInSlidingKeyInput();
- }
- }
+ public void closing() {
+ PointerTracker.dismissAllKeyPreviews();
+ cancelAllMessages();
- public int getPointerCount() {
- return mOldPointerCount;
+ mInvalidateAllKeys = true;
+ requestLayout();
}
@Override
- public boolean onTouchEvent(MotionEvent me) {
- final int action = me.getActionMasked();
- final int pointerCount = me.getPointerCount();
- final int oldPointerCount = mOldPointerCount;
- mOldPointerCount = pointerCount;
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // If the device does not have distinct multi-touch support panel, ignore all multi-touch
- // events except a transition from/to single-touch.
- if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
- return true;
- }
-
- // Track the last few movements to look for spurious swipes.
- mSwipeTracker.addMovement(me);
-
- // Gesture detector must be enabled only when mini-keyboard is not on the screen.
- if (mPopupMiniKeyboardPanel == null && mGestureDetector != null
- && mGestureDetector.onTouchEvent(me)) {
- dismissAllKeyPreviews();
- mHandler.cancelKeyTimers();
- return true;
- }
-
- final long eventTime = me.getEventTime();
- final int index = me.getActionIndex();
- final int id = me.getPointerId(index);
- final int x = (int)me.getX(index);
- final int y = (int)me.getY(index);
-
- // Needs to be called after the gesture detector gets a turn, as it may have displayed the
- // mini keyboard
- if (mPopupMiniKeyboardPanel != null) {
- return mPopupMiniKeyboardPanel.onTouchEvent(me);
- }
-
- if (mHandler.isInKeyRepeat()) {
- final PointerTracker tracker = getPointerTracker(id);
- // Key repeating timer will be canceled if 2 or more keys are in action, and current
- // event (UP or DOWN) is non-modifier key.
- if (pointerCount > 1 && !tracker.isModifier()) {
- mHandler.cancelKeyRepeatTimer();
- }
- // Up event will pass through.
- }
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // Translate mutli-touch event to single-touch events on the device that has no distinct
- // multi-touch panel.
- if (!mHasDistinctMultitouch) {
- // Use only main (id=0) pointer tracker.
- PointerTracker tracker = getPointerTracker(0);
- if (pointerCount == 1 && oldPointerCount == 2) {
- // Multi-touch to single touch transition.
- // Send a down event for the latest pointer if the key is different from the
- // previous key.
- final int newKeyIndex = tracker.getKeyIndexOn(x, y);
- if (mOldKeyIndex != newKeyIndex) {
- tracker.onDownEvent(x, y, eventTime, null);
- if (action == MotionEvent.ACTION_UP)
- tracker.onUpEvent(x, y, eventTime, null);
- }
- } else if (pointerCount == 2 && oldPointerCount == 1) {
- // Single-touch to multi-touch transition.
- // Send an up event for the last pointer.
- final int lastX = tracker.getLastX();
- final int lastY = tracker.getLastY();
- mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
- tracker.onUpEvent(lastX, lastY, eventTime, null);
- } else if (pointerCount == 1 && oldPointerCount == 1) {
- tracker.onTouchEvent(action, x, y, eventTime, null);
- } else {
- Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
- + " (old " + oldPointerCount + ")");
- }
- return true;
- }
-
- final PointerTrackerQueue queue = mPointerQueue;
- if (action == MotionEvent.ACTION_MOVE) {
- for (int i = 0; i < pointerCount; i++) {
- final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
- tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
- }
- } else {
- final PointerTracker tracker = getPointerTracker(id);
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- case MotionEvent.ACTION_POINTER_DOWN:
- tracker.onDownEvent(x, y, eventTime, queue);
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_POINTER_UP:
- tracker.onUpEvent(x, y, eventTime, queue);
- break;
- case MotionEvent.ACTION_CANCEL:
- tracker.onCancelEvent(x, y, eventTime, queue);
- break;
- }
- }
-
- return true;
- }
-
- protected void onSwipeDown() {
- mKeyboardActionListener.onSwipeDown();
- }
-
- public void closing() {
- mPreviewText.setVisibility(View.GONE);
- mHandler.cancelAllMessages();
-
- dismissMiniKeyboard();
- mDirtyRect.union(0, 0, getWidth(), getHeight());
- mPopupPanelCache.clear();
- requestLayout();
+ public boolean dismissMoreKeysPanel() {
+ return false;
}
public void purgeKeyboardAndClosing() {
@@ -1341,55 +1010,15 @@ public class KeyboardView extends View implements PointerTracker.UIProxy {
}
@Override
- public void onDetachedFromWindow() {
+ protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
closing();
- }
-
- private boolean dismissMiniKeyboard() {
- if (mPopupWindow != null && mPopupWindow.isShowing()) {
- mPopupWindow.dismiss();
- mPopupMiniKeyboardPanel = null;
- invalidateAllKeys();
- return true;
+ if (mPreviewPlacer != null) {
+ mPreviewPlacer.removeAllViews();
}
- return false;
- }
-
- public boolean handleBack() {
- return dismissMiniKeyboard();
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
- return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event)
- || super.dispatchTouchEvent(event);
- }
-
- return super.dispatchTouchEvent(event);
- }
-
- @Override
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
- final PointerTracker tracker = getPointerTracker(0);
- return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent(
- event, tracker) || super.dispatchPopulateAccessibilityEvent(event);
- }
-
- return super.dispatchPopulateAccessibilityEvent(event);
- }
-
- public boolean onHoverEvent(MotionEvent event) {
- // Since reflection doesn't support calling superclass methods, this
- // method checks for the existence of onHoverEvent() in the View class
- // before returning a value.
- if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
- final PointerTracker tracker = getPointerTracker(0);
- return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker);
+ if (mBuffer != null) {
+ mBuffer.recycle();
+ mBuffer = null;
}
-
- return false;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java
deleted file mode 100644
index 1966d2d65..000000000
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboard.java
+++ /dev/null
@@ -1,433 +0,0 @@
-/*
- * Copyright (C) 2008 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.keyboard;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.Resources.Theme;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-
-import com.android.inputmethod.keyboard.internal.SlidingLocaleDrawable;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-
-import java.lang.ref.SoftReference;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-
-// TODO: We should remove this class
-public class LatinKeyboard extends Keyboard {
- private static final int SPACE_LED_LENGTH_PERCENT = 80;
-
- public static final int CODE_NEXT_LANGUAGE = -100;
- public static final int CODE_PREV_LANGUAGE = -101;
-
- private final Resources mRes;
- private final Theme mTheme;
- private final SubtypeSwitcher mSubtypeSwitcher = SubtypeSwitcher.getInstance();
-
- /* Space key and its icons, drawables and colors. */
- private final Key mSpaceKey;
- private final Drawable mSpaceIcon;
- private final Drawable mSpacePreviewIcon;
- private final int mSpaceKeyIndex;
- private final boolean mAutoCorrectionSpacebarLedEnabled;
- private final Drawable mAutoCorrectionSpacebarLedIcon;
- private final Drawable mSpacebarArrowLeftIcon;
- private final Drawable mSpacebarArrowRightIcon;
- private final int mSpacebarTextColor;
- private final int mSpacebarTextShadowColor;
- private float mSpacebarTextFadeFactor = 0.0f;
- private final int mSpacebarLanguageSwitchThreshold;
- private int mSpacebarSlidingLanguageSwitchDiff;
- private final SlidingLocaleDrawable mSlidingLocaleIcon;
- private final HashMap<Integer, SoftReference<BitmapDrawable>> mSpaceDrawableCache =
- new HashMap<Integer, SoftReference<BitmapDrawable>>();
-
- /* Shortcut key and its icons if available */
- private final Key mShortcutKey;
- private final Drawable mEnabledShortcutIcon;
- private final Drawable mDisabledShortcutIcon;
-
- // Minimum width of spacebar dragging to trigger the language switch (represented by the number
- // of the most common key width of this keyboard).
- private static final int SPACEBAR_DRAG_WIDTH = 3;
- // Minimum width of space key preview (proportional to keyboard width).
- private static final float SPACEBAR_POPUP_MIN_RATIO = 0.5f;
- // Height in space key the language name will be drawn. (proportional to space key height)
- public static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
- // If the full language name needs to be smaller than this value to be drawn on space key,
- // its short language name will be used instead.
- private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
-
- private static final String SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "small";
- private static final String MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "medium";
-
- public LatinKeyboard(Context context, KeyboardId id, int width) {
- super(context, id.getXmlId(), id, width);
- mRes = context.getResources();
- mTheme = context.getTheme();
-
- final List<Key> keys = getKeys();
- int spaceKeyIndex = -1;
- int shortcutKeyIndex = -1;
- final int keyCount = keys.size();
- for (int index = 0; index < keyCount; index++) {
- // For now, assuming there are up to one space key and one shortcut key respectively.
- switch (keys.get(index).mCode) {
- case CODE_SPACE:
- spaceKeyIndex = index;
- break;
- case CODE_SHORTCUT:
- shortcutKeyIndex = index;
- break;
- }
- }
-
- // The index of space key is available only after Keyboard constructor has finished.
- mSpaceKey = (spaceKeyIndex >= 0) ? keys.get(spaceKeyIndex) : null;
- mSpaceIcon = (mSpaceKey != null) ? mSpaceKey.getIcon() : null;
- mSpacePreviewIcon = (mSpaceKey != null) ? mSpaceKey.getPreviewIcon() : null;
- mSpaceKeyIndex = spaceKeyIndex;
-
- mShortcutKey = (shortcutKeyIndex >= 0) ? keys.get(shortcutKeyIndex) : null;
- mEnabledShortcutIcon = (mShortcutKey != null) ? mShortcutKey.getIcon() : null;
-
- final TypedArray a = context.obtainStyledAttributes(
- null, R.styleable.LatinKeyboard, R.attr.latinKeyboardStyle, R.style.LatinKeyboard);
- mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
- R.styleable.LatinKeyboard_autoCorrectionSpacebarLedEnabled, false);
- mAutoCorrectionSpacebarLedIcon = a.getDrawable(
- R.styleable.LatinKeyboard_autoCorrectionSpacebarLedIcon);
- mDisabledShortcutIcon = a.getDrawable(R.styleable.LatinKeyboard_disabledShortcutIcon);
- mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboard_spacebarTextColor, 0);
- mSpacebarTextShadowColor = a.getColor(
- R.styleable.LatinKeyboard_spacebarTextShadowColor, 0);
- mSpacebarArrowLeftIcon = a.getDrawable(
- R.styleable.LatinKeyboard_spacebarArrowLeftIcon);
- mSpacebarArrowRightIcon = a.getDrawable(
- R.styleable.LatinKeyboard_spacebarArrowRightIcon);
- a.recycle();
-
- // The threshold is "key width" x 1.25
- mSpacebarLanguageSwitchThreshold = (getMostCommonKeyWidth() * 5) / 4;
-
- if (mSpaceKey != null && mSpacePreviewIcon != null) {
- final int slidingIconWidth = Math.max(mSpaceKey.mWidth,
- (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO));
- final int spaceKeyheight = mSpacePreviewIcon.getIntrinsicHeight();
- mSlidingLocaleIcon = new SlidingLocaleDrawable(
- context, mSpacePreviewIcon, slidingIconWidth, spaceKeyheight);
- mSlidingLocaleIcon.setBounds(0, 0, slidingIconWidth, spaceKeyheight);
- } else {
- mSlidingLocaleIcon = null;
- }
- }
-
- public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboardView view) {
- mSpacebarTextFadeFactor = fadeFactor;
- updateSpacebarForLocale(false);
- if (view != null)
- view.invalidateKey(mSpaceKey);
- }
-
- private static int getSpacebarTextColor(int color, float fadeFactor) {
- final int newColor = Color.argb((int)(Color.alpha(color) * fadeFactor),
- Color.red(color), Color.green(color), Color.blue(color));
- return newColor;
- }
-
- private static ColorFilter getSpacebarDrawableFilter(float fadeFactor) {
- final ColorMatrix colorMatrix = new ColorMatrix();
- colorMatrix.setScale(1, 1, 1, fadeFactor);
- return new ColorMatrixColorFilter(colorMatrix);
- }
-
- public void updateShortcutKey(boolean available, LatinKeyboardView view) {
- if (mShortcutKey == null)
- return;
- mShortcutKey.setEnabled(available);
- mShortcutKey.setIcon(available ? mEnabledShortcutIcon : mDisabledShortcutIcon);
- if (view != null)
- view.invalidateKey(mShortcutKey);
- }
-
- public boolean needsAutoCorrectionSpacebarLed() {
- return mAutoCorrectionSpacebarLedEnabled;
- }
-
- /**
- * @return a key which should be invalidated.
- */
- public Key onAutoCorrectionStateChanged(boolean isAutoCorrection) {
- updateSpacebarForLocale(isAutoCorrection);
- return mSpaceKey;
- }
-
- private void updateSpacebarForLocale(boolean isAutoCorrection) {
- if (mSpaceKey == null)
- return;
- // If application locales are explicitly selected.
- if (mSubtypeSwitcher.needsToDisplayLanguage()) {
- mSpaceKey.setIcon(getSpaceDrawable(
- mSubtypeSwitcher.getInputLocale(), isAutoCorrection));
- } else if (isAutoCorrection) {
- mSpaceKey.setIcon(getSpaceDrawable(null, true));
- } else {
- mSpaceKey.setIcon(mSpaceIcon);
- }
- }
-
- // Compute width of text with specified text size using paint.
- private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
- paint.setTextSize(textSize);
- paint.getTextBounds(text, 0, text.length(), bounds);
- return bounds.width();
- }
-
- // Layout local language name and left and right arrow on spacebar.
- private static String layoutSpacebar(Paint paint, Locale locale, Drawable icon, Drawable lArrow,
- Drawable rArrow, int width, int height, float origTextSize) {
- final float arrowWidth;
- if (lArrow != null && rArrow != null) {
- arrowWidth = lArrow.getIntrinsicWidth();
- } else {
- arrowWidth = 0;
- }
- final float maxTextWidth = width - (arrowWidth + arrowWidth);
- final Rect bounds = new Rect();
-
- // Estimate appropriate language name text size to fit in maxTextWidth.
- String language = SubtypeSwitcher.getFullDisplayName(locale, true);
- int textWidth = getTextWidth(paint, language, origTextSize, bounds);
- // Assuming text width and text size are proportional to each other.
- float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
- // allow variable text size
- textWidth = getTextWidth(paint, language, textSize, bounds);
- // If text size goes too small or text does not fit, use middle or short name
- final boolean useMiddleName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
- || (textWidth > maxTextWidth);
-
- final boolean useShortName;
- if (useMiddleName) {
- language = SubtypeSwitcher.getMiddleDisplayLanguage(locale);
- textWidth = getTextWidth(paint, language, origTextSize, bounds);
- textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
- useShortName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
- || (textWidth > maxTextWidth);
- } else {
- useShortName = false;
- }
-
- if (useShortName) {
- language = SubtypeSwitcher.getShortDisplayLanguage(locale);
- textWidth = getTextWidth(paint, language, origTextSize, bounds);
- textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f);
- }
- paint.setTextSize(textSize);
-
- // Place left and right arrow just before and after language text.
- if (lArrow != null && rArrow != null) {
- final float textHeight = -paint.ascent() + paint.descent();
- final float baseline = (icon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
- : height / 2 + textHeight / 2;
- final int arrowHeight = lArrow.getIntrinsicHeight();
- final int top = (int)(baseline - arrowHeight);
- final float remains = (width - textWidth) / 2;
- lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline);
- rArrow.setBounds((int)(remains + textWidth), top,
- (int)(remains + textWidth + arrowWidth), (int)baseline);
- }
-
- return language;
- }
-
- private BitmapDrawable getSpaceDrawable(Locale locale, boolean isAutoCorrection) {
- final Integer hashCode = Arrays.hashCode(
- new Object[] { locale, isAutoCorrection, mSpacebarTextFadeFactor });
- final SoftReference<BitmapDrawable> ref = mSpaceDrawableCache.get(hashCode);
- BitmapDrawable drawable = (ref == null) ? null : ref.get();
- if (drawable == null) {
- drawable = new BitmapDrawable(mRes, drawSpacebar(
- locale, isAutoCorrection, mSpacebarTextFadeFactor));
- mSpaceDrawableCache.put(hashCode, new SoftReference<BitmapDrawable>(drawable));
- }
- return drawable;
- }
-
- private Bitmap drawSpacebar(Locale inputLocale, boolean isAutoCorrection,
- float textFadeFactor) {
- final int width = mSpaceKey.mWidth;
- final int height = mSpaceIcon != null ? mSpaceIcon.getIntrinsicHeight() : mSpaceKey.mHeight;
- final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(buffer);
- final Resources res = mRes;
- canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
-
- // If application locales are explicitly selected.
- if (inputLocale != null) {
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
- paint.setTextAlign(Align.CENTER);
-
- final String textSizeOfLanguageOnSpacebar = res.getString(
- R.string.config_text_size_of_language_on_spacebar,
- SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR);
- final int textStyle;
- final int defaultTextSize;
- if (MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR.equals(textSizeOfLanguageOnSpacebar)) {
- textStyle = android.R.style.TextAppearance_Medium;
- defaultTextSize = 18;
- } else {
- textStyle = android.R.style.TextAppearance_Small;
- defaultTextSize = 14;
- }
-
- final String language = layoutSpacebar(paint, inputLocale, mSpaceIcon,
- mSpacebarArrowLeftIcon, mSpacebarArrowRightIcon, width, height,
- getTextSizeFromTheme(mTheme, textStyle, defaultTextSize));
-
- // Draw language text with shadow
- // In case there is no space icon, we will place the language text at the center of
- // spacebar.
- final float descent = paint.descent();
- final float textHeight = -paint.ascent() + descent;
- final float baseline = (mSpaceIcon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
- : height / 2 + textHeight / 2;
- paint.setColor(getSpacebarTextColor(mSpacebarTextShadowColor, textFadeFactor));
- canvas.drawText(language, width / 2, baseline - descent - 1, paint);
- paint.setColor(getSpacebarTextColor(mSpacebarTextColor, textFadeFactor));
- canvas.drawText(language, width / 2, baseline - descent, paint);
-
- // Put arrows that are already laid out on either side of the text
- // Because language switch is disabled on phone and number layouts, hide arrows.
- // TODO: Sort out how to enable language switch on these layouts.
- if (mSpacebarArrowLeftIcon != null && mSpacebarArrowRightIcon != null
- && mSubtypeSwitcher.useSpacebarLanguageSwitcher()
- && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1
- && !(isPhoneKeyboard() || isNumberKeyboard())) {
- mSpacebarArrowLeftIcon.setColorFilter(getSpacebarDrawableFilter(textFadeFactor));
- mSpacebarArrowRightIcon.setColorFilter(getSpacebarDrawableFilter(textFadeFactor));
- mSpacebarArrowLeftIcon.draw(canvas);
- mSpacebarArrowRightIcon.draw(canvas);
- }
- }
-
- // Draw the spacebar icon at the bottom
- if (isAutoCorrection) {
- final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
- final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
- int x = (width - iconWidth) / 2;
- int y = height - iconHeight;
- mAutoCorrectionSpacebarLedIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
- mAutoCorrectionSpacebarLedIcon.draw(canvas);
- } else if (mSpaceIcon != null) {
- final int iconWidth = mSpaceIcon.getIntrinsicWidth();
- final int iconHeight = mSpaceIcon.getIntrinsicHeight();
- int x = (width - iconWidth) / 2;
- int y = height - iconHeight;
- mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
- mSpaceIcon.draw(canvas);
- }
- return buffer;
- }
-
- public void setSpacebarSlidingLanguageSwitchDiff(int diff) {
- mSpacebarSlidingLanguageSwitchDiff = diff;
- }
-
- public void updateSpacebarPreviewIcon(int diff) {
- if (mSpacebarSlidingLanguageSwitchDiff == diff)
- return;
- mSpacebarSlidingLanguageSwitchDiff = diff;
- if (mSlidingLocaleIcon == null)
- return;
- mSlidingLocaleIcon.setDiff(diff);
- if (Math.abs(diff) == Integer.MAX_VALUE) {
- mSpaceKey.setPreviewIcon(mSpacePreviewIcon);
- } else {
- mSpaceKey.setPreviewIcon(mSlidingLocaleIcon);
- }
- mSpaceKey.getPreviewIcon().invalidateSelf();
- }
-
- public boolean shouldTriggerSpacebarSlidingLanguageSwitch(int diff) {
- // On phone and number layouts, sliding language switch is disabled.
- // TODO: Sort out how to enable language switch on these layouts.
- if (isPhoneKeyboard() || isNumberKeyboard())
- return false;
- return Math.abs(diff) > mSpacebarLanguageSwitchThreshold;
- }
-
- /**
- * Return true if spacebar needs showing preview even when "popup on keypress" is off.
- * @param keyIndex index of the pressing key
- * @return true if spacebar needs showing preview
- */
- @Override
- public boolean needSpacebarPreview(int keyIndex) {
- // This method is called when "popup on keypress" is off.
- if (!mSubtypeSwitcher.useSpacebarLanguageSwitcher())
- return false;
- // Dismiss key preview.
- if (keyIndex == KeyDetector.NOT_A_KEY)
- return true;
- // Key is not a spacebar.
- if (keyIndex != mSpaceKeyIndex)
- return false;
- // The language switcher will be displayed only when the dragging distance is greater
- // than the threshold.
- return shouldTriggerSpacebarSlidingLanguageSwitch(mSpacebarSlidingLanguageSwitchDiff);
- }
-
- public int getLanguageChangeDirection() {
- if (mSpaceKey == null || mSubtypeSwitcher.getEnabledKeyboardLocaleCount() <= 1
- || Math.abs(mSpacebarSlidingLanguageSwitchDiff)
- < getMostCommonKeyWidth() * SPACEBAR_DRAG_WIDTH) {
- return 0; // No change
- }
- return mSpacebarSlidingLanguageSwitchDiff > 0 ? 1 : -1;
- }
-
- @Override
- public int[] getNearestKeys(int x, int y) {
- // Avoid dead pixels at edges of the keyboard
- return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)),
- Math.max(0, Math.min(y, getHeight() - 1)));
- }
-
- public static int getTextSizeFromTheme(Theme theme, int style, int defValue) {
- TypedArray array = theme.obtainStyledAttributes(
- style, new int[] { android.R.attr.textSize });
- int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue);
- return textSize;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
index 901df6ab7..383298de9 100644
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
@@ -1,223 +1,806 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2011 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
+ * 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
+ * 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.
+ * 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.keyboard;
+import android.animation.AnimatorInflater;
+import android.animation.ObjectAnimator;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Message;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.PopupWindow;
-import com.android.inputmethod.deprecated.VoiceProxy;
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
+import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
+import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.SubtypeLocale;
import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
+import com.android.inputmethod.latin.define.ProductionFlag;
-// TODO: We should remove this class
-public class LatinKeyboardView extends KeyboardView {
+import java.util.Locale;
+import java.util.WeakHashMap;
+
+/**
+ * A view that is responsible for detecting key presses and touch movements.
+ *
+ * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
+ * @attr ref R.styleable#KeyboardView_verticalCorrection
+ * @attr ref R.styleable#KeyboardView_popupLayout
+ */
+public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
+ SuddenJumpingTouchEventHandler.ProcessMotionEvent {
private static final String TAG = LatinKeyboardView.class.getSimpleName();
- private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
- /** Whether we've started dropping move events because we found a big jump */
- private boolean mDroppingEvents;
- /**
- * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
- * occured
- */
- private boolean mDisableDisambiguation;
- /** The distance threshold at which we start treating the touch session as a multi-touch */
- private int mJumpThresholdSquare = Integer.MAX_VALUE;
- /** The y coordinate of the last row */
- private int mLastRowY;
- private int mLastX;
- private int mLastY;
+ // TODO: Kill process when the usability study mode was changed.
+ private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
+
+ /** Listener for {@link KeyboardActionListener}. */
+ private KeyboardActionListener mKeyboardActionListener;
+
+ /* Space key and its icons */
+ private Key mSpaceKey;
+ private Drawable mSpaceIcon;
+ // Stuff to draw language name on spacebar.
+ private final int mLanguageOnSpacebarFinalAlpha;
+ private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
+ private static final int ALPHA_OPAQUE = 255;
+ private boolean mNeedsToDisplayLanguage;
+ private boolean mHasMultipleEnabledIMEsOrSubtypes;
+ private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE;
+ private final float mSpacebarTextRatio;
+ private float mSpacebarTextSize;
+ private final int mSpacebarTextColor;
+ private final int mSpacebarTextShadowColor;
+ // The minimum x-scale to fit the language name on spacebar.
+ private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
+ // Stuff to draw auto correction LED on spacebar.
+ private boolean mAutoCorrectionSpacebarLedOn;
+ private final boolean mAutoCorrectionSpacebarLedEnabled;
+ private final Drawable mAutoCorrectionSpacebarLedIcon;
+ private static final int SPACE_LED_LENGTH_PERCENT = 80;
+
+ // Stuff to draw altCodeWhileTyping keys.
+ private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
+ private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
+ private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE;
+
+ // More keys keyboard
+ private PopupWindow mMoreKeysWindow;
+ private MoreKeysPanel mMoreKeysPanel;
+ private int mMoreKeysPanelPointerTrackerId;
+ private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
+ new WeakHashMap<Key, MoreKeysPanel>();
+ private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
+
+ private final PointerTrackerParams mPointerTrackerParams;
+ private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
+
+ protected KeyDetector mKeyDetector;
+ private boolean mHasDistinctMultitouch;
+ private int mOldPointerCount = 1;
+ private Key mOldKey;
+
+ private final KeyTimerHandler mKeyTimerHandler;
+
+ private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
+ implements TimerProxy {
+ private static final int MSG_REPEAT_KEY = 1;
+ private static final int MSG_LONGPRESS_KEY = 2;
+ private static final int MSG_DOUBLE_TAP = 3;
+ private static final int MSG_TYPING_STATE_EXPIRED = 4;
+
+ private final KeyTimerParams mParams;
+ private boolean mInKeyRepeat;
+
+ public KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params) {
+ super(outerInstance);
+ mParams = params;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final LatinKeyboardView keyboardView = getOuterInstance();
+ final PointerTracker tracker = (PointerTracker) msg.obj;
+ switch (msg.what) {
+ case MSG_REPEAT_KEY:
+ tracker.onRegisterKey(tracker.getKey());
+ startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
+ break;
+ case MSG_LONGPRESS_KEY:
+ if (tracker != null) {
+ keyboardView.openMoreKeysKeyboardIfRequired(tracker.getKey(), tracker);
+ } else {
+ KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
+ }
+ break;
+ case MSG_TYPING_STATE_EXPIRED:
+ cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
+ keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
+ break;
+ }
+ }
+
+ private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
+ sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
+ }
+
+ @Override
+ public void startKeyRepeatTimer(PointerTracker tracker) {
+ mInKeyRepeat = true;
+ startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
+ }
+
+ public void cancelKeyRepeatTimer() {
+ mInKeyRepeat = false;
+ removeMessages(MSG_REPEAT_KEY);
+ }
+
+ public boolean isInKeyRepeat() {
+ return mInKeyRepeat;
+ }
+
+ @Override
+ public void startLongPressTimer(int code) {
+ cancelLongPressTimer();
+ final int delay;
+ switch (code) {
+ case Keyboard.CODE_SHIFT:
+ delay = mParams.mLongPressShiftKeyTimeout;
+ break;
+ default:
+ delay = 0;
+ break;
+ }
+ if (delay > 0) {
+ sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay);
+ }
+ }
+
+ @Override
+ public void startLongPressTimer(PointerTracker tracker) {
+ cancelLongPressTimer();
+ if (tracker == null) {
+ return;
+ }
+ final Key key = tracker.getKey();
+ final int delay;
+ switch (key.mCode) {
+ case Keyboard.CODE_SHIFT:
+ delay = mParams.mLongPressShiftKeyTimeout;
+ break;
+ default:
+ if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
+ // We use longer timeout for sliding finger input started from the symbols
+ // mode key.
+ delay = mParams.mLongPressKeyTimeout * 3;
+ } else {
+ delay = mParams.mLongPressKeyTimeout;
+ }
+ break;
+ }
+ if (delay > 0) {
+ sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
+ }
+ }
+
+ @Override
+ public void cancelLongPressTimer() {
+ removeMessages(MSG_LONGPRESS_KEY);
+ }
+
+ public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
+ final ObjectAnimator animatorToStart) {
+ float startFraction = 0.0f;
+ if (animatorToCancel.isStarted()) {
+ animatorToCancel.cancel();
+ startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
+ }
+ final long startTime = (long)(animatorToStart.getDuration() * startFraction);
+ animatorToStart.start();
+ animatorToStart.setCurrentPlayTime(startTime);
+ }
+
+ @Override
+ public void startTypingStateTimer() {
+ final boolean isTyping = isTypingState();
+ removeMessages(MSG_TYPING_STATE_EXPIRED);
+ sendMessageDelayed(
+ obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
+ if (isTyping) {
+ return;
+ }
+ final LatinKeyboardView keyboardView = getOuterInstance();
+ cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
+ keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
+ }
+
+ @Override
+ public boolean isTypingState() {
+ return hasMessages(MSG_TYPING_STATE_EXPIRED);
+ }
+
+ @Override
+ public void startDoubleTapTimer() {
+ sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP),
+ ViewConfiguration.getDoubleTapTimeout());
+ }
+
+ @Override
+ public void cancelDoubleTapTimer() {
+ removeMessages(MSG_DOUBLE_TAP);
+ }
+
+ @Override
+ public boolean isInDoubleTapTimeout() {
+ return hasMessages(MSG_DOUBLE_TAP);
+ }
+
+ @Override
+ public void cancelKeyTimers() {
+ cancelKeyRepeatTimer();
+ cancelLongPressTimer();
+ }
+
+ public void cancelAllMessages() {
+ cancelKeyTimers();
+ }
+ }
+
+ public static class PointerTrackerParams {
+ public final boolean mSlidingKeyInputEnabled;
+ public final int mTouchNoiseThresholdTime;
+ public final float mTouchNoiseThresholdDistance;
+
+ public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
+
+ private PointerTrackerParams() {
+ mSlidingKeyInputEnabled = false;
+ mTouchNoiseThresholdTime =0;
+ mTouchNoiseThresholdDistance = 0;
+ }
+
+ public PointerTrackerParams(TypedArray latinKeyboardViewAttr) {
+ mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
+ R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
+ mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
+ mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
+ R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
+ }
+ }
+
+ static class KeyTimerParams {
+ public final int mKeyRepeatStartTimeout;
+ public final int mKeyRepeatInterval;
+ public final int mLongPressKeyTimeout;
+ public final int mLongPressShiftKeyTimeout;
+ public final int mIgnoreAltCodeKeyTimeout;
+
+ public KeyTimerParams(TypedArray latinKeyboardViewAttr) {
+ mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
+ mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
+ mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
+ mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
+ mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
+ R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
+ }
+ }
public LatinKeyboardView(Context context, AttributeSet attrs) {
- super(context, attrs);
+ this(context, attrs, R.attr.latinKeyboardViewStyle);
}
public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+
+ mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
+
+ mHasDistinctMultitouch = context.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
+ final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
+ Utils.getDeviceOverrideValue(context.getResources(),
+ R.array.phantom_sudden_move_event_device_list, "false"));
+ PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
+ mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
+ R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
+ mAutoCorrectionSpacebarLedIcon = a.getDrawable(
+ R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
+ mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
+ 1000, 1000, 1) / 1000.0f;
+ mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
+ mSpacebarTextShadowColor = a.getColor(
+ R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
+ mLanguageOnSpacebarFinalAlpha = a.getInt(
+ R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE);
+ final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
+ R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
+ final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
+ R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
+ final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
+ R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
+
+ final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
+ mPointerTrackerParams = new PointerTrackerParams(a);
+
+ final float keyHysteresisDistance = a.getDimension(
+ R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
+ mKeyDetector = new KeyDetector(keyHysteresisDistance);
+ mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
+ mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
+ R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+ a.recycle();
+
+ PointerTracker.setParameters(mPointerTrackerParams);
+
+ mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
+ languageOnSpacebarFadeoutAnimatorResId, this);
+ mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
+ altCodeKeyWhileTypingFadeoutAnimatorResId, this);
+ mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
+ altCodeKeyWhileTypingFadeinAnimatorResId, this);
}
- @Override
- public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
- LatinKeyboard latinKeyboard = getLatinKeyboard();
- if (latinKeyboard != null
- && (latinKeyboard.isPhoneKeyboard() || latinKeyboard.isNumberKeyboard())) {
- // Phone and number keyboard never shows popup preview (except language switch).
- super.setKeyPreviewPopupEnabled(false, delay);
- } else {
- super.setKeyPreviewPopupEnabled(previewEnabled, delay);
+ private ObjectAnimator loadObjectAnimator(int resId, Object target) {
+ if (resId == 0) return null;
+ final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
+ getContext(), resId);
+ if (animator != null) {
+ animator.setTarget(target);
}
+ return animator;
+ }
+
+ // Getter/setter methods for {@link ObjectAnimator}.
+ public int getLanguageOnSpacebarAnimAlpha() {
+ return mLanguageOnSpacebarAnimAlpha;
+ }
+
+ public void setLanguageOnSpacebarAnimAlpha(int alpha) {
+ mLanguageOnSpacebarAnimAlpha = alpha;
+ invalidateKey(mSpaceKey);
+ }
+
+ public int getAltCodeKeyWhileTypingAnimAlpha() {
+ return mAltCodeKeyWhileTypingAnimAlpha;
}
+ public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
+ mAltCodeKeyWhileTypingAnimAlpha = alpha;
+ updateAltCodeKeyWhileTyping();
+ }
+
+ public void setKeyboardActionListener(KeyboardActionListener listener) {
+ mKeyboardActionListener = listener;
+ PointerTracker.setKeyboardActionListener(listener);
+ }
+
+ /**
+ * Returns the {@link KeyboardActionListener} object.
+ * @return the listener attached to this keyboard
+ */
@Override
- public void setKeyboard(Keyboard newKeyboard) {
- super.setKeyboard(newKeyboard);
- // One-seventh of the keyboard width seems like a reasonable threshold
- mJumpThresholdSquare = newKeyboard.getMinWidth() / 7;
- mJumpThresholdSquare *= mJumpThresholdSquare;
- // Assuming there are 4 rows, this is the coordinate of the last row
- mLastRowY = (newKeyboard.getHeight() * 3) / 4;
- }
-
- private LatinKeyboard getLatinKeyboard() {
- Keyboard keyboard = getKeyboard();
- if (keyboard instanceof LatinKeyboard) {
- return (LatinKeyboard)keyboard;
- } else {
- return null;
- }
+ public KeyboardActionListener getKeyboardActionListener() {
+ return mKeyboardActionListener;
}
- public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboard oldKeyboard) {
- final LatinKeyboard currentKeyboard = getLatinKeyboard();
- // We should not set text fade factor to the keyboard which does not display the language on
- // its spacebar.
- if (currentKeyboard != null && currentKeyboard == oldKeyboard)
- currentKeyboard.setSpacebarTextFadeFactor(fadeFactor, this);
+ @Override
+ public KeyDetector getKeyDetector() {
+ return mKeyDetector;
}
@Override
- protected boolean onLongPress(Key key, PointerTracker tracker) {
- int primaryCode = key.mCode;
- if (primaryCode == Keyboard.CODE_SETTINGS) {
- return invokeOnKey(Keyboard.CODE_SETTINGS_LONGPRESS);
- } else if (primaryCode == '0' && getLatinKeyboard().isPhoneKeyboard()) {
- // Long pressing on 0 in phone number keypad gives you a '+'.
- return invokeOnKey('+');
- } else {
- return super.onLongPress(key, tracker);
- }
+ public DrawingProxy getDrawingProxy() {
+ return this;
}
- private boolean invokeOnKey(int primaryCode) {
- getOnKeyboardActionListener().onCodeInput(primaryCode, null,
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
- return true;
+ @Override
+ public TimerProxy getTimerProxy() {
+ return mKeyTimerHandler;
}
+ /**
+ * Attaches a keyboard to this view. The keyboard can be switched at any time and the
+ * view will re-layout itself to accommodate the keyboard.
+ * @see Keyboard
+ * @see #getKeyboard()
+ * @param keyboard the keyboard to display in this view
+ */
@Override
- protected CharSequence adjustCase(CharSequence label) {
- LatinKeyboard keyboard = getLatinKeyboard();
- if (keyboard.isAlphaKeyboard()
- && keyboard.isShiftedOrShiftLocked()
- && !TextUtils.isEmpty(label) && label.length() < 3
- && Character.isLowerCase(label.charAt(0))) {
- return label.toString().toUpperCase(keyboard.mId.mLocale);
+ public void setKeyboard(Keyboard keyboard) {
+ // Remove any pending messages, except dismissing preview
+ mKeyTimerHandler.cancelKeyTimers();
+ super.setKeyboard(keyboard);
+ mKeyDetector.setKeyboard(
+ keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
+ PointerTracker.setKeyDetector(mKeyDetector);
+ mTouchScreenRegulator.setKeyboard(keyboard);
+ mMoreKeysPanelCache.clear();
+
+ mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
+ mSpaceIcon = (mSpaceKey != null)
+ ? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null;
+ final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
+ mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
}
- return label;
+
+ // This always needs to be set since the accessibility state can
+ // potentially change without the keyboard being set again.
+ AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
}
/**
- * This function checks to see if we need to handle any sudden jumps in the pointer location
- * that could be due to a multi-touch being treated as a move by the firmware or hardware.
- * Once a sudden jump is detected, all subsequent move events are discarded
- * until an UP is received.<P>
- * When a sudden jump is detected, an UP event is simulated at the last position and when
- * the sudden moves subside, a DOWN event is simulated for the second key.
- * @param me the motion event
- * @return true if the event was consumed, so that it doesn't continue to be handled by
- * KeyboardView.
+ * Returns whether the device has distinct multi-touch panel.
+ * @return true if the device has distinct multi-touch panel.
*/
- private boolean handleSuddenJump(MotionEvent me) {
- // If device has distinct multi touch panel, there is no need to check sudden jump.
- if (hasDistinctMultitouch())
+ public boolean hasDistinctMultitouch() {
+ return mHasDistinctMultitouch;
+ }
+
+ public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
+ mHasDistinctMultitouch = hasDistinctMultitouch;
+ }
+
+ /**
+ * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
+ * codes for adjacent keys. When disabled, only the primary key code will be
+ * reported.
+ * @param enabled whether or not the proximity correction is enabled
+ */
+ public void setProximityCorrectionEnabled(boolean enabled) {
+ mKeyDetector.setProximityCorrectionEnabled(enabled);
+ }
+
+ /**
+ * Returns true if proximity correction is enabled.
+ */
+ public boolean isProximityCorrectionEnabled() {
+ return mKeyDetector.isProximityCorrectionEnabled();
+ }
+
+ @Override
+ public void cancelAllMessages() {
+ mKeyTimerHandler.cancelAllMessages();
+ super.cancelAllMessages();
+ }
+
+ private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
+ // Check if we have a popup layout specified first.
+ if (mMoreKeysLayout == 0) {
return false;
- final int action = me.getAction();
- final int x = (int) me.getX();
- final int y = (int) me.getY();
- boolean result = false;
-
- // Real multi-touch event? Stop looking for sudden jumps
- if (me.getPointerCount() > 1) {
- mDisableDisambiguation = true;
- }
- if (mDisableDisambiguation) {
- // If UP, reset the multi-touch flag
- if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
+ }
+
+ // Check if we are already displaying popup panel.
+ if (mMoreKeysPanel != null)
+ return false;
+ if (parentKey == null)
return false;
+ return onLongPress(parentKey, tracker);
+ }
+
+ // This default implementation returns a more keys panel.
+ protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
+ if (parentKey.mMoreKeys == null)
+ return null;
+
+ final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
+ if (container == null)
+ throw new NullPointerException();
+
+ final MoreKeysKeyboardView moreKeysKeyboardView =
+ (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
+ final Keyboard moreKeysKeyboard = new MoreKeysKeyboard.Builder(container, parentKey, this)
+ .build();
+ moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
+ container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ return moreKeysKeyboardView;
+ }
+
+ /**
+ * Called when a key is long pressed. By default this will open more keys keyboard associated
+ * with this key.
+ * @param parentKey the key that was long pressed
+ * @param tracker the pointer tracker which pressed the parent key
+ * @return true if the long press is handled, false otherwise. Subclasses should call the
+ * method on the base class if the subclass doesn't wish to handle the call.
+ */
+ protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinKeyboardView_onLongPress();
+ }
+ final int primaryCode = parentKey.mCode;
+ if (parentKey.hasEmbeddedMoreKey()) {
+ final int embeddedCode = parentKey.mMoreKeys[0].mCode;
+ tracker.onLongPressed();
+ invokeCodeInput(embeddedCode);
+ invokeReleaseKey(primaryCode);
+ KeyboardSwitcher.getInstance().hapticAndAudioFeedback(primaryCode);
+ return true;
+ }
+ if (primaryCode == Keyboard.CODE_SPACE || primaryCode == Keyboard.CODE_LANGUAGE_SWITCH) {
+ // Long pressing the space key invokes IME switcher dialog.
+ if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
+ tracker.onLongPressed();
+ invokeReleaseKey(primaryCode);
+ return true;
+ }
}
+ return openMoreKeysPanel(parentKey, tracker);
+ }
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- // Reset the "session"
- mDroppingEvents = false;
- mDisableDisambiguation = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Is this a big jump?
- final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
- // Check the distance and also if the move is not entirely within the bottom row
- // If it's only in the bottom row, it might be an intentional slide gesture
- // for language switching
- if (distanceSquare > mJumpThresholdSquare
- && (mLastY < mLastRowY || y < mLastRowY)) {
- // If we're not yet dropping events, start dropping and send an UP event
- if (!mDroppingEvents) {
- mDroppingEvents = true;
- // Send an up event
- MotionEvent translated = MotionEvent.obtain(
- me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_UP,
- mLastX, mLastY, me.getMetaState());
- super.onTouchEvent(translated);
- translated.recycle();
- }
- result = true;
- } else if (mDroppingEvents) {
- // If moves are small and we're already dropping events, continue dropping
- result = true;
- }
- break;
- case MotionEvent.ACTION_UP:
- if (mDroppingEvents) {
- // Send a down event first, as we dropped a bunch of sudden jumps and assume that
- // the user is releasing the touch on the second key.
- MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_DOWN,
- x, y, me.getMetaState());
- super.onTouchEvent(translated);
- translated.recycle();
- mDroppingEvents = false;
- // Let the up event get processed as well, result = false
- }
- break;
- }
- // Track the previous coordinate
- mLastX = x;
- mLastY = y;
- return result;
+ private boolean invokeCustomRequest(int code) {
+ return mKeyboardActionListener.onCustomRequest(code);
+ }
+
+ private void invokeCodeInput(int primaryCode) {
+ mKeyboardActionListener.onCodeInput(primaryCode,
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
+ KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+ }
+
+ private void invokeReleaseKey(int primaryCode) {
+ mKeyboardActionListener.onReleaseKey(primaryCode, false);
+ }
+
+ private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
+ MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
+ if (moreKeysPanel == null) {
+ moreKeysPanel = onCreateMoreKeysPanel(parentKey);
+ if (moreKeysPanel == null)
+ return false;
+ mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
+ }
+ if (mMoreKeysWindow == null) {
+ mMoreKeysWindow = new PopupWindow(getContext());
+ mMoreKeysWindow.setBackgroundDrawable(null);
+ mMoreKeysWindow.setAnimationStyle(R.style.MoreKeysKeyboardAnimation);
+ }
+ mMoreKeysPanel = moreKeysPanel;
+ mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
+
+ final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview();
+ // The more keys keyboard is usually horizontally aligned with the center of the parent key.
+ // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
+ // keys keyboard is placed at the touch point of the parent key.
+ final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
+ ? tracker.getLastX()
+ : parentKey.mX + parentKey.mWidth / 2;
+ // The more keys keyboard is usually vertically aligned with the top edge of the parent key
+ // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
+ // aligned with the bottom edge of the visible part of the key preview.
+ final int pointY = parentKey.mY + (keyPreviewEnabled
+ ? mKeyPreviewDrawParams.mPreviewVisibleOffset
+ : -parentKey.mVerticalGap);
+ moreKeysPanel.showMoreKeysPanel(
+ this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
+ final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
+ final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
+ tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
+ dimEntireKeyboard(true);
+ return true;
+ }
+
+ public boolean isInSlidingKeyInput() {
+ if (mMoreKeysPanel != null) {
+ return true;
+ } else {
+ return PointerTracker.isAnyInSlidingKeyInput();
+ }
+ }
+
+ public int getPointerCount() {
+ return mOldPointerCount;
}
@Override
public boolean onTouchEvent(MotionEvent me) {
- if (getLatinKeyboard() == null) return true;
+ if (getKeyboard() == null) {
+ return false;
+ }
+ return mTouchScreenRegulator.onTouchEvent(me);
+ }
+
+ @Override
+ public boolean processMotionEvent(MotionEvent me) {
+ final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
+ final int action = me.getActionMasked();
+ final int pointerCount = me.getPointerCount();
+ final int oldPointerCount = mOldPointerCount;
+ mOldPointerCount = pointerCount;
- // If there was a sudden jump, return without processing the actual motion event.
- if (handleSuddenJump(me)) {
- if (DEBUG_MODE)
- Log.w(TAG, "onTouchEvent: ignore sudden jump " + me);
+ // TODO: cleanup this code into a multi-touch to single-touch event converter class?
+ // If the device does not have distinct multi-touch support panel, ignore all multi-touch
+ // events except a transition from/to single-touch.
+ if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
return true;
}
- return super.onTouchEvent(me);
+ final long eventTime = me.getEventTime();
+ final int index = me.getActionIndex();
+ final int id = me.getPointerId(index);
+ final int x, y;
+ if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
+ x = mMoreKeysPanel.translateX((int)me.getX(index));
+ y = mMoreKeysPanel.translateY((int)me.getY(index));
+ } else {
+ x = (int)me.getX(index);
+ y = (int)me.getY(index);
+ }
+ if (ENABLE_USABILITY_STUDY_LOG) {
+ final String eventTag;
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ eventTag = "[Up]";
+ break;
+ case MotionEvent.ACTION_DOWN:
+ eventTag = "[Down]";
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ eventTag = "[PointerUp]";
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ eventTag = "[PointerDown]";
+ break;
+ case MotionEvent.ACTION_MOVE: // Skip this as being logged below
+ eventTag = "";
+ break;
+ default:
+ eventTag = "[Action" + action + "]";
+ break;
+ }
+ if (!TextUtils.isEmpty(eventTag)) {
+ final float size = me.getSize(index);
+ final float pressure = me.getPressure(index);
+ UsabilityStudyLogUtils.getInstance().write(
+ eventTag + eventTime + "," + id + "," + x + "," + y + ","
+ + size + "," + pressure);
+ }
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
+ x, y);
+ }
+
+ if (mKeyTimerHandler.isInKeyRepeat()) {
+ final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
+ // Key repeating timer will be canceled if 2 or more keys are in action, and current
+ // event (UP or DOWN) is non-modifier key.
+ if (pointerCount > 1 && !tracker.isModifier()) {
+ mKeyTimerHandler.cancelKeyRepeatTimer();
+ }
+ // Up event will pass through.
+ }
+
+ // TODO: cleanup this code into a multi-touch to single-touch event converter class?
+ // Translate mutli-touch event to single-touch events on the device that has no distinct
+ // multi-touch panel.
+ if (nonDistinctMultitouch) {
+ // Use only main (id=0) pointer tracker.
+ final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
+ if (pointerCount == 1 && oldPointerCount == 2) {
+ // Multi-touch to single touch transition.
+ // Send a down event for the latest pointer if the key is different from the
+ // previous key.
+ final Key newKey = tracker.getKeyOn(x, y);
+ if (mOldKey != newKey) {
+ tracker.onDownEvent(x, y, eventTime, this);
+ if (action == MotionEvent.ACTION_UP)
+ tracker.onUpEvent(x, y, eventTime);
+ }
+ } else if (pointerCount == 2 && oldPointerCount == 1) {
+ // Single-touch to multi-touch transition.
+ // Send an up event for the last pointer.
+ final int lastX = tracker.getLastX();
+ final int lastY = tracker.getLastY();
+ mOldKey = tracker.getKeyOn(lastX, lastY);
+ tracker.onUpEvent(lastX, lastY, eventTime);
+ } else if (pointerCount == 1 && oldPointerCount == 1) {
+ tracker.processMotionEvent(action, x, y, eventTime, this);
+ } else {
+ Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
+ + " (old " + oldPointerCount + ")");
+ }
+ return true;
+ }
+
+ if (action == MotionEvent.ACTION_MOVE) {
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = me.getPointerId(i);
+ final PointerTracker tracker = PointerTracker.getPointerTracker(
+ pointerId, this);
+ final int px, py;
+ if (mMoreKeysPanel != null
+ && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
+ px = mMoreKeysPanel.translateX((int)me.getX(i));
+ py = mMoreKeysPanel.translateY((int)me.getY(i));
+ } else {
+ px = (int)me.getX(i);
+ py = (int)me.getY(i);
+ }
+ tracker.onMoveEvent(px, py, eventTime);
+ if (ENABLE_USABILITY_STUDY_LOG) {
+ final float pointerSize = me.getSize(i);
+ final float pointerPressure = me.getPressure(i);
+ UsabilityStudyLogUtils.getInstance().write("[Move]" + eventTime + ","
+ + pointerId + "," + px + "," + py + ","
+ + pointerSize + "," + pointerPressure);
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
+ i, pointerId, px, py);
+ }
+ }
+ } else {
+ final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
+ tracker.processMotionEvent(action, x, y, eventTime, this);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void closing() {
+ super.closing();
+ dismissMoreKeysPanel();
+ mMoreKeysPanelCache.clear();
+ }
+
+ @Override
+ public boolean dismissMoreKeysPanel() {
+ if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
+ mMoreKeysWindow.dismiss();
+ mMoreKeysPanel = null;
+ mMoreKeysPanelPointerTrackerId = -1;
+ dimEntireKeyboard(false);
+ return true;
+ }
+ return false;
}
@Override
@@ -229,14 +812,206 @@ public class LatinKeyboardView extends KeyboardView {
super.draw(c);
tryGC = false;
} catch (OutOfMemoryError e) {
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
+ tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
}
}
}
+ /**
+ * Receives hover events from the input framework.
+ *
+ * @param event The motion event to be dispatched.
+ * @return {@code true} if the event was handled by the view, {@code false}
+ * otherwise
+ */
@Override
- protected void onAttachedToWindow() {
- // Token is available from here.
- VoiceProxy.getInstance().onAttachedToWindow();
+ public boolean dispatchHoverEvent(MotionEvent event) {
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
+ return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
+ }
+
+ // Reflection doesn't support calling superclass methods.
+ return false;
+ }
+
+ public void updateShortcutKey(boolean available) {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard == null) return;
+ final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
+ if (shortcutKey == null) return;
+ shortcutKey.setEnabled(available);
+ invalidateKey(shortcutKey);
+ }
+
+ private void updateAltCodeKeyWhileTyping() {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard == null) return;
+ for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
+ invalidateKey(key);
+ }
+ }
+
+ public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
+ boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
+ mNeedsToDisplayLanguage = needsToDisplayLanguage;
+ mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
+ final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
+ if (animator == null) {
+ mNeedsToDisplayLanguage = false;
+ } else {
+ if (subtypeChanged && needsToDisplayLanguage) {
+ setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE);
+ if (animator.isStarted()) {
+ animator.cancel();
+ }
+ animator.start();
+ } else {
+ if (!animator.isStarted()) {
+ mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
+ }
+ }
+ }
+ invalidateKey(mSpaceKey);
+ }
+
+ public void updateAutoCorrectionState(boolean isAutoCorrection) {
+ if (!mAutoCorrectionSpacebarLedEnabled) return;
+ mAutoCorrectionSpacebarLedOn = isAutoCorrection;
+ invalidateKey(mSpaceKey);
+ }
+
+ @Override
+ protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+ if (key.altCodeWhileTyping() && key.isEnabled()) {
+ params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
+ }
+ if (key.mCode == Keyboard.CODE_SPACE) {
+ drawSpacebar(key, canvas, paint);
+ // Whether space key needs to show the "..." popup hint for special purposes
+ if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
+ drawKeyPopupHint(key, canvas, paint, params);
+ }
+ } else if (key.mCode == Keyboard.CODE_LANGUAGE_SWITCH) {
+ super.onDrawKeyTopVisuals(key, canvas, paint, params);
+ drawKeyPopupHint(key, canvas, paint, params);
+ } else {
+ super.onDrawKeyTopVisuals(key, canvas, paint, params);
+ }
+ }
+
+ private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
+ paint.setTextScaleX(1.0f);
+ final float textWidth = getLabelWidth(text, paint);
+ if (textWidth < width) return true;
+
+ final float scaleX = width / textWidth;
+ if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) return false;
+
+ paint.setTextScaleX(scaleX);
+ return getLabelWidth(text, paint) < width;
+ }
+
+ // Layout language name on spacebar.
+ private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
+ final int width) {
+ // Choose appropriate language name to fit into the width.
+ String text = getFullDisplayName(subtype, getResources());
+ if (fitsTextIntoWidth(width, text, paint)) {
+ return text;
+ }
+
+ text = getMiddleDisplayName(subtype);
+ if (fitsTextIntoWidth(width, text, paint)) {
+ return text;
+ }
+
+ text = getShortDisplayName(subtype);
+ if (fitsTextIntoWidth(width, text, paint)) {
+ return text;
+ }
+
+ return "";
+ }
+
+ private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
+ final int width = key.mWidth;
+ final int height = key.mHeight;
+
+ // If input language are explicitly selected.
+ if (mNeedsToDisplayLanguage) {
+ paint.setTextAlign(Align.CENTER);
+ paint.setTypeface(Typeface.DEFAULT);
+ paint.setTextSize(mSpacebarTextSize);
+ final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
+ final String language = layoutLanguageOnSpacebar(paint, subtype, width);
+ // Draw language text with shadow
+ final float descent = paint.descent();
+ final float textHeight = -paint.ascent() + descent;
+ final float baseline = height / 2 + textHeight / 2;
+ paint.setColor(mSpacebarTextShadowColor);
+ paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
+ canvas.drawText(language, width / 2, baseline - descent - 1, paint);
+ paint.setColor(mSpacebarTextColor);
+ paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
+ canvas.drawText(language, width / 2, baseline - descent, paint);
+ }
+
+ // Draw the spacebar icon at the bottom
+ if (mAutoCorrectionSpacebarLedOn) {
+ final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
+ final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
+ int x = (width - iconWidth) / 2;
+ int y = height - iconHeight;
+ drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
+ } else if (mSpaceIcon != null) {
+ final int iconWidth = mSpaceIcon.getIntrinsicWidth();
+ final int iconHeight = mSpaceIcon.getIntrinsicHeight();
+ int x = (width - iconWidth) / 2;
+ int y = height - iconHeight;
+ drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
+ }
+ }
+
+ // InputMethodSubtype's display name for spacebar text in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | Short Middle Full
+ // ------ ------ - ---- --------- ----------------------
+ // en_US qwerty F En English English (US) exception
+ // en_GB qwerty F En English English (UK) exception
+ // fr azerty F Fr Français Français
+ // fr_CA qwerty F Fr Français Français (Canada)
+ // de qwertz F De Deutsch Deutsch
+ // zz qwerty F QWERTY QWERTY
+ // fr qwertz T Fr Français Français (QWERTZ)
+ // de qwerty T De Deutsch Deutsch (QWERTY)
+ // en_US azerty T En English English (US) (AZERTY)
+ // zz azerty T AZERTY AZERTY
+
+ // Get InputMethodSubtype's full display name in its locale.
+ static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
+ if (SubtypeLocale.isNoLanguage(subtype)) {
+ return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
+ }
+
+ return SubtypeLocale.getSubtypeDisplayName(subtype, res);
+ }
+
+ // Get InputMethodSubtype's short display name in its locale.
+ static String getShortDisplayName(InputMethodSubtype subtype) {
+ if (SubtypeLocale.isNoLanguage(subtype)) {
+ return "";
+ }
+ final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
+ return StringUtils.toTitleCase(locale.getLanguage(), locale);
+ }
+
+ // Get InputMethodSubtype's middle display name in its locale.
+ static String getMiddleDisplayName(InputMethodSubtype subtype) {
+ if (SubtypeLocale.isNoLanguage(subtype)) {
+ return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
+ }
+ final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
+ return StringUtils.toTitleCase(locale.getDisplayLanguage(locale), locale);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
deleted file mode 100644
index 2d6766f2d..000000000
--- a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2011 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.keyboard;
-
-import android.content.Context;
-
-import java.util.List;
-
-public class MiniKeyboard extends Keyboard {
- private int mDefaultKeyCoordX;
-
- public MiniKeyboard(Context context, int xmlLayoutResId, Keyboard parentKeyboard) {
- super(context, xmlLayoutResId, null, parentKeyboard.getMinWidth());
- }
-
- public void setDefaultCoordX(int pos) {
- mDefaultKeyCoordX = pos;
- }
-
- public int getDefaultCoordX() {
- return mDefaultKeyCoordX;
- }
-
- public boolean isOneRowKeyboard() {
- final List<Key> keys = getKeys();
- if (keys.size() == 0) return false;
- final int edgeFlags = keys.get(0).mEdgeFlags;
- // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
- // does not have both top and bottom edge flags on at the same time. On the other hand,
- // the first key of mini keyboard that was created with popupCharacters must have both top
- // and bottom edge flags on.
- // When you want to use one row mini-keyboard from xml file, make sure that the row has
- // both top and bottom edge flags set.
- return (edgeFlags & Keyboard.EDGE_TOP) != 0
- && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
-
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java
deleted file mode 100644
index cc5c3bbfe..000000000
--- a/java/src/com/android/inputmethod/keyboard/MiniKeyboardKeyDetector.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard;
-
-import java.util.List;
-
-public class MiniKeyboardKeyDetector extends KeyDetector {
- private final int mSlideAllowanceSquare;
- private final int mSlideAllowanceSquareTop;
-
- public MiniKeyboardKeyDetector(float slideAllowance) {
- super();
- mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance);
- // Top slide allowance is slightly longer (sqrt(2) times) than other edges.
- mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2;
- }
-
- @Override
- protected int getMaxNearbyKeys() {
- // No nearby key will be returned.
- return 1;
- }
-
- @Override
- public int getKeyIndexAndNearbyCodes(int x, int y, final int[] allCodes) {
- final List<Key> keys = getKeys();
- final int touchX = getTouchX(x);
- final int touchY = getTouchY(y);
-
- int nearestIndex = NOT_A_KEY;
- int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
- final int keyCount = keys.size();
- for (int index = 0; index < keyCount; index++) {
- final int dist = keys.get(index).squaredDistanceToEdge(touchX, touchY);
- if (dist < nearestDist) {
- nearestIndex = index;
- nearestDist = dist;
- }
- }
-
- if (allCodes != null && nearestIndex != NOT_A_KEY)
- allCodes[0] = keys.get(nearestIndex).mCode;
- return nearestIndex;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
new file mode 100644
index 000000000..cd4e3001e
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+public class MoreKeysDetector extends KeyDetector {
+ private final int mSlideAllowanceSquare;
+ private final int mSlideAllowanceSquareTop;
+
+ public MoreKeysDetector(float slideAllowance) {
+ super(/* keyHysteresisDistance */0);
+ mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance);
+ // Top slide allowance is slightly longer (sqrt(2) times) than other edges.
+ mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2;
+ }
+
+ @Override
+ public boolean alwaysAllowsSlidingInput() {
+ return true;
+ }
+
+ @Override
+ public Key detectHitKey(int x, int y) {
+ final int touchX = getTouchX(x);
+ final int touchY = getTouchY(y);
+
+ Key nearestKey = null;
+ int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
+ for (final Key key : getKeyboard().mKeys) {
+ final int dist = key.squaredDistanceToEdge(touchX, touchY);
+ if (dist < nearestDist) {
+ nearestKey = key;
+ nearestDist = dist;
+ }
+ }
+ return nearestKey;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
new file mode 100644
index 000000000..a3741a2d8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
+import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.StringUtils;
+
+public class MoreKeysKeyboard extends Keyboard {
+ private final int mDefaultKeyCoordX;
+
+ MoreKeysKeyboard(Builder.MoreKeysKeyboardParams params) {
+ super(params);
+ mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
+ }
+
+ public int getDefaultCoordX() {
+ return mDefaultKeyCoordX;
+ }
+
+ public static class Builder extends Keyboard.Builder<Builder.MoreKeysKeyboardParams> {
+ private final Key mParentKey;
+ private final Drawable mDivider;
+
+ private static final float LABEL_PADDING_RATIO = 0.2f;
+ private static final float DIVIDER_RATIO = 0.2f;
+
+ public static class MoreKeysKeyboardParams extends Keyboard.Params {
+ public boolean mIsFixedOrder;
+ /* package */int mTopRowAdjustment;
+ public int mNumRows;
+ public int mNumColumns;
+ public int mTopKeys;
+ public int mLeftKeys;
+ public int mRightKeys; // includes default key.
+ public int mDividerWidth;
+ public int mColumnWidth;
+
+ public MoreKeysKeyboardParams() {
+ super();
+ }
+
+ /**
+ * Set keyboard parameters of more keys keyboard.
+ *
+ * @param numKeys number of keys in this more keys keyboard.
+ * @param maxColumns number of maximum columns of this more keys keyboard.
+ * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
+ * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
+ * @param coordXInParent coordinate x of the key preview in parent keyboard.
+ * @param parentKeyboardWidth parent keyboard width in pixel.
+ * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
+ * @param dividerWidth width of divider, zero for no dividers.
+ */
+ public void setParameters(int numKeys, int maxColumns, int keyWidth, int rowHeight,
+ int coordXInParent, int parentKeyboardWidth, boolean isFixedColumnOrder,
+ int dividerWidth) {
+ mIsFixedOrder = isFixedColumnOrder;
+ if (parentKeyboardWidth / keyWidth < maxColumns) {
+ throw new IllegalArgumentException(
+ "Keyboard is too small to hold more keys keyboard: "
+ + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
+ }
+ mDefaultKeyWidth = keyWidth;
+ mDefaultRowHeight = rowHeight;
+
+ final int numRows = (numKeys + maxColumns - 1) / maxColumns;
+ mNumRows = numRows;
+ final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
+ : getOptimizedColumns(numKeys, maxColumns);
+ mNumColumns = numColumns;
+ final int topKeys = numKeys % numColumns;
+ mTopKeys = topKeys == 0 ? numColumns : topKeys;
+
+ final int numLeftKeys = (numColumns - 1) / 2;
+ final int numRightKeys = numColumns - numLeftKeys; // including default key.
+ // Maximum number of keys we can layout both side of the parent key
+ final int maxLeftKeys = coordXInParent / keyWidth;
+ final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
+ int leftKeys, rightKeys;
+ if (numLeftKeys > maxLeftKeys) {
+ leftKeys = maxLeftKeys;
+ rightKeys = numColumns - leftKeys;
+ } else if (numRightKeys > maxRightKeys + 1) {
+ rightKeys = maxRightKeys + 1; // include default key
+ leftKeys = numColumns - rightKeys;
+ } else {
+ leftKeys = numLeftKeys;
+ rightKeys = numRightKeys;
+ }
+ // If the left keys fill the left side of the parent key, entire more keys keyboard
+ // should be shifted to the right unless the parent key is on the left edge.
+ if (maxLeftKeys == leftKeys && leftKeys > 0) {
+ leftKeys--;
+ rightKeys++;
+ }
+ // If the right keys fill the right side of the parent key, entire more keys
+ // should be shifted to the left unless the parent key is on the right edge.
+ if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
+ leftKeys++;
+ rightKeys--;
+ }
+ mLeftKeys = leftKeys;
+ mRightKeys = rightKeys;
+
+ // Adjustment of the top row.
+ mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
+ : getAutoOrderTopRowAdjustment();
+ mDividerWidth = dividerWidth;
+ mColumnWidth = mDefaultKeyWidth + mDividerWidth;
+ mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
+ // Need to subtract the bottom row's gutter only.
+ mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
+ + mTopPadding + mBottomPadding;
+ }
+
+ private int getFixedOrderTopRowAdjustment() {
+ if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
+ || mLeftKeys == 0 || mRightKeys == 1) {
+ return 0;
+ }
+ return -1;
+ }
+
+ private int getAutoOrderTopRowAdjustment() {
+ if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
+ || mLeftKeys == 0 || mRightKeys == 1) {
+ return 0;
+ }
+ return -1;
+ }
+
+ // Return key position according to column count (0 is default).
+ /* package */int getColumnPos(int n) {
+ return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
+ }
+
+ private int getFixedOrderColumnPos(int n) {
+ final int col = n % mNumColumns;
+ final int row = n / mNumColumns;
+ if (!isTopRow(row)) {
+ return col - mLeftKeys;
+ }
+ final int rightSideKeys = mTopKeys / 2;
+ final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
+ final int pos = col - leftSideKeys;
+ final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
+ final int numRightKeys = mRightKeys - 1;
+ if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
+ return pos;
+ } else if (numRightKeys < rightSideKeys) {
+ return pos - (rightSideKeys - numRightKeys);
+ } else { // numLeftKeys < leftSideKeys
+ return pos + (leftSideKeys - numLeftKeys);
+ }
+ }
+
+ private int getAutomaticColumnPos(int n) {
+ final int col = n % mNumColumns;
+ final int row = n / mNumColumns;
+ int leftKeys = mLeftKeys;
+ if (isTopRow(row)) {
+ leftKeys += mTopRowAdjustment;
+ }
+ if (col == 0) {
+ // default position.
+ return 0;
+ }
+
+ int pos = 0;
+ int right = 1; // include default position key.
+ int left = 0;
+ int i = 0;
+ while (true) {
+ // Assign right key if available.
+ if (right < mRightKeys) {
+ pos = right;
+ right++;
+ i++;
+ }
+ if (i >= col)
+ break;
+ // Assign left key if available.
+ if (left < leftKeys) {
+ left++;
+ pos = -left;
+ i++;
+ }
+ if (i >= col)
+ break;
+ }
+ return pos;
+ }
+
+ private static int getTopRowEmptySlots(int numKeys, int numColumns) {
+ final int remainings = numKeys % numColumns;
+ return remainings == 0 ? 0 : numColumns - remainings;
+ }
+
+ private int getOptimizedColumns(int numKeys, int maxColumns) {
+ int numColumns = Math.min(numKeys, maxColumns);
+ while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
+ numColumns--;
+ }
+ return numColumns;
+ }
+
+ public int getDefaultKeyCoordX() {
+ return mLeftKeys * mColumnWidth;
+ }
+
+ public int getX(int n, int row) {
+ final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
+ if (isTopRow(row)) {
+ return x + mTopRowAdjustment * (mColumnWidth / 2);
+ }
+ return x;
+ }
+
+ public int getY(int row) {
+ return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
+ }
+
+ public void markAsEdgeKey(Key key, int row) {
+ if (row == 0)
+ key.markAsTopEdge(this);
+ if (isTopRow(row))
+ key.markAsBottomEdge(this);
+ }
+
+ private boolean isTopRow(int rowCount) {
+ return mNumRows > 1 && rowCount == mNumRows - 1;
+ }
+ }
+
+ /**
+ * The builder of MoreKeysKeyboard.
+ * @param containerView the container of {@link MoreKeysKeyboardView}.
+ * @param parentKey the {@link Key} that invokes more keys keyboard.
+ * @param parentKeyboardView the {@link KeyboardView} that contains the parentKey.
+ */
+ public Builder(View containerView, Key parentKey, KeyboardView parentKeyboardView) {
+ super(containerView.getContext(), new MoreKeysKeyboardParams());
+ final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
+ load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId);
+
+ // TODO: More keys keyboard's vertical gap is currently calculated heuristically.
+ // Should revise the algorithm.
+ mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2;
+ mParentKey = parentKey;
+
+ final int width, height;
+ final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled()
+ && !parentKey.noKeyPreview() && parentKey.mMoreKeys.length == 1;
+ if (singleMoreKeyWithPreview) {
+ // Use pre-computed width and height if this more keys keyboard has only one key to
+ // mitigate visual flicker between key preview and more keys keyboard.
+ // Caveats for the visual assets: To achieve this effect, both the key preview
+ // backgrounds and the more keys keyboard panel background have the exact same
+ // left/right/top paddings. The bottom paddings of both backgrounds don't need to
+ // be considered because the vertical positions of both backgrounds were already
+ // adjusted with their bottom paddings deducted.
+ width = parentKeyboardView.mKeyPreviewDrawParams.mPreviewVisibleWidth;
+ height = parentKeyboardView.mKeyPreviewDrawParams.mPreviewVisibleHeight
+ + mParams.mVerticalGap;
+ } else {
+ width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth);
+ height = parentKeyboard.mMostCommonKeyHeight;
+ }
+ final int dividerWidth;
+ if (parentKey.needsDividersInMoreKeys()) {
+ mDivider = mResources.getDrawable(R.drawable.more_keys_divider);
+ dividerWidth = (int)(width * DIVIDER_RATIO);
+ } else {
+ mDivider = null;
+ dividerWidth = 0;
+ }
+ mParams.setParameters(parentKey.mMoreKeys.length, parentKey.getMoreKeysColumn(),
+ width, height, parentKey.mX + parentKey.mWidth / 2,
+ parentKeyboardView.getMeasuredWidth(), parentKey.isFixedColumnOrderMoreKeys(),
+ dividerWidth);
+ }
+
+ private static int getMaxKeyWidth(KeyboardView view, Key parentKey, int minKeyWidth) {
+ final int padding = (int)(view.getResources()
+ .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
+ + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0));
+ final Paint paint = view.newDefaultLabelPaint();
+ paint.setTextSize(parentKey.hasLabelsInMoreKeys()
+ ? view.mKeyDrawParams.mKeyLabelSize
+ : view.mKeyDrawParams.mKeyLetterSize);
+ int maxWidth = minKeyWidth;
+ for (final MoreKeySpec spec : parentKey.mMoreKeys) {
+ final String label = spec.mLabel;
+ // If the label is single letter, minKeyWidth is enough to hold the label.
+ if (label != null && StringUtils.codePointCount(label) > 1) {
+ final int width = (int)view.getLabelWidth(label, paint) + padding;
+ if (maxWidth < width) {
+ maxWidth = width;
+ }
+ }
+ }
+ return maxWidth;
+ }
+
+ private static class MoreKeyDivider extends Key.Spacer {
+ private final Drawable mIcon;
+
+ public MoreKeyDivider(MoreKeysKeyboardParams params, Drawable icon, int x, int y) {
+ super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
+ mIcon = icon;
+ }
+
+ @Override
+ public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
+ // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+ // constructor.
+ // TODO: Drawable itself should have an alpha value.
+ mIcon.setAlpha(128);
+ return mIcon;
+ }
+ }
+
+ @Override
+ public MoreKeysKeyboard build() {
+ final MoreKeysKeyboardParams params = mParams;
+ final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
+ final MoreKeySpec[] moreKeys = mParentKey.mMoreKeys;
+ for (int n = 0; n < moreKeys.length; n++) {
+ final MoreKeySpec moreKeySpec = moreKeys[n];
+ final int row = n / params.mNumColumns;
+ final int x = params.getX(n, row);
+ final int y = params.getY(row);
+ final Key key = new Key(params, moreKeySpec, x, y,
+ params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags);
+ params.markAsEdgeKey(key, row);
+ params.onAddKey(key);
+
+ final int pos = params.getColumnPos(n);
+ // The "pos" value represents the offset from the default position. Negative means
+ // left of the default position.
+ if (params.mDividerWidth > 0 && pos != 0) {
+ final int dividerX = (pos > 0) ? x - params.mDividerWidth
+ : x + params.mDefaultKeyWidth;
+ final Key divider = new MoreKeyDivider(params, mDivider, dividerX, y);
+ params.onAddKey(divider);
+ }
+ }
+ return new MoreKeysKeyboard(params);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
new file mode 100644
index 000000000..be7644fb5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.PopupWindow;
+
+import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
+import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.R;
+
+/**
+ * A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
+ * detecting key presses and touch movements.
+ */
+public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
+ private final int[] mCoordinates = new int[2];
+
+ private final KeyDetector mKeyDetector;
+
+ private Controller mController;
+ private KeyboardActionListener mListener;
+ private int mOriginX;
+ private int mOriginY;
+
+ private static final TimerProxy EMPTY_TIMER_PROXY = new TimerProxy.Adapter();
+
+ private final KeyboardActionListener mMoreKeysKeyboardListener =
+ new KeyboardActionListener.Adapter() {
+ @Override
+ public void onCodeInput(int primaryCode, int x, int y) {
+ // Because a more keys keyboard doesn't need proximity characters correction, we don't
+ // send touch event coordinates.
+ mListener.onCodeInput(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE);
+ }
+
+ @Override
+ public void onTextInput(CharSequence text) {
+ mListener.onTextInput(text);
+ }
+
+ @Override
+ public void onCancelInput() {
+ mListener.onCancelInput();
+ }
+
+ @Override
+ public void onPressKey(int primaryCode) {
+ mListener.onPressKey(primaryCode);
+ }
+
+ @Override
+ public void onReleaseKey(int primaryCode, boolean withSliding) {
+ mListener.onReleaseKey(primaryCode, withSliding);
+ }
+ };
+
+ public MoreKeysKeyboardView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
+ }
+
+ public MoreKeysKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final Resources res = context.getResources();
+ mKeyDetector = new MoreKeysDetector(
+ res.getDimension(R.dimen.more_keys_keyboard_slide_allowance));
+ setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard != null) {
+ final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
+ final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ @Override
+ public void setKeyboard(Keyboard keyboard) {
+ super.setKeyboard(keyboard);
+ mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
+ -getPaddingTop() + mVerticalCorrection);
+ }
+
+ @Override
+ public KeyDetector getKeyDetector() {
+ return mKeyDetector;
+ }
+
+ @Override
+ public KeyboardActionListener getKeyboardActionListener() {
+ return mMoreKeysKeyboardListener;
+ }
+
+ @Override
+ public DrawingProxy getDrawingProxy() {
+ return this;
+ }
+
+ @Override
+ public TimerProxy getTimerProxy() {
+ return EMPTY_TIMER_PROXY;
+ }
+
+ @Override
+ public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ // More keys keyboard needs no pop-up key preview displayed, so we pass always false with a
+ // delay of 0. The delay does not matter actually since the popup is not shown anyway.
+ super.setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ public void showMoreKeysPanel(View parentView, Controller controller, int pointX, int pointY,
+ PopupWindow window, KeyboardActionListener listener) {
+ mController = controller;
+ mListener = listener;
+ final View container = (View)getParent();
+ final MoreKeysKeyboard pane = (MoreKeysKeyboard)getKeyboard();
+ final int defaultCoordX = pane.getDefaultCoordX();
+ // The coordinates of panel's left-top corner in parentView's coordinate system.
+ final int x = pointX - defaultCoordX - container.getPaddingLeft();
+ final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom();
+
+ window.setContentView(container);
+ window.setWidth(container.getMeasuredWidth());
+ window.setHeight(container.getMeasuredHeight());
+ parentView.getLocationInWindow(mCoordinates);
+ window.showAtLocation(parentView, Gravity.NO_GRAVITY,
+ x + mCoordinates[0], y + mCoordinates[1]);
+
+ mOriginX = x + container.getPaddingLeft();
+ mOriginY = y + container.getPaddingTop();
+ }
+
+ private boolean mIsDismissing;
+
+ @Override
+ public boolean dismissMoreKeysPanel() {
+ if (mIsDismissing || mController == null) return false;
+ mIsDismissing = true;
+ final boolean dismissed = mController.dismissMoreKeysPanel();
+ mIsDismissing = false;
+ return dismissed;
+ }
+
+ @Override
+ public int translateX(int x) {
+ return x - mOriginX;
+ }
+
+ @Override
+ public int translateY(int y) {
+ return y - mOriginY;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
new file mode 100644
index 000000000..f9a196d24
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+import android.view.View;
+import android.widget.PopupWindow;
+
+public interface MoreKeysPanel extends PointerTracker.KeyEventHandler {
+ public interface Controller {
+ public boolean dismissMoreKeysPanel();
+ }
+
+ /**
+ * Show more keys panel.
+ *
+ * @param parentView the parent view of this more keys panel
+ * @param controller the controller that can dismiss this more keys panel
+ * @param pointX x coordinate of this more keys panel
+ * @param pointY y coordinate of this more keys panel
+ * @param window PopupWindow to be used to show this more keys panel
+ * @param listener the listener that will receive keyboard action from this more keys panel.
+ */
+ public void showMoreKeysPanel(View parentView, Controller controller, int pointX, int pointY,
+ PopupWindow window, KeyboardActionListener listener);
+
+ /**
+ * Translate X-coordinate of touch event to the local X-coordinate of this
+ * {@link MoreKeysPanel}.
+ *
+ * @param x the global X-coordinate
+ * @return the local X-coordinate to this {@link MoreKeysPanel}
+ */
+ public int translateX(int x);
+
+ /**
+ * Translate Y-coordinate of touch event to the local Y-coordinate of this
+ * {@link MoreKeysPanel}.
+ *
+ * @param y the global Y-coordinate
+ * @return the local Y-coordinate to this {@link MoreKeysPanel}
+ */
+ public int translateY(int y);
+}
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 8b03360bf..34e428e82 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -16,73 +16,143 @@
package com.android.inputmethod.keyboard;
-import android.content.res.Resources;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.View;
+import android.widget.TextView;
-import com.android.inputmethod.keyboard.KeyboardView.UIHandler;
-import com.android.inputmethod.keyboard.internal.PointerTrackerKeyState;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.define.ProductionFlag;
-import java.util.Arrays;
-import java.util.List;
+import java.util.ArrayList;
public class PointerTracker {
private static final String TAG = PointerTracker.class.getSimpleName();
- private static final boolean ENABLE_ASSERTION = false;
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_MOVE_EVENT = false;
private static final boolean DEBUG_LISTENER = false;
private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
- public interface UIProxy {
+ public interface KeyEventHandler {
+ /**
+ * Get KeyDetector object that is used for this PointerTracker.
+ * @return the KeyDetector object that is used for this PointerTracker
+ */
+ public KeyDetector getKeyDetector();
+
+ /**
+ * Get KeyboardActionListener object that is used to register key code and so on.
+ * @return the KeyboardActionListner for this PointerTracker
+ */
+ public KeyboardActionListener getKeyboardActionListener();
+
+ /**
+ * Get DrawingProxy object that is used for this PointerTracker.
+ * @return the DrawingProxy object that is used for this PointerTracker
+ */
+ public DrawingProxy getDrawingProxy();
+
+ /**
+ * Get TimerProxy object that handles key repeat and long press timer event for this
+ * PointerTracker.
+ * @return the TimerProxy object that handles key repeat and long press timer event.
+ */
+ public TimerProxy getTimerProxy();
+ }
+
+ public interface DrawingProxy extends MoreKeysPanel.Controller {
public void invalidateKey(Key key);
- public void showKeyPreview(int keyIndex, PointerTracker tracker);
+ public TextView inflateKeyPreviewText();
+ public void showKeyPreview(PointerTracker tracker);
public void dismissKeyPreview(PointerTracker tracker);
- public boolean hasDistinctMultitouch();
}
- public final int mPointerId;
+ public interface TimerProxy {
+ public void startTypingStateTimer();
+ public boolean isTypingState();
+ public void startKeyRepeatTimer(PointerTracker tracker);
+ public void startLongPressTimer(PointerTracker tracker);
+ public void startLongPressTimer(int code);
+ public void cancelLongPressTimer();
+ public void startDoubleTapTimer();
+ public void cancelDoubleTapTimer();
+ public boolean isInDoubleTapTimeout();
+ public void cancelKeyTimers();
+
+ public static class Adapter implements TimerProxy {
+ @Override
+ public void startTypingStateTimer() {}
+ @Override
+ public boolean isTypingState() { return false; }
+ @Override
+ public void startKeyRepeatTimer(PointerTracker tracker) {}
+ @Override
+ public void startLongPressTimer(PointerTracker tracker) {}
+ @Override
+ public void startLongPressTimer(int code) {}
+ @Override
+ public void cancelLongPressTimer() {}
+ @Override
+ public void startDoubleTapTimer() {}
+ @Override
+ public void cancelDoubleTapTimer() {}
+ @Override
+ public boolean isInDoubleTapTimeout() { return false; }
+ @Override
+ public void cancelKeyTimers() {}
+ }
+ }
- // Timing constants
- private final int mDelayBeforeKeyRepeatStart;
- private final int mLongPressKeyTimeout;
- private final int mLongPressShiftKeyTimeout;
+ // Parameters for pointer handling.
+ private static LatinKeyboardView.PointerTrackerParams sParams;
+ private static int sTouchNoiseThresholdDistanceSquared;
+ private static boolean sNeedsPhantomSuddenMoveEventHack;
- private final KeyboardView mKeyboardView;
- private final UIProxy mProxy;
- private final UIHandler mHandler;
- private final KeyDetector mKeyDetector;
- private KeyboardActionListener mListener = EMPTY_LISTENER;
- private final KeyboardSwitcher mKeyboardSwitcher;
- private final boolean mHasDistinctMultitouch;
- private final boolean mConfigSlidingKeyInputEnabled;
+ private static final ArrayList<PointerTracker> sTrackers = new ArrayList<PointerTracker>();
+ private static PointerTrackerQueue sPointerTrackerQueue;
+
+ public final int mPointerId;
- private final int mTouchNoiseThresholdMillis;
- private final int mTouchNoiseThresholdDistanceSquared;
+ private DrawingProxy mDrawingProxy;
+ private TimerProxy mTimerProxy;
+ private KeyDetector mKeyDetector;
+ private KeyboardActionListener mListener = EMPTY_LISTENER;
private Keyboard mKeyboard;
- private List<Key> mKeys;
- private int mKeyHysteresisDistanceSquared = -1;
private int mKeyQuarterWidthSquared;
+ private final TextView mKeyPreviewText;
+
+ // The position and time at which first down event occurred.
+ private long mDownTime;
+ private long mUpTime;
- private final PointerTrackerKeyState mKeyState;
+ // The current key where this pointer is.
+ private Key mCurrentKey = null;
+ // The position where the current key was recognized for the first time.
+ private int mKeyX;
+ private int mKeyY;
+
+ // Last pointer position.
+ private int mLastX;
+ private int mLastY;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
- // true if event is already translated to a key action (long press or mini-keyboard)
+ // true if event is already translated to a key action.
private boolean mKeyAlreadyProcessed;
+ // true if this pointer has been long-pressed and is showing a more keys panel.
+ private boolean mIsShowingMoreKeysPanel;
+
// true if this pointer is repeatable key
private boolean mIsRepeatableKey;
// true if this pointer is in sliding key input
- private boolean mIsInSlidingKeyInput;
+ boolean mIsInSlidingKeyInput;
// true if sliding key is allowed.
private boolean mIsAllowedSlidingKeyInput;
@@ -90,69 +160,98 @@ public class PointerTracker {
// ignore modifier key if true
private boolean mIgnoreModifierKey;
- // TODO: Remove these hacking variables
- // true if this pointer is in sliding language switch
- private boolean mIsInSlidingLanguageSwitch;
- private int mSpaceKeyIndex;
- private final SubtypeSwitcher mSubtypeSwitcher;
-
// Empty {@link KeyboardActionListener}
- private static final KeyboardActionListener EMPTY_LISTENER = new KeyboardActionListener() {
- @Override
- public void onPress(int primaryCode, boolean withSliding) {}
- @Override
- public void onRelease(int primaryCode, boolean withSliding) {}
- @Override
- public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {}
- @Override
- public void onTextInput(CharSequence text) {}
- @Override
- public void onCancelInput() {}
- @Override
- public void onSwipeDown() {}
- };
-
- public PointerTracker(int id, KeyboardView keyboardView, UIHandler handler,
- KeyDetector keyDetector, UIProxy proxy) {
- if (proxy == null || handler == null || keyDetector == null)
+ private static final KeyboardActionListener EMPTY_LISTENER =
+ new KeyboardActionListener.Adapter();
+
+ public static void init(boolean hasDistinctMultitouch,
+ boolean needsPhantomSuddenMoveEventHack) {
+ if (hasDistinctMultitouch) {
+ sPointerTrackerQueue = new PointerTrackerQueue();
+ } else {
+ sPointerTrackerQueue = null;
+ }
+ sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
+
+ setParameters(LatinKeyboardView.PointerTrackerParams.DEFAULT);
+ }
+
+ public static void setParameters(LatinKeyboardView.PointerTrackerParams params) {
+ sParams = params;
+ sTouchNoiseThresholdDistanceSquared = (int)(
+ params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance);
+ }
+
+ public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) {
+ final ArrayList<PointerTracker> trackers = sTrackers;
+
+ // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
+ for (int i = trackers.size(); i <= id; i++) {
+ final PointerTracker tracker = new PointerTracker(i, handler);
+ trackers.add(tracker);
+ }
+
+ return trackers.get(id);
+ }
+
+ public static boolean isAnyInSlidingKeyInput() {
+ return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
+ }
+
+ public static void setKeyboardActionListener(KeyboardActionListener listener) {
+ for (final PointerTracker tracker : sTrackers) {
+ tracker.mListener = listener;
+ }
+ }
+
+ public static void setKeyDetector(KeyDetector keyDetector) {
+ for (final PointerTracker tracker : sTrackers) {
+ tracker.setKeyDetectorInner(keyDetector);
+ // Mark that keyboard layout has been changed.
+ tracker.mKeyboardLayoutHasBeenChanged = true;
+ }
+ }
+
+ public static void dismissAllKeyPreviews() {
+ for (final PointerTracker tracker : sTrackers) {
+ tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
+ tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
+ }
+ }
+
+ public PointerTracker(int id, KeyEventHandler handler) {
+ if (handler == null)
throw new NullPointerException();
mPointerId = id;
- mKeyboardView = keyboardView;
- mProxy = proxy;
- mHandler = handler;
- mKeyDetector = keyDetector;
- mKeyboardSwitcher = KeyboardSwitcher.getInstance();
- mKeyState = new PointerTrackerKeyState(keyDetector);
- mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
- final Resources res = mKeyboardView.getResources();
- mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled);
- mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
- mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
- mLongPressShiftKeyTimeout = res.getInteger(R.integer.config_long_press_shift_key_timeout);
- mTouchNoiseThresholdMillis = res.getInteger(R.integer.config_touch_noise_threshold_millis);
- final float touchNoiseThresholdDistance = res.getDimension(
- R.dimen.config_touch_noise_threshold_distance);
- mTouchNoiseThresholdDistanceSquared = (int)(
- touchNoiseThresholdDistance * touchNoiseThresholdDistance);
- mSubtypeSwitcher = SubtypeSwitcher.getInstance();
- }
-
- public void setOnKeyboardActionListener(KeyboardActionListener listener) {
- mListener = listener;
+ setKeyDetectorInner(handler.getKeyDetector());
+ mListener = handler.getKeyboardActionListener();
+ mDrawingProxy = handler.getDrawingProxy();
+ mTimerProxy = handler.getTimerProxy();
+ mKeyPreviewText = mDrawingProxy.inflateKeyPreviewText();
+ }
+
+ public TextView getKeyPreviewText() {
+ return mKeyPreviewText;
}
// Returns true if keyboard has been changed by this callback.
- private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key, boolean withSliding) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
- if (DEBUG_LISTENER)
- Log.d(TAG, "onPress : " + keyCodePrintable(key.mCode) + " sliding=" + withSliding
- + " ignoreModifier=" + ignoreModifierKey);
- if (ignoreModifierKey)
+ private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
+ final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, "onPress : " + KeyDetector.printableCode(key)
+ + " ignoreModifier=" + ignoreModifierKey
+ + " enabled=" + key.isEnabled());
+ }
+ if (ignoreModifierKey) {
return false;
+ }
if (key.isEnabled()) {
- mListener.onPress(key.mCode, withSliding);
+ mListener.onPressKey(key.mCode);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
+ if (!key.altCodeWhileTyping() && !key.isModifier()) {
+ mTimerProxy.startTypingStateTimer();
+ }
return keyboardLayoutHasBeenChanged;
}
return false;
@@ -160,170 +259,250 @@ public class PointerTracker {
// Note that we need primaryCode argument because the keyboard may in shifted state and the
// primaryCode is different from {@link Key#mCode}.
- private void callListenerOnCodeInput(Key key, int primaryCode, int[] keyCodes, int x, int y) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
- if (DEBUG_LISTENER)
- Log.d(TAG, "onCodeInput: " + keyCodePrintable(primaryCode)
- + " codes="+ Arrays.toString(keyCodes) + " x=" + x + " y=" + y
- + " ignoreModifier=" + ignoreModifierKey);
- if (ignoreModifierKey)
+ private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) {
+ final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
+ final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
+ final int code = altersCode ? key.mAltCode : primaryCode;
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + " text=" + key.mOutputText
+ + " x=" + x + " y=" + y
+ + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
+ + " enabled=" + key.isEnabled());
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
+ altersCode, code);
+ }
+ if (ignoreModifierKey) {
return;
- if (key.isEnabled())
- mListener.onCodeInput(primaryCode, keyCodes, x, y);
- }
-
- private void callListenerOnTextInput(Key key) {
- if (DEBUG_LISTENER)
- Log.d(TAG, "onTextInput: text=" + key.mOutputText);
- if (key.isEnabled())
- mListener.onTextInput(key.mOutputText);
+ }
+ // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
+ if (key.isEnabled() || altersCode) {
+ if (code == Keyboard.CODE_OUTPUT_TEXT) {
+ mListener.onTextInput(key.mOutputText);
+ } else if (code != Keyboard.CODE_UNSPECIFIED) {
+ mListener.onCodeInput(code, x, y);
+ }
+ }
}
// Note that we need primaryCode argument because the keyboard may in shifted state and the
// primaryCode is different from {@link Key#mCode}.
private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && isModifierCode(key.mCode);
- if (DEBUG_LISTENER)
- Log.d(TAG, "onRelease : " + keyCodePrintable(primaryCode) + " sliding="
- + withSliding + " ignoreModifier=" + ignoreModifierKey);
- if (ignoreModifierKey)
+ final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode)
+ + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
+ + " enabled="+ key.isEnabled());
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
+ ignoreModifierKey);
+ }
+ if (ignoreModifierKey) {
return;
- if (key.isEnabled())
- mListener.onRelease(primaryCode, withSliding);
+ }
+ if (key.isEnabled()) {
+ mListener.onReleaseKey(primaryCode, withSliding);
+ }
}
private void callListenerOnCancelInput() {
if (DEBUG_LISTENER)
Log.d(TAG, "onCancelInput");
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.pointerTracker_callListenerOnCancelInput();
+ }
mListener.onCancelInput();
}
- public void setKeyboard(Keyboard keyboard, float keyHysteresisDistance) {
- if (keyboard == null || keyHysteresisDistance < 0)
- throw new IllegalArgumentException();
- mKeyboard = keyboard;
- mKeys = keyboard.getKeys();
- mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
- final int keyQuarterWidth = keyboard.getKeyWidth() / 4;
+ private void setKeyDetectorInner(KeyDetector keyDetector) {
+ mKeyDetector = keyDetector;
+ mKeyboard = keyDetector.getKeyboard();
+ final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4;
mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth;
- // Mark that keyboard layout has been changed.
- mKeyboardLayoutHasBeenChanged = true;
}
public boolean isInSlidingKeyInput() {
return mIsInSlidingKeyInput;
}
- private boolean isValidKeyIndex(int keyIndex) {
- return keyIndex >= 0 && keyIndex < mKeys.size();
+ public Key getKey() {
+ return mCurrentKey;
}
- public Key getKey(int keyIndex) {
- return isValidKeyIndex(keyIndex) ? mKeys.get(keyIndex) : null;
+ public boolean isModifier() {
+ return mCurrentKey != null && mCurrentKey.isModifier();
}
- private static boolean isModifierCode(int primaryCode) {
- return primaryCode == Keyboard.CODE_SHIFT
- || primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL;
+ public Key getKeyOn(int x, int y) {
+ return mKeyDetector.detectHitKey(x, y);
}
- private boolean isModifierInternal(int keyIndex) {
- final Key key = getKey(keyIndex);
- return key == null ? false : isModifierCode(key.mCode);
+ private void setReleasedKeyGraphics(Key key) {
+ mDrawingProxy.dismissKeyPreview(this);
+ if (key == null) {
+ return;
+ }
+
+ // Even if the key is disabled, update the key release graphics just in case.
+ updateReleaseKeyGraphics(key);
+
+ if (key.isShift()) {
+ for (final Key shiftKey : mKeyboard.mShiftKeys) {
+ if (shiftKey != key) {
+ updateReleaseKeyGraphics(shiftKey);
+ }
+ }
+ }
+
+ if (key.altCodeWhileTyping()) {
+ final int altCode = key.mAltCode;
+ final Key altKey = mKeyboard.getKey(altCode);
+ if (altKey != null) {
+ updateReleaseKeyGraphics(altKey);
+ }
+ for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
+ if (k != key && k.mAltCode == altCode) {
+ updateReleaseKeyGraphics(k);
+ }
+ }
+ }
}
- public boolean isModifier() {
- return isModifierInternal(mKeyState.getKeyIndex());
+ private void setPressedKeyGraphics(Key key) {
+ if (key == null) {
+ return;
+ }
+
+ // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
+ final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
+ final boolean needsToUpdateGraphics = key.isEnabled() || altersCode;
+ if (!needsToUpdateGraphics) {
+ return;
+ }
+
+ if (!key.noKeyPreview()) {
+ mDrawingProxy.showKeyPreview(this);
+ }
+ updatePressKeyGraphics(key);
+
+ if (key.isShift()) {
+ for (final Key shiftKey : mKeyboard.mShiftKeys) {
+ if (shiftKey != key) {
+ updatePressKeyGraphics(shiftKey);
+ }
+ }
+ }
+
+ if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
+ final int altCode = key.mAltCode;
+ final Key altKey = mKeyboard.getKey(altCode);
+ if (altKey != null) {
+ updatePressKeyGraphics(altKey);
+ }
+ for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
+ if (k != key && k.mAltCode == altCode) {
+ updatePressKeyGraphics(k);
+ }
+ }
+ }
}
- private boolean isOnModifierKey(int x, int y) {
- return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
+ private void updateReleaseKeyGraphics(Key key) {
+ key.onReleased();
+ mDrawingProxy.invalidateKey(key);
}
- public boolean isOnShiftKey(int x, int y) {
- final Key key = getKey(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
- return key != null && key.mCode == Keyboard.CODE_SHIFT;
+ private void updatePressKeyGraphics(Key key) {
+ key.onPressed();
+ mDrawingProxy.invalidateKey(key);
}
- public int getKeyIndexOn(int x, int y) {
- return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
+ public int getLastX() {
+ return mLastX;
}
- public boolean isSpaceKey(int keyIndex) {
- Key key = getKey(keyIndex);
- return key != null && key.mCode == Keyboard.CODE_SPACE;
+ public int getLastY() {
+ return mLastY;
}
- public void setReleasedKeyGraphics() {
- setReleasedKeyGraphics(mKeyState.getKeyIndex());
+ public long getDownTime() {
+ return mDownTime;
}
- private void setReleasedKeyGraphics(int keyIndex) {
- final Key key = getKey(keyIndex);
- if (key != null) {
- key.onReleased();
- mProxy.invalidateKey(key);
- }
+ private Key onDownKey(int x, int y, long eventTime) {
+ mDownTime = eventTime;
+ return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
}
- private void setPressedKeyGraphics(int keyIndex) {
- final Key key = getKey(keyIndex);
- if (key != null && key.isEnabled()) {
- key.onPressed();
- mProxy.invalidateKey(key);
- }
+ private Key onMoveKeyInternal(int x, int y) {
+ mLastX = x;
+ mLastY = y;
+ return mKeyDetector.detectHitKey(x, y);
}
- private void checkAssertion(PointerTrackerQueue queue) {
- if (mHasDistinctMultitouch && queue == null)
- throw new RuntimeException(
- "PointerTrackerQueue must be passed on distinct multi touch device");
- if (!mHasDistinctMultitouch && queue != null)
- throw new RuntimeException(
- "PointerTrackerQueue must be null on non-distinct multi touch device");
+ private Key onMoveKey(int x, int y) {
+ return onMoveKeyInternal(x, y);
}
- public void onTouchEvent(int action, int x, int y, long eventTime, PointerTrackerQueue queue) {
+ private Key onMoveToNewKey(Key newKey, int x, int y) {
+ mCurrentKey = newKey;
+ mKeyX = x;
+ mKeyY = y;
+ return newKey;
+ }
+
+ public void processMotionEvent(int action, int x, int y, long eventTime,
+ KeyEventHandler handler) {
switch (action) {
- case MotionEvent.ACTION_MOVE:
- onMoveEvent(x, y, eventTime, queue);
- break;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
- onDownEvent(x, y, eventTime, queue);
+ onDownEvent(x, y, eventTime, handler);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
- onUpEvent(x, y, eventTime, queue);
+ onUpEvent(x, y, eventTime);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onMoveEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_CANCEL:
- onCancelEvent(x, y, eventTime, queue);
+ onCancelEvent(x, y, eventTime);
break;
}
}
- public void onDownEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
- if (ENABLE_ASSERTION) checkAssertion(queue);
+ public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) {
if (DEBUG_EVENT)
printTouchEvent("onDownEvent:", x, y, eventTime);
+ mDrawingProxy = handler.getDrawingProxy();
+ mTimerProxy = handler.getTimerProxy();
+ setKeyboardActionListener(handler.getKeyboardActionListener());
+ setKeyDetectorInner(handler.getKeyDetector());
// Naive up-to-down noise filter.
- final long deltaT = eventTime - mKeyState.getUpTime();
- if (deltaT < mTouchNoiseThresholdMillis) {
- final int dx = x - mKeyState.getLastX();
- final int dy = y - mKeyState.getLastY();
+ final long deltaT = eventTime - mUpTime;
+ if (deltaT < sParams.mTouchNoiseThresholdTime) {
+ final int dx = x - mLastX;
+ final int dy = y - mLastY;
final int distanceSquared = (dx * dx + dy * dy);
- if (distanceSquared < mTouchNoiseThresholdDistanceSquared) {
+ if (distanceSquared < sTouchNoiseThresholdDistanceSquared) {
if (DEBUG_MODE)
Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
+ " distance=" + distanceSquared);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.pointerTracker_onDownEvent(deltaT, distanceSquared);
+ }
mKeyAlreadyProcessed = true;
return;
}
}
+ final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- if (isOnModifierKey(x, y)) {
+ final Key key = getKeyOn(x, y);
+ if (key != null && key.isModifier()) {
// Before processing a down event of modifier key, all pointers already being
// tracked should be released.
queue.releaseAllPointers(eventTime);
@@ -334,88 +513,80 @@ public class PointerTracker {
}
private void onDownEventInternal(int x, int y, long eventTime) {
- int keyIndex = mKeyState.onDownKey(x, y, eventTime);
+ Key key = onDownKey(x, y, eventTime);
// Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
- // from modifier key, or 3) this pointer is on mini-keyboard.
- mIsAllowedSlidingKeyInput = mConfigSlidingKeyInputEnabled || isModifierInternal(keyIndex)
- || mKeyDetector instanceof MiniKeyboardKeyDetector;
+ // from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
+ mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled
+ || (key != null && key.isModifier())
+ || mKeyDetector.alwaysAllowsSlidingInput();
mKeyboardLayoutHasBeenChanged = false;
mKeyAlreadyProcessed = false;
mIsRepeatableKey = false;
mIsInSlidingKeyInput = false;
- mIsInSlidingLanguageSwitch = false;
mIgnoreModifierKey = false;
- if (isValidKeyIndex(keyIndex)) {
+ if (key != null) {
// This onPress call may have changed keyboard layout. Those cases are detected at
- // {@link #setKeyboard}. In those cases, we should update keyIndex according to the new
+ // {@link #setKeyboard}. In those cases, we should update key according to the new
// keyboard layout.
- if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), false))
- keyIndex = mKeyState.onDownKey(x, y, eventTime);
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
+ key = onDownKey(x, y, eventTime);
+ }
- startRepeatKey(keyIndex);
- startLongPressTimer(keyIndex);
- showKeyPreview(keyIndex);
- setPressedKeyGraphics(keyIndex);
+ startRepeatKey(key);
+ startLongPressTimer(key);
+ setPressedKeyGraphics(key);
}
}
private void startSlidingKeyInput(Key key) {
- if (!mIsInSlidingKeyInput)
- mIgnoreModifierKey = isModifierCode(key.mCode);
+ if (!mIsInSlidingKeyInput) {
+ mIgnoreModifierKey = key.isModifier();
+ }
mIsInSlidingKeyInput = true;
}
- public void onMoveEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
- if (ENABLE_ASSERTION) checkAssertion(queue);
+ public void onMoveEvent(int x, int y, long eventTime) {
if (DEBUG_MOVE_EVENT)
printTouchEvent("onMoveEvent:", x, y, eventTime);
if (mKeyAlreadyProcessed)
return;
- final PointerTrackerKeyState keyState = mKeyState;
- // TODO: Remove this hacking code
- if (mIsInSlidingLanguageSwitch) {
- ((LatinKeyboard)mKeyboard).updateSpacebarPreviewIcon(x - keyState.getKeyX());
- showKeyPreview(mSpaceKeyIndex);
- return;
- }
- final int lastX = keyState.getLastX();
- final int lastY = keyState.getLastY();
- final int oldKeyIndex = keyState.getKeyIndex();
- final Key oldKey = getKey(oldKeyIndex);
- int keyIndex = keyState.onMoveKey(x, y);
- if (isValidKeyIndex(keyIndex)) {
+ final int lastX = mLastX;
+ final int lastY = mLastY;
+ final Key oldKey = mCurrentKey;
+ Key key = onMoveKey(x, y);
+ if (key != null) {
if (oldKey == null) {
// The pointer has been slid in to the new key, but the finger was not on any keys.
// In this case, we must call onPress() to notify that the new key is being pressed.
// This onPress call may have changed keyboard layout. Those cases are detected at
- // {@link #setKeyboard}. In those cases, we should update keyIndex according to the
+ // {@link #setKeyboard}. In those cases, we should update key according to the
// new keyboard layout.
- if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
- keyIndex = keyState.onMoveKey(x, y);
- keyState.onMoveToNewKey(keyIndex, x, y);
- startLongPressTimer(keyIndex);
- showKeyPreview(keyIndex);
- setPressedKeyGraphics(keyIndex);
- } else if (isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
+ key = onMoveKey(x, y);
+ }
+ onMoveToNewKey(key, x, y);
+ startLongPressTimer(key);
+ setPressedKeyGraphics(key);
+ } else if (isMajorEnoughMoveToBeOnNewKey(x, y, key)) {
// The pointer has been slid in to the new key from the previous key, we must call
// onRelease() first to notify that the previous key has been released, then call
// onPress() to notify that the new key is being pressed.
- setReleasedKeyGraphics(oldKeyIndex);
+ setReleasedKeyGraphics(oldKey);
callListenerOnRelease(oldKey, oldKey.mCode, true);
startSlidingKeyInput(oldKey);
- mHandler.cancelKeyTimers();
- startRepeatKey(keyIndex);
+ mTimerProxy.cancelKeyTimers();
+ startRepeatKey(key);
if (mIsAllowedSlidingKeyInput) {
// This onPress call may have changed keyboard layout. Those cases are detected
- // at {@link #setKeyboard}. In those cases, we should update keyIndex according
+ // at {@link #setKeyboard}. In those cases, we should update key according
// to the new keyboard layout.
- if (callListenerOnPressAndCheckKeyboardLayoutChange(getKey(keyIndex), true))
- keyIndex = keyState.onMoveKey(x, y);
- keyState.onMoveToNewKey(keyIndex, x, y);
- startLongPressTimer(keyIndex);
- setPressedKeyGraphics(keyIndex);
- showKeyPreview(keyIndex);
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
+ key = onMoveKey(x, y);
+ }
+ onMoveToNewKey(key, x, y);
+ startLongPressTimer(key);
+ setPressedKeyGraphics(key);
} else {
// HACK: On some devices, quick successive touches may be translated to sudden
// move by touch panel firmware. This hack detects the case and translates the
@@ -423,287 +594,180 @@ public class PointerTracker {
final int dx = x - lastX;
final int dy = y - lastY;
final int lastMoveSquared = dx * dx + dy * dy;
- if (lastMoveSquared >= mKeyQuarterWidthSquared) {
- if (DEBUG_MODE)
- Log.w(TAG, String.format("onMoveEvent: sudden move is translated to "
+ if (sNeedsPhantomSuddenMoveEventHack
+ && lastMoveSquared >= mKeyQuarterWidthSquared) {
+ if (DEBUG_MODE) {
+ Log.w(TAG, String.format("onMoveEvent:"
+ + " phantom sudden move event is translated to "
+ "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
- onUpEventInternal(lastX, lastY, eventTime, true);
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
+ }
+ onUpEventInternal();
onDownEventInternal(x, y, eventTime);
} else {
mKeyAlreadyProcessed = true;
- dismissKeyPreview();
- setReleasedKeyGraphics(oldKeyIndex);
- }
- }
- }
- // TODO: Remove this hack code
- else if (isSpaceKey(keyIndex) && !mIsInSlidingLanguageSwitch
- && mKeyboard instanceof LatinKeyboard) {
- final LatinKeyboard keyboard = ((LatinKeyboard)mKeyboard);
- if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()
- && mSubtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) {
- final int diff = x - keyState.getKeyX();
- if (keyboard.shouldTriggerSpacebarSlidingLanguageSwitch(diff)) {
- // Detect start sliding language switch.
- mIsInSlidingLanguageSwitch = true;
- mSpaceKeyIndex = keyIndex;
- keyboard.updateSpacebarPreviewIcon(diff);
- // Display spacebar slide language switcher.
- showKeyPreview(keyIndex);
- if (queue != null)
- queue.releaseAllPointersExcept(this, eventTime, true);
+ setReleasedKeyGraphics(oldKey);
}
}
}
} else {
- if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, keyIndex)) {
+ if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, key)) {
// The pointer has been slid out from the previous key, we must call onRelease() to
// notify that the previous key has been released.
- setReleasedKeyGraphics(oldKeyIndex);
+ setReleasedKeyGraphics(oldKey);
callListenerOnRelease(oldKey, oldKey.mCode, true);
startSlidingKeyInput(oldKey);
- mHandler.cancelLongPressTimers();
+ mTimerProxy.cancelLongPressTimer();
if (mIsAllowedSlidingKeyInput) {
- keyState.onMoveToNewKey(keyIndex, x, y);
+ onMoveToNewKey(key, x, y);
} else {
mKeyAlreadyProcessed = true;
- dismissKeyPreview();
}
}
}
}
- public void onUpEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
- if (ENABLE_ASSERTION) checkAssertion(queue);
+ public void onUpEvent(int x, int y, long eventTime) {
if (DEBUG_EVENT)
printTouchEvent("onUpEvent :", x, y, eventTime);
+ final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- if (isModifier()) {
+ if (mCurrentKey != null && mCurrentKey.isModifier()) {
// Before processing an up event of modifier key, all pointers already being
// tracked should be released.
- queue.releaseAllPointersExcept(this, eventTime, true);
+ queue.releaseAllPointersExcept(this, eventTime);
} else {
queue.releaseAllPointersOlderThan(this, eventTime);
}
queue.remove(this);
}
- onUpEventInternal(x, y, eventTime, true);
+ onUpEventInternal();
}
// Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
// This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
// "virtual" up event.
- public void onPhantomUpEvent(int x, int y, long eventTime, boolean updateReleasedKeyGraphics) {
+ public void onPhantomUpEvent(int x, int y, long eventTime) {
if (DEBUG_EVENT)
printTouchEvent("onPhntEvent:", x, y, eventTime);
- onUpEventInternal(x, y, eventTime, updateReleasedKeyGraphics);
+ onUpEventInternal();
mKeyAlreadyProcessed = true;
}
- private void onUpEventInternal(int x, int y, long eventTime,
- boolean updateReleasedKeyGraphics) {
- mHandler.cancelKeyTimers();
- mHandler.cancelShowKeyPreview(this);
+ private void onUpEventInternal() {
+ mTimerProxy.cancelKeyTimers();
mIsInSlidingKeyInput = false;
- final PointerTrackerKeyState keyState = mKeyState;
- final int keyX, keyY;
- if (isMajorEnoughMoveToBeOnNewKey(x, y, keyState.onMoveKey(x, y))) {
- keyX = x;
- keyY = y;
- } else {
- // Use previous fixed key coordinates.
- keyX = keyState.getKeyX();
- keyY = keyState.getKeyY();
- }
- final int keyIndex = keyState.onUpKey(keyX, keyY, eventTime);
- dismissKeyPreview();
- if (updateReleasedKeyGraphics)
- setReleasedKeyGraphics(keyIndex);
+ // Release the last pressed key.
+ setReleasedKeyGraphics(mCurrentKey);
+ if (mIsShowingMoreKeysPanel) {
+ mDrawingProxy.dismissMoreKeysPanel();
+ mIsShowingMoreKeysPanel = false;
+ }
if (mKeyAlreadyProcessed)
return;
- // TODO: Remove this hacking code
- if (mIsInSlidingLanguageSwitch) {
- setReleasedKeyGraphics(mSpaceKeyIndex);
- final int languageDir = ((LatinKeyboard)mKeyboard).getLanguageChangeDirection();
- if (languageDir != 0) {
- final int code = (languageDir == 1)
- ? LatinKeyboard.CODE_NEXT_LANGUAGE : LatinKeyboard.CODE_PREV_LANGUAGE;
- // This will change keyboard layout.
- mListener.onCodeInput(code, new int[] {code}, keyX, keyY);
- }
- mIsInSlidingLanguageSwitch = false;
- ((LatinKeyboard)mKeyboard).setSpacebarSlidingLanguageSwitchDiff(0);
- return;
- }
if (!mIsRepeatableKey) {
- detectAndSendKey(keyIndex, keyX, keyY);
+ detectAndSendKey(mCurrentKey, mKeyX, mKeyY);
}
}
- public void onLongPressed(PointerTrackerQueue queue) {
+ public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) {
+ onLongPressed();
+ onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
+ mIsShowingMoreKeysPanel = true;
+ }
+
+ public void onLongPressed() {
mKeyAlreadyProcessed = true;
+ setReleasedKeyGraphics(mCurrentKey);
+ final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- // TODO: Support chording + long-press input.
- queue.releaseAllPointersExcept(this, SystemClock.uptimeMillis(), true);
queue.remove(this);
}
}
- public void onCancelEvent(int x, int y, long eventTime, PointerTrackerQueue queue) {
- if (ENABLE_ASSERTION) checkAssertion(queue);
+ public void onCancelEvent(int x, int y, long eventTime) {
if (DEBUG_EVENT)
printTouchEvent("onCancelEvt:", x, y, eventTime);
+ final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- queue.releaseAllPointersExcept(this, eventTime, true);
+ queue.releaseAllPointersExcept(this, eventTime);
queue.remove(this);
}
onCancelEventInternal();
}
private void onCancelEventInternal() {
- mHandler.cancelKeyTimers();
- mHandler.cancelShowKeyPreview(this);
- dismissKeyPreview();
- setReleasedKeyGraphics(mKeyState.getKeyIndex());
+ mTimerProxy.cancelKeyTimers();
+ setReleasedKeyGraphics(mCurrentKey);
mIsInSlidingKeyInput = false;
+ if (mIsShowingMoreKeysPanel) {
+ mDrawingProxy.dismissMoreKeysPanel();
+ mIsShowingMoreKeysPanel = false;
+ }
}
- private void startRepeatKey(int keyIndex) {
- final Key key = getKey(keyIndex);
- if (key != null && key.mRepeatable) {
- dismissKeyPreview();
- onRepeatKey(keyIndex);
- mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
+ private void startRepeatKey(Key key) {
+ if (key != null && key.isRepeatable()) {
+ onRegisterKey(key);
+ mTimerProxy.startKeyRepeatTimer(this);
mIsRepeatableKey = true;
} else {
mIsRepeatableKey = false;
}
}
- public void onRepeatKey(int keyIndex) {
- Key key = getKey(keyIndex);
+ public void onRegisterKey(Key key) {
if (key != null) {
- detectAndSendKey(keyIndex, key.mX, key.mY);
+ detectAndSendKey(key, key.mX, key.mY);
+ if (!key.altCodeWhileTyping() && !key.isModifier()) {
+ mTimerProxy.startTypingStateTimer();
+ }
}
}
- public int getLastX() {
- return mKeyState.getLastX();
- }
-
- public int getLastY() {
- return mKeyState.getLastY();
- }
-
- public long getDownTime() {
- return mKeyState.getDownTime();
- }
-
- private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, int newKey) {
- if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
- throw new IllegalStateException("keyboard and/or hysteresis not set");
- int curKey = mKeyState.getKeyIndex();
+ private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) {
+ if (mKeyDetector == null)
+ throw new NullPointerException("keyboard and/or key detector not set");
+ Key curKey = mCurrentKey;
if (newKey == curKey) {
return false;
- } else if (isValidKeyIndex(curKey)) {
- return mKeys.get(curKey).squaredDistanceToEdge(x, y) >= mKeyHysteresisDistanceSquared;
+ } else if (curKey != null) {
+ return curKey.squaredDistanceToEdge(x, y)
+ >= mKeyDetector.getKeyHysteresisDistanceSquared();
} else {
return true;
}
}
- // The modifier key, such as shift key, should not show its key preview.
- private boolean isKeyPreviewNotRequired(int keyIndex) {
- final Key key = getKey(keyIndex);
- if (key == null || !key.isEnabled())
- return true;
- // Such as spacebar sliding language switch.
- if (mKeyboard.needSpacebarPreview(keyIndex))
- return false;
- final int code = key.mCode;
- return isModifierCode(code) || code == Keyboard.CODE_DELETE
- || code == Keyboard.CODE_ENTER || code == Keyboard.CODE_SPACE;
- }
-
- private void showKeyPreview(int keyIndex) {
- if (isKeyPreviewNotRequired(keyIndex))
- return;
- mProxy.showKeyPreview(keyIndex, this);
- }
-
- private void dismissKeyPreview() {
- mProxy.dismissKeyPreview(this);
- }
-
- private void startLongPressTimer(int keyIndex) {
- Key key = getKey(keyIndex);
- if (key.mCode == Keyboard.CODE_SHIFT) {
- mHandler.startLongPressShiftTimer(mLongPressShiftKeyTimeout, keyIndex, this);
- } else if (key.hasUppercaseLetter() && mKeyboard.isManualTemporaryUpperCase()) {
- // We need not start long press timer on the key which has manual temporary upper case
- // code defined and the keyboard is in manual temporary upper case mode.
- return;
- } else if (mKeyboardSwitcher.isInMomentarySwitchState()) {
- // We use longer timeout for sliding finger input started from the symbols mode key.
- mHandler.startLongPressTimer(mLongPressKeyTimeout * 3, keyIndex, this);
- } else {
- mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
+ private void startLongPressTimer(Key key) {
+ if (key != null && key.isLongPressEnabled()) {
+ mTimerProxy.startLongPressTimer(this);
}
}
- private void detectAndSendKey(int index, int x, int y) {
- final Key key = getKey(index);
+ private void detectAndSendKey(Key key, int x, int y) {
if (key == null) {
callListenerOnCancelInput();
return;
}
- if (key.mOutputText != null) {
- callListenerOnTextInput(key);
- callListenerOnRelease(key, key.mCode, false);
- } else {
- int code = key.mCode;
- final int[] codes = mKeyDetector.newCodeArray();
- mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
-
- // If keyboard is in manual temporary upper case state and key has manual temporary
- // uppercase letter as key hint letter, alternate character code should be sent.
- if (mKeyboard.isManualTemporaryUpperCase() && key.hasUppercaseLetter()) {
- code = key.mHintLabel.charAt(0);
- codes[0] = code;
- }
- // Swap the first and second values in the codes array if the primary code is not the
- // first value but the second value in the array. This happens when key debouncing is
- // in effect.
- if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
- codes[1] = codes[0];
- codes[0] = code;
- }
- callListenerOnCodeInput(key, code, codes, x, y);
- callListenerOnRelease(key, code, false);
- }
- }
-
- public CharSequence getPreviewText(Key key) {
- return key.mLabel;
+ int code = key.mCode;
+ callListenerOnCodeInput(key, code, x, y);
+ callListenerOnRelease(key, code, false);
}
private long mPreviousEventTime;
private void printTouchEvent(String title, int x, int y, long eventTime) {
- final int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
- final Key key = getKey(keyIndex);
- final String code = (key == null) ? "----" : keyCodePrintable(key.mCode);
+ final Key key = mKeyDetector.detectHitKey(x, y);
+ final String code = KeyDetector.printableCode(key);
final long delta = eventTime - mPreviousEventTime;
- Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %3d(%s)", title,
- (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, keyIndex, code));
+ Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title,
+ (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, code));
mPreviousEventTime = eventTime;
}
-
- private static String keyCodePrintable(int primaryCode) {
- final String modifier = isModifierCode(primaryCode) ? " modifier" : "";
- return String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode) + modifier;
- }
}
diff --git a/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java b/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java
deleted file mode 100644
index 3b8c36487..000000000
--- a/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2011 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.keyboard;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.SystemClock;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.PopupWindow;
-
-import com.android.inputmethod.latin.R;
-
-/**
- * A view that renders a virtual {@link MiniKeyboard}. It handles rendering of keys and detecting
- * key presses and touch movements.
- */
-public class PopupMiniKeyboardView extends KeyboardView implements PopupPanel {
- private final int[] mCoordinates = new int[2];
- private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
-
- private int mOriginX;
- private int mOriginY;
- private long mDownTime;
-
- public PopupMiniKeyboardView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.popupMiniKeyboardViewStyle);
- }
-
- public PopupMiniKeyboardView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- final Resources res = context.getResources();
- mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
- R.bool.config_show_mini_keyboard_at_touched_point);
- // Override default ProximityKeyDetector.
- mKeyDetector = new MiniKeyboardKeyDetector(res.getDimension(
- R.dimen.mini_keyboard_slide_allowance));
- // Remove gesture detector on mini-keyboard
- mGestureDetector = null;
- setKeyPreviewPopupEnabled(false, 0);
- }
-
- @Override
- public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
- // Mini keyboard needs no pop-up key preview displayed, so we pass always false with a
- // delay of 0. The delay does not matter actually since the popup is not shown anyway.
- super.setKeyPreviewPopupEnabled(false, 0);
- }
-
- @Override
- public void showPanel(KeyboardView parentKeyboardView, Key parentKey,
- PointerTracker tracker, PopupWindow window) {
- final View container = (View)getParent();
- final MiniKeyboard miniKeyboard = (MiniKeyboard)getKeyboard();
- final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
-
- parentKeyboardView.getLocationInWindow(mCoordinates);
- final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
- : parentKey.mX + parentKey.mWidth / 2;
- final int pointY = parentKey.mY;
- final int miniKeyboardLeft = pointX - miniKeyboard.getDefaultCoordX()
- + parentKeyboardView.getPaddingLeft();
- final int x = Math.max(0, Math.min(miniKeyboardLeft,
- parentKeyboardView.getWidth() - miniKeyboard.getMinWidth()))
- - container.getPaddingLeft() + mCoordinates[0];
- final int y = pointY - parentKeyboard.getVerticalGap()
- - (container.getMeasuredHeight() - container.getPaddingBottom())
- + parentKeyboardView.getPaddingTop() + mCoordinates[1];
-
- if (miniKeyboard.setShifted(parentKeyboard.isShiftedOrShiftLocked())) {
- invalidateAllKeys();
- }
- window.setContentView(container);
- window.setWidth(container.getMeasuredWidth());
- window.setHeight(container.getMeasuredHeight());
- window.showAtLocation(parentKeyboardView, Gravity.NO_GRAVITY, x, y);
-
- mOriginX = x + container.getPaddingLeft() - mCoordinates[0];
- mOriginY = y + container.getPaddingTop() - mCoordinates[1];
- mDownTime = SystemClock.uptimeMillis();
-
- // Inject down event on the key to mini keyboard.
- final MotionEvent downEvent = MotionEvent.obtain(mDownTime, mDownTime,
- MotionEvent.ACTION_DOWN, pointX - mOriginX,
- pointY + parentKey.mHeight / 2 - mOriginY, 0);
- onTouchEvent(downEvent);
- downEvent.recycle();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent me) {
- me.offsetLocation(-mOriginX, -mOriginY);
- return super.onTouchEvent(me);
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/PopupPanel.java b/java/src/com/android/inputmethod/keyboard/PopupPanel.java
deleted file mode 100644
index 386e11f2c..000000000
--- a/java/src/com/android/inputmethod/keyboard/PopupPanel.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2011 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.keyboard;
-
-import android.view.MotionEvent;
-import android.widget.PopupWindow;
-
-public interface PopupPanel {
- /**
- * Show popup panel.
- * @param parentKeyboardView the parent KeyboardView that has the parent key.
- * @param parentKey the parent key that is the source of this popup panel
- * @param tracker the pointer tracker that pressesd the parent key
- * @param window PopupWindow to be used to show this popup panel
- */
- public void showPanel(KeyboardView parentKeyboardView, Key parentKey,
- PointerTracker tracker, PopupWindow window);
-
- /**
- * Check if the pointer is in siding key input mode.
- * @return true if the pointer is sliding key input mode.
- */
- public boolean isInSlidingKeyInput();
-
- /**
- * The motion event handler.
- * @param me the MotionEvent to be processed.
- * @return true if the motion event is processed and should be consumed.
- */
- public boolean onTouchEvent(MotionEvent me);
-}
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 33acc6907..1bc825479 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -16,53 +16,162 @@
package com.android.inputmethod.keyboard;
-import com.android.inputmethod.latin.Utils;
+import android.graphics.Rect;
+import android.text.TextUtils;
+import android.util.FloatMath;
+
+import com.android.inputmethod.keyboard.Keyboard.Params.TouchPositionCorrection;
+import com.android.inputmethod.latin.JniUtils;
import java.util.Arrays;
-import java.util.List;
+import java.util.HashMap;
public class ProximityInfo {
+ /** MAX_PROXIMITY_CHARS_SIZE must be the same as MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+ * in defines.h */
public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
+ /** Number of key widths from current touch point to search for nearest keys. */
+ private static float SEARCH_DISTANCE = 1.2f;
+ private static final Key[] EMPTY_KEY_ARRAY = new Key[0];
+ private final int mKeyHeight;
private final int mGridWidth;
private final int mGridHeight;
private final int mGridSize;
+ private final int mCellWidth;
+ private final int mCellHeight;
+ // TODO: Find a proper name for mKeyboardMinWidth
+ private final int mKeyboardMinWidth;
+ private final int mKeyboardHeight;
+ private final int mMostCommonKeyWidth;
+ private final Key[] mKeys;
+ private final TouchPositionCorrection mTouchPositionCorrection;
+ private final Key[][] mGridNeighbors;
+ private final String mLocaleStr;
- ProximityInfo(int gridWidth, int gridHeight) {
+ ProximityInfo(String localeStr, int gridWidth, int gridHeight, int minWidth, int height,
+ int mostCommonKeyWidth, int mostCommonKeyHeight, final Key[] keys,
+ TouchPositionCorrection touchPositionCorrection) {
+ if (TextUtils.isEmpty(localeStr)) {
+ mLocaleStr = "";
+ } else {
+ mLocaleStr = localeStr;
+ }
mGridWidth = gridWidth;
mGridHeight = gridHeight;
mGridSize = mGridWidth * mGridHeight;
+ mCellWidth = (minWidth + mGridWidth - 1) / mGridWidth;
+ mCellHeight = (height + mGridHeight - 1) / mGridHeight;
+ mKeyboardMinWidth = minWidth;
+ mKeyboardHeight = height;
+ mKeyHeight = mostCommonKeyHeight;
+ mMostCommonKeyWidth = mostCommonKeyWidth;
+ mKeys = keys;
+ mTouchPositionCorrection = touchPositionCorrection;
+ mGridNeighbors = new Key[mGridSize][];
+ if (minWidth == 0 || height == 0) {
+ // No proximity required. Keyboard might be more keys keyboard.
+ return;
+ }
+ computeNearestNeighbors();
+ mNativeProximityInfo = createNativeProximityInfo();
+ }
+
+ public static ProximityInfo createDummyProximityInfo() {
+ return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
}
- private int mNativeProximityInfo;
+ public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity,
+ int rowSize, int gridWidth, int gridHeight) {
+ final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
+ spellCheckerProximityInfo.mNativeProximityInfo =
+ spellCheckerProximityInfo.setProximityInfoNative("",
+ rowSize, gridWidth, gridHeight, gridWidth, gridHeight,
+ 1, proximity, 0, null, null, null, null, null, null, null, null);
+ return spellCheckerProximityInfo;
+ }
+
+ private long mNativeProximityInfo;
static {
- Utils.loadNativeLibrary();
+ JniUtils.loadNativeLibrary();
}
- private native int setProximityInfoNative(int maxProximityCharsSize, int displayWidth,
- int displayHeight, int gridWidth, int gridHeight, int[] proximityCharsArray);
- private native void releaseProximityInfoNative(int nativeProximityInfo);
- public final void setProximityInfo(int[][] gridNeighborKeyIndexes, int keyboardWidth,
- int keyboardHeight, List<Key> keys) {
- int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
+ private native long setProximityInfoNative(
+ String locale, int maxProximityCharsSize, int displayWidth,
+ int displayHeight, int gridWidth, int gridHeight,
+ int mostCommonKeyWidth, int[] proximityCharsArray,
+ int keyCount, int[] keyXCoordinates, int[] keyYCoordinates,
+ int[] keyWidths, int[] keyHeights, int[] keyCharCodes,
+ float[] sweetSpotCenterX, float[] sweetSpotCenterY, float[] sweetSpotRadii);
+
+ private native void releaseProximityInfoNative(long nativeProximityInfo);
+
+ private final long createNativeProximityInfo() {
+ final Key[][] gridNeighborKeys = mGridNeighbors;
+ final int keyboardWidth = mKeyboardMinWidth;
+ final int keyboardHeight = mKeyboardHeight;
+ final Key[] keys = mKeys;
+ final TouchPositionCorrection touchPositionCorrection = mTouchPositionCorrection;
+ final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
Arrays.fill(proximityCharsArray, KeyDetector.NOT_A_CODE);
for (int i = 0; i < mGridSize; ++i) {
- final int proximityCharsLength = gridNeighborKeyIndexes[i].length;
+ final int proximityCharsLength = gridNeighborKeys[i].length;
for (int j = 0; j < proximityCharsLength; ++j) {
proximityCharsArray[i * MAX_PROXIMITY_CHARS_SIZE + j] =
- keys.get(gridNeighborKeyIndexes[i][j]).mCode;
+ gridNeighborKeys[i][j].mCode;
}
}
- mNativeProximityInfo = setProximityInfoNative(MAX_PROXIMITY_CHARS_SIZE,
- keyboardWidth, keyboardHeight, mGridWidth, mGridHeight, proximityCharsArray);
- }
+ final int keyCount = keys.length;
+ final int[] keyXCoordinates = new int[keyCount];
+ final int[] keyYCoordinates = new int[keyCount];
+ final int[] keyWidths = new int[keyCount];
+ final int[] keyHeights = new int[keyCount];
+ final int[] keyCharCodes = new int[keyCount];
+ final float[] sweetSpotCenterXs;
+ final float[] sweetSpotCenterYs;
+ final float[] sweetSpotRadii;
+
+ for (int i = 0; i < keyCount; ++i) {
+ final Key key = keys[i];
+ keyXCoordinates[i] = key.mX;
+ keyYCoordinates[i] = key.mY;
+ keyWidths[i] = key.mWidth;
+ keyHeights[i] = key.mHeight;
+ keyCharCodes[i] = key.mCode;
+ }
- // TODO: Get rid of this function's input (keyboard).
- public int getNativeProximityInfo(Keyboard keyboard) {
- if (mNativeProximityInfo == 0) {
- // TODO: Move this function to ProximityInfo and make this private.
- keyboard.computeNearestNeighbors();
+ if (touchPositionCorrection != null && touchPositionCorrection.isValid()) {
+ sweetSpotCenterXs = new float[keyCount];
+ sweetSpotCenterYs = new float[keyCount];
+ sweetSpotRadii = new float[keyCount];
+ for (int i = 0; i < keyCount; i++) {
+ final Key key = keys[i];
+ final Rect hitBox = key.mHitBox;
+ final int row = hitBox.top / mKeyHeight;
+ if (row < touchPositionCorrection.mRadii.length) {
+ final int hitBoxWidth = hitBox.width();
+ final int hitBoxHeight = hitBox.height();
+ final float x = touchPositionCorrection.mXs[row];
+ final float y = touchPositionCorrection.mYs[row];
+ final float radius = touchPositionCorrection.mRadii[row];
+ sweetSpotCenterXs[i] = hitBox.exactCenterX() + x * hitBoxWidth;
+ sweetSpotCenterYs[i] = hitBox.exactCenterY() + y * hitBoxHeight;
+ sweetSpotRadii[i] = radius * FloatMath.sqrt(
+ hitBoxWidth * hitBoxWidth + hitBoxHeight * hitBoxHeight);
+ }
+ }
+ } else {
+ sweetSpotCenterXs = sweetSpotCenterYs = sweetSpotRadii = null;
}
+
+ return setProximityInfoNative(mLocaleStr, MAX_PROXIMITY_CHARS_SIZE,
+ keyboardWidth, keyboardHeight, mGridWidth, mGridHeight, mMostCommonKeyWidth,
+ proximityCharsArray,
+ keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
+ sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
+ }
+
+ public long getNativeProximityInfo() {
return mNativeProximityInfo;
}
@@ -77,4 +186,72 @@ public class ProximityInfo {
super.finalize();
}
}
+
+ private void computeNearestNeighbors() {
+ final int defaultWidth = mMostCommonKeyWidth;
+ final Key[] keys = mKeys;
+ final HashMap<Integer, Key> keyCodeMap = new HashMap<Integer, Key>();
+ for (final Key key : keys) {
+ keyCodeMap.put(key.mCode, key);
+ }
+ final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
+ final int threshold = thresholdBase * thresholdBase;
+ // Round-up so we don't have any pixels outside the grid
+ final Key[] neighborKeys = new Key[keys.length];
+ final int gridWidth = mGridWidth * mCellWidth;
+ final int gridHeight = mGridHeight * mCellHeight;
+ for (int x = 0; x < gridWidth; x += mCellWidth) {
+ for (int y = 0; y < gridHeight; y += mCellHeight) {
+ final int centerX = x + mCellWidth / 2;
+ final int centerY = y + mCellHeight / 2;
+ int count = 0;
+ for (final Key key : keys) {
+ if (key.isSpacer()) continue;
+ if (key.squaredDistanceToEdge(centerX, centerY) < threshold) {
+ neighborKeys[count++] = key;
+ }
+ }
+ mGridNeighbors[(y / mCellHeight) * mGridWidth + (x / mCellWidth)] =
+ Arrays.copyOfRange(neighborKeys, 0, count);
+ }
+ }
+ }
+
+ public void fillArrayWithNearestKeyCodes(int x, int y, int primaryKeyCode, int[] dest) {
+ final int destLength = dest.length;
+ if (destLength < 1) {
+ return;
+ }
+ int index = 0;
+ if (primaryKeyCode > Keyboard.CODE_SPACE) {
+ dest[index++] = primaryKeyCode;
+ }
+ final Key[] nearestKeys = getNearestKeys(x, y);
+ for (Key key : nearestKeys) {
+ if (index >= destLength) {
+ break;
+ }
+ final int code = key.mCode;
+ if (code <= Keyboard.CODE_SPACE) {
+ break;
+ }
+ dest[index++] = code;
+ }
+ if (index < destLength) {
+ dest[index] = KeyDetector.NOT_A_CODE;
+ }
+ }
+
+ public Key[] getNearestKeys(int x, int y) {
+ if (mGridNeighbors == null) {
+ return EMPTY_KEY_ARRAY;
+ }
+ if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) {
+ int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth);
+ if (index < mGridSize) {
+ return mGridNeighbors[index];
+ }
+ }
+ return EMPTY_KEY_ARRAY;
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
new file mode 100644
index 000000000..107138395
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011 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.keyboard;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+public class SuddenJumpingTouchEventHandler {
+ private static final String TAG = SuddenJumpingTouchEventHandler.class.getSimpleName();
+ private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
+
+ public interface ProcessMotionEvent {
+ public boolean processMotionEvent(MotionEvent me);
+ }
+
+ private final ProcessMotionEvent mView;
+ private final boolean mNeedsSuddenJumpingHack;
+
+ /** Whether we've started dropping move events because we found a big jump */
+ private boolean mDroppingEvents;
+ /**
+ * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
+ * occured
+ */
+ private boolean mDisableDisambiguation;
+ /** The distance threshold at which we start treating the touch session as a multi-touch */
+ private int mJumpThresholdSquare = Integer.MAX_VALUE;
+ private int mLastX;
+ private int mLastY;
+
+ public SuddenJumpingTouchEventHandler(Context context, ProcessMotionEvent view) {
+ mView = view;
+ mNeedsSuddenJumpingHack = Boolean.parseBoolean(Utils.getDeviceOverrideValue(
+ context.getResources(), R.array.sudden_jumping_touch_event_device_list, "false"));
+ }
+
+ public void setKeyboard(Keyboard newKeyboard) {
+ // One-seventh of the keyboard width seems like a reasonable threshold
+ final int jumpThreshold = newKeyboard.mOccupiedWidth / 7;
+ mJumpThresholdSquare = jumpThreshold * jumpThreshold;
+ }
+
+ /**
+ * This function checks to see if we need to handle any sudden jumps in the pointer location
+ * that could be due to a multi-touch being treated as a move by the firmware or hardware.
+ * Once a sudden jump is detected, all subsequent move events are discarded
+ * until an UP is received.<P>
+ * When a sudden jump is detected, an UP event is simulated at the last position and when
+ * the sudden moves subside, a DOWN event is simulated for the second key.
+ * @param me the motion event
+ * @return true if the event was consumed, so that it doesn't continue to be handled by
+ * {@link LatinKeyboardView}.
+ */
+ private boolean handleSuddenJumping(MotionEvent me) {
+ if (!mNeedsSuddenJumpingHack)
+ return false;
+ final int action = me.getAction();
+ final int x = (int) me.getX();
+ final int y = (int) me.getY();
+ boolean result = false;
+
+ // Real multi-touch event? Stop looking for sudden jumps
+ if (me.getPointerCount() > 1) {
+ mDisableDisambiguation = true;
+ }
+ if (mDisableDisambiguation) {
+ // If UP, reset the multi-touch flag
+ if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
+ return false;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ // Reset the "session"
+ mDroppingEvents = false;
+ mDisableDisambiguation = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // Is this a big jump?
+ final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
+ // Check the distance.
+ if (distanceSquare > mJumpThresholdSquare) {
+ // If we're not yet dropping events, start dropping and send an UP event
+ if (!mDroppingEvents) {
+ mDroppingEvents = true;
+ // Send an up event
+ MotionEvent translated = MotionEvent.obtain(
+ me.getEventTime(), me.getEventTime(),
+ MotionEvent.ACTION_UP,
+ mLastX, mLastY, me.getMetaState());
+ mView.processMotionEvent(translated);
+ translated.recycle();
+ }
+ result = true;
+ } else if (mDroppingEvents) {
+ // If moves are small and we're already dropping events, continue dropping
+ result = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mDroppingEvents) {
+ // Send a down event first, as we dropped a bunch of sudden jumps and assume that
+ // the user is releasing the touch on the second key.
+ MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
+ MotionEvent.ACTION_DOWN,
+ x, y, me.getMetaState());
+ mView.processMotionEvent(translated);
+ translated.recycle();
+ mDroppingEvents = false;
+ // Let the up event get processed as well, result = false
+ }
+ break;
+ }
+ // Track the previous coordinate
+ mLastX = x;
+ mLastY = y;
+ return result;
+ }
+
+ public boolean onTouchEvent(MotionEvent me) {
+ // If there was a sudden jump, return without processing the actual motion event.
+ if (handleSuddenJumping(me)) {
+ if (DEBUG_MODE)
+ Log.w(TAG, "onTouchEvent: ignore sudden jump " + me);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.suddenJumpingTouchEventHandler_onTouchEvent(me);
+ }
+ return true;
+ }
+ return mView.processMotionEvent(me);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
index 523bf7d0e..ee5047083 100644
--- a/java/src/com/android/inputmethod/compat/FrameLayoutCompatUtils.java
+++ b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.compat;
+package com.android.inputmethod.keyboard;
import android.view.View;
import android.view.ViewGroup;
@@ -22,20 +22,9 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
-public class FrameLayoutCompatUtils {
- private static final boolean NEEDS_FRAME_LAYOUT_HACK = (
- android.os.Build.VERSION.SDK_INT < 11 /* Honeycomb */);
-
- public static ViewGroup getPlacer(ViewGroup container) {
- if (NEEDS_FRAME_LAYOUT_HACK) {
- // Insert RelativeLayout to be able to setMargin because pre-Honeycomb FrameLayout
- // could not handle setMargin properly.
- final ViewGroup placer = new RelativeLayout(container.getContext());
- container.addView(placer);
- return placer;
- } else {
- return container;
- }
+public class ViewLayoutUtils {
+ private ViewLayoutUtils() {
+ // This utility class is not publicly instantiable.
}
public static MarginLayoutParams newLayoutParam(ViewGroup placer, int width, int height) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java b/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java
index 0cde4e5b5..5712df1fc 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardShiftState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java
@@ -18,29 +18,27 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
+public class AlphabetShiftState {
+ private static final String TAG = AlphabetShiftState.class.getSimpleName();
+ private static final boolean DEBUG = false;
-public class KeyboardShiftState {
- private static final String TAG = "KeyboardShiftState";
- private static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
-
- private static final int NORMAL = 0;
+ private static final int UNSHIFTED = 0;
private static final int MANUAL_SHIFTED = 1;
private static final int MANUAL_SHIFTED_FROM_AUTO = 2;
- private static final int AUTO_SHIFTED = 3;
+ private static final int AUTOMATIC_SHIFTED = 3;
private static final int SHIFT_LOCKED = 4;
private static final int SHIFT_LOCK_SHIFTED = 5;
- private int mState = NORMAL;
+ private int mState = UNSHIFTED;
- public boolean setShifted(boolean newShiftState) {
+ public void setShifted(boolean newShiftState) {
final int oldState = mState;
if (newShiftState) {
switch (oldState) {
- case NORMAL:
+ case UNSHIFTED:
mState = MANUAL_SHIFTED;
break;
- case AUTO_SHIFTED:
+ case AUTOMATIC_SHIFTED:
mState = MANUAL_SHIFTED_FROM_AUTO;
break;
case SHIFT_LOCKED:
@@ -51,8 +49,8 @@ public class KeyboardShiftState {
switch (oldState) {
case MANUAL_SHIFTED:
case MANUAL_SHIFTED_FROM_AUTO:
- case AUTO_SHIFTED:
- mState = NORMAL;
+ case AUTOMATIC_SHIFTED:
+ mState = UNSHIFTED;
break;
case SHIFT_LOCK_SHIFTED:
mState = SHIFT_LOCKED;
@@ -61,58 +59,56 @@ public class KeyboardShiftState {
}
if (DEBUG)
Log.d(TAG, "setShifted(" + newShiftState + "): " + toString(oldState) + " > " + this);
- return mState != oldState;
}
public void setShiftLocked(boolean newShiftLockState) {
final int oldState = mState;
if (newShiftLockState) {
switch (oldState) {
- case NORMAL:
+ case UNSHIFTED:
case MANUAL_SHIFTED:
case MANUAL_SHIFTED_FROM_AUTO:
- case AUTO_SHIFTED:
+ case AUTOMATIC_SHIFTED:
mState = SHIFT_LOCKED;
break;
}
} else {
- switch (oldState) {
- case SHIFT_LOCKED:
- case SHIFT_LOCK_SHIFTED:
- mState = NORMAL;
- break;
- }
+ mState = UNSHIFTED;
}
if (DEBUG)
Log.d(TAG, "setShiftLocked(" + newShiftLockState + "): " + toString(oldState)
+ " > " + this);
}
- public void setAutomaticTemporaryUpperCase() {
+ public void setAutomaticShifted() {
final int oldState = mState;
- mState = AUTO_SHIFTED;
+ mState = AUTOMATIC_SHIFTED;
if (DEBUG)
- Log.d(TAG, "setAutomaticTemporaryUpperCase: " + toString(oldState) + " > " + this);
+ Log.d(TAG, "setAutomaticShifted: " + toString(oldState) + " > " + this);
}
public boolean isShiftedOrShiftLocked() {
- return mState != NORMAL;
+ return mState != UNSHIFTED;
}
public boolean isShiftLocked() {
return mState == SHIFT_LOCKED || mState == SHIFT_LOCK_SHIFTED;
}
- public boolean isAutomaticTemporaryUpperCase() {
- return mState == AUTO_SHIFTED;
+ public boolean isShiftLockShifted() {
+ return mState == SHIFT_LOCK_SHIFTED;
+ }
+
+ public boolean isAutomaticShifted() {
+ return mState == AUTOMATIC_SHIFTED;
}
- public boolean isManualTemporaryUpperCase() {
+ public boolean isManualShifted() {
return mState == MANUAL_SHIFTED || mState == MANUAL_SHIFTED_FROM_AUTO
|| mState == SHIFT_LOCK_SHIFTED;
}
- public boolean isManualTemporaryUpperCaseFromAuto() {
+ public boolean isManualShiftedFromAutomaticShifted() {
return mState == MANUAL_SHIFTED_FROM_AUTO;
}
@@ -123,13 +119,13 @@ public class KeyboardShiftState {
private static String toString(int state) {
switch (state) {
- case NORMAL: return "NORMAL";
+ case UNSHIFTED: return "UNSHIFTED";
case MANUAL_SHIFTED: return "MANUAL_SHIFTED";
case MANUAL_SHIFTED_FROM_AUTO: return "MANUAL_SHIFTED_FROM_AUTO";
- case AUTO_SHIFTED: return "AUTO_SHIFTED";
+ case AUTOMATIC_SHIFTED: return "AUTOMATIC_SHIFTED";
case SHIFT_LOCKED: return "SHIFT_LOCKED";
case SHIFT_LOCK_SHIFTED: return "SHIFT_LOCK_SHIFTED";
- default: return "UKNOWN";
+ default: return "UNKNOWN";
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
new file mode 100644
index 000000000..c4452a5f5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -0,0 +1,498 @@
+/*
+ * Copyright (C) 2010 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.keyboard.internal;
+
+import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * The string parser of more keys specification.
+ * The specification is comma separated texts each of which represents one "more key".
+ * The specification might have label or string resource reference in it. These references are
+ * expanded before parsing comma.
+ * - Label reference should be a string representation of label (!text/label_name)
+ * - String resource reference should be a string representation of resource (!text/resource_name)
+ * Each "more key" specification is one of the following:
+ * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
+ * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name)
+ * - Icon should be a string representation of icon (!icon/icon_name).
+ * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string
+ * representation of code (!code/code_name).
+ * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
+ * Note that the '\' is also parsed by XML parser and CSV parser as well.
+ * See {@link KeyboardIconsSet} about icon_name.
+ */
+public class KeySpecParser {
+ private static final boolean DEBUG = LatinImeLogger.sDBG;
+
+ private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+
+ // Constants for parsing.
+ private static int COMMA = ',';
+ private static final char ESCAPE_CHAR = '\\';
+ private static final char LABEL_END = '|';
+ private static final String PREFIX_TEXT = "!text/";
+ private static final String PREFIX_ICON = "!icon/";
+ private static final String PREFIX_CODE = "!code/";
+ private static final String PREFIX_HEX = "0x";
+ private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
+
+ public static class MoreKeySpec {
+ public final int mCode;
+ public final String mLabel;
+ public final String mOutputText;
+ public final int mIconId;
+
+ public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale,
+ final KeyboardCodesSet codesSet) {
+ mCode = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet),
+ needsToUpperCase, locale);
+ mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec),
+ needsToUpperCase, locale);
+ mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec),
+ needsToUpperCase, locale);
+ mIconId = getIconId(moreKeySpec);
+ }
+ }
+
+ private KeySpecParser() {
+ // Intentional empty constructor for utility class.
+ }
+
+ private static boolean hasIcon(String moreKeySpec) {
+ return moreKeySpec.startsWith(PREFIX_ICON);
+ }
+
+ private static boolean hasCode(String moreKeySpec) {
+ final int end = indexOfLabelEnd(moreKeySpec, 0);
+ if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
+ PREFIX_CODE, end + 1)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String parseEscape(String text) {
+ if (text.indexOf(ESCAPE_CHAR) < 0) {
+ return text;
+ }
+ final int length = text.length();
+ final StringBuilder sb = new StringBuilder();
+ for (int pos = 0; pos < length; pos++) {
+ final char c = text.charAt(pos);
+ if (c == ESCAPE_CHAR && pos + 1 < length) {
+ // Skip escape char
+ pos++;
+ sb.append(text.charAt(pos));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static int indexOfLabelEnd(String moreKeySpec, int start) {
+ if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) {
+ final int end = moreKeySpec.indexOf(LABEL_END, start);
+ if (end == 0) {
+ throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
+ }
+ return end;
+ }
+ final int length = moreKeySpec.length();
+ for (int pos = start; pos < length; pos++) {
+ final char c = moreKeySpec.charAt(pos);
+ if (c == ESCAPE_CHAR && pos + 1 < length) {
+ // Skip escape char
+ pos++;
+ } else if (c == LABEL_END) {
+ return pos;
+ }
+ }
+ return -1;
+ }
+
+ public static String getLabel(String moreKeySpec) {
+ if (hasIcon(moreKeySpec)) {
+ return null;
+ }
+ final int end = indexOfLabelEnd(moreKeySpec, 0);
+ final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
+ : parseEscape(moreKeySpec);
+ if (TextUtils.isEmpty(label)) {
+ throw new KeySpecParserError("Empty label: " + moreKeySpec);
+ }
+ return label;
+ }
+
+ private static String getOutputTextInternal(String moreKeySpec) {
+ final int end = indexOfLabelEnd(moreKeySpec, 0);
+ if (end <= 0) {
+ return null;
+ }
+ if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
+ throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+ }
+ return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
+ }
+
+ static String getOutputText(String moreKeySpec) {
+ if (hasCode(moreKeySpec)) {
+ return null;
+ }
+ final String outputText = getOutputTextInternal(moreKeySpec);
+ if (outputText != null) {
+ if (StringUtils.codePointCount(outputText) == 1) {
+ // If output text is one code point, it should be treated as a code.
+ // See {@link #getCode(Resources, String)}.
+ return null;
+ }
+ if (!TextUtils.isEmpty(outputText)) {
+ return outputText;
+ }
+ throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
+ }
+ final String label = getLabel(moreKeySpec);
+ if (label == null) {
+ throw new KeySpecParserError("Empty label: " + moreKeySpec);
+ }
+ // Code is automatically generated for one letter label. See {@link getCode()}.
+ return (StringUtils.codePointCount(label) == 1) ? null : label;
+ }
+
+ static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) {
+ if (hasCode(moreKeySpec)) {
+ final int end = indexOfLabelEnd(moreKeySpec, 0);
+ if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
+ throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+ }
+ return parseCode(moreKeySpec.substring(end + 1), codesSet, Keyboard.CODE_UNSPECIFIED);
+ }
+ final String outputText = getOutputTextInternal(moreKeySpec);
+ if (outputText != null) {
+ // If output text is one code point, it should be treated as a code.
+ // See {@link #getOutputText(String)}.
+ if (StringUtils.codePointCount(outputText) == 1) {
+ return outputText.codePointAt(0);
+ }
+ return Keyboard.CODE_OUTPUT_TEXT;
+ }
+ final String label = getLabel(moreKeySpec);
+ // Code is automatically generated for one letter label.
+ if (StringUtils.codePointCount(label) == 1) {
+ return label.codePointAt(0);
+ }
+ return Keyboard.CODE_OUTPUT_TEXT;
+ }
+
+ public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) {
+ if (text == null) return defCode;
+ if (text.startsWith(PREFIX_CODE)) {
+ return codesSet.getCode(text.substring(PREFIX_CODE.length()));
+ } else if (text.startsWith(PREFIX_HEX)) {
+ return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
+ } else {
+ return Integer.parseInt(text);
+ }
+ }
+
+ public static int getIconId(String moreKeySpec) {
+ if (moreKeySpec != null && hasIcon(moreKeySpec)) {
+ final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
+ final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
+ : moreKeySpec.substring(PREFIX_ICON.length(), end);
+ return KeyboardIconsSet.getIconId(name);
+ }
+ return KeyboardIconsSet.ICON_UNDEFINED;
+ }
+
+ private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) {
+ if (array == null) {
+ throw new NullPointerException();
+ }
+ if (start < 0 || start > end || end > array.length) {
+ throw new IllegalArgumentException();
+ }
+
+ final ArrayList<T> list = new ArrayList<T>(end - start);
+ for (int i = start; i < end; i++) {
+ list.add(array[i]);
+ }
+ return list;
+ }
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static String[] filterOutEmptyString(String[] array) {
+ if (array == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ ArrayList<String> out = null;
+ for (int i = 0; i < array.length; i++) {
+ final String entry = array[i];
+ if (TextUtils.isEmpty(entry)) {
+ if (out == null) {
+ out = arrayAsList(array, 0, i);
+ }
+ } else if (out != null) {
+ out.add(entry);
+ }
+ }
+ if (out == null) {
+ return array;
+ }
+ return out.toArray(new String[out.size()]);
+ }
+
+ public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs,
+ String[] additionalMoreKeySpecs) {
+ final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
+ final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
+ final int moreKeysCount = moreKeys.length;
+ final int additionalCount = additionalMoreKeys.length;
+ ArrayList<String> out = null;
+ int additionalIndex = 0;
+ for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
+ final String moreKeySpec = moreKeys[moreKeyIndex];
+ if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
+ if (additionalIndex < additionalCount) {
+ // Replace '%' marker with additional more key specification.
+ final String additionalMoreKey = additionalMoreKeys[additionalIndex];
+ if (out != null) {
+ out.add(additionalMoreKey);
+ } else {
+ moreKeys[moreKeyIndex] = additionalMoreKey;
+ }
+ additionalIndex++;
+ } else {
+ // Filter out excessive '%' marker.
+ if (out == null) {
+ out = arrayAsList(moreKeys, 0, moreKeyIndex);
+ }
+ }
+ } else {
+ if (out != null) {
+ out.add(moreKeySpec);
+ }
+ }
+ }
+ if (additionalCount > 0 && additionalIndex == 0) {
+ // No '%' marker is found in more keys.
+ // Insert all additional more keys to the head of more keys.
+ if (DEBUG && out != null) {
+ throw new RuntimeException("Internal logic error:"
+ + " moreKeys=" + Arrays.toString(moreKeys)
+ + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+ }
+ out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
+ for (int i = 0; i < moreKeysCount; i++) {
+ out.add(moreKeys[i]);
+ }
+ } else if (additionalIndex < additionalCount) {
+ // The number of '%' markers are less than additional more keys.
+ // Append remained additional more keys to the tail of more keys.
+ if (DEBUG && out != null) {
+ throw new RuntimeException("Internal logic error:"
+ + " moreKeys=" + Arrays.toString(moreKeys)
+ + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+ }
+ out = arrayAsList(moreKeys, 0, moreKeysCount);
+ for (int i = additionalIndex; i < additionalCount; i++) {
+ out.add(additionalMoreKeys[additionalIndex]);
+ }
+ }
+ if (out == null && moreKeysCount > 0) {
+ return moreKeys;
+ } else if (out != null && out.size() > 0) {
+ return out.toArray(new String[out.size()]);
+ } else {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class KeySpecParserError extends RuntimeException {
+ public KeySpecParserError(String message) {
+ super(message);
+ }
+ }
+
+ public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) {
+ int level = 0;
+ String text = rawText;
+ StringBuilder sb;
+ do {
+ level++;
+ if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
+ throw new RuntimeException("too many @string/resource indirection: " + text);
+ }
+
+ final int prefixLen = PREFIX_TEXT.length();
+ final int size = text.length();
+ if (size < prefixLen) {
+ return text;
+ }
+
+ sb = null;
+ for (int pos = 0; pos < size; pos++) {
+ final char c = text.charAt(pos);
+ if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) {
+ if (sb == null) {
+ sb = new StringBuilder(text.substring(0, pos));
+ }
+ final int end = searchTextNameEnd(text, pos + prefixLen);
+ final String name = text.substring(pos + prefixLen, end);
+ sb.append(textsSet.getText(name));
+ pos = end - 1;
+ } else if (c == ESCAPE_CHAR) {
+ if (sb != null) {
+ // Append both escape character and escaped character.
+ sb.append(text.substring(pos, Math.min(pos + 2, size)));
+ }
+ pos++;
+ } else if (sb != null) {
+ sb.append(c);
+ }
+ }
+
+ if (sb != null) {
+ text = sb.toString();
+ }
+ } while (sb != null);
+
+ return text;
+ }
+
+ private static int searchTextNameEnd(String text, int start) {
+ final int size = text.length();
+ for (int pos = start; pos < size; pos++) {
+ final char c = text.charAt(pos);
+ // Label name should be consisted of [a-zA-Z_0-9].
+ if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+ continue;
+ }
+ return pos;
+ }
+ return size;
+ }
+
+ public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) {
+ final String text = resolveTextReference(rawText, textsSet);
+ final int size = text.length();
+ if (size == 0) {
+ return null;
+ }
+ if (StringUtils.codePointCount(text) == 1) {
+ return text.codePointAt(0) == COMMA ? null : new String[] { text };
+ }
+
+ ArrayList<String> list = null;
+ int start = 0;
+ for (int pos = 0; pos < size; pos++) {
+ final char c = text.charAt(pos);
+ if (c == COMMA) {
+ // Skip empty entry.
+ if (pos - start > 0) {
+ if (list == null) {
+ list = new ArrayList<String>();
+ }
+ list.add(text.substring(start, pos));
+ }
+ // Skip comma
+ start = pos + 1;
+ } else if (c == ESCAPE_CHAR) {
+ // Skip escape character and escaped character.
+ pos++;
+ }
+ }
+ final String remain = (size - start > 0) ? text.substring(start) : null;
+ if (list == null) {
+ return remain != null ? new String[] { remain } : null;
+ }
+ if (remain != null) {
+ list.add(remain);
+ }
+ return list.toArray(new String[list.size()]);
+ }
+
+ public static int getIntValue(String[] moreKeys, String key, int defaultValue) {
+ if (moreKeys == null) {
+ return defaultValue;
+ }
+ final int keyLen = key.length();
+ boolean foundValue = false;
+ int value = defaultValue;
+ for (int i = 0; i < moreKeys.length; i++) {
+ final String moreKeySpec = moreKeys[i];
+ if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
+ continue;
+ }
+ moreKeys[i] = null;
+ try {
+ if (!foundValue) {
+ value = Integer.parseInt(moreKeySpec.substring(keyLen));
+ foundValue = true;
+ }
+ } catch (NumberFormatException e) {
+ throw new RuntimeException(
+ "integer should follow after " + key + ": " + moreKeySpec);
+ }
+ }
+ return value;
+ }
+
+ public static boolean getBooleanValue(String[] moreKeys, String key) {
+ if (moreKeys == null) {
+ return false;
+ }
+ boolean value = false;
+ for (int i = 0; i < moreKeys.length; i++) {
+ final String moreKeySpec = moreKeys[i];
+ if (moreKeySpec == null || !moreKeySpec.equals(key)) {
+ continue;
+ }
+ moreKeys[i] = null;
+ value = true;
+ }
+ return value;
+ }
+
+ public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase,
+ Locale locale) {
+ if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code;
+ final String text = new String(new int[] { code } , 0, 1);
+ final String casedText = KeySpecParser.toUpperCaseOfStringForLocale(
+ text, needsToUpperCase, locale);
+ return StringUtils.codePointCount(casedText) == 1
+ ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
+ }
+
+ public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase,
+ Locale locale) {
+ if (text == null || !needsToUpperCase) return text;
+ return text.toUpperCase(locale);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
index 30d9692a8..80f4f259b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -17,225 +17,208 @@
package com.android.inputmethod.keyboard.internal;
import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
import android.util.Log;
-import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException;
+import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.XmlParseUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
-import java.util.ArrayList;
import java.util.HashMap;
public class KeyStyles {
- private static final String TAG = "KeyStyles";
+ private static final String TAG = KeyStyles.class.getSimpleName();
private static final boolean DEBUG = false;
- private final HashMap<String, DeclaredKeyStyle> mStyles =
- new HashMap<String, DeclaredKeyStyle>();
- private static final KeyStyle EMPTY_KEY_STYLE = new EmptyKeyStyle();
+ final HashMap<String, KeyStyle> mStyles = new HashMap<String, KeyStyle>();
+
+ final KeyboardTextsSet mTextsSet;
+ private final KeyStyle mEmptyKeyStyle;
+ private static final String EMPTY_STYLE_NAME = "<empty>";
- public interface KeyStyle {
- public CharSequence[] getTextArray(TypedArray a, int index);
- public CharSequence getText(TypedArray a, int index);
- public int getInt(TypedArray a, int index, int defaultValue);
- public int getFlag(TypedArray a, int index, int defaultValue);
- public boolean getBoolean(TypedArray a, int index, boolean defaultValue);
+ public KeyStyles(KeyboardTextsSet textsSet) {
+ mTextsSet = textsSet;
+ mEmptyKeyStyle = new EmptyKeyStyle();
+ mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle);
}
- /* package */ static class EmptyKeyStyle implements KeyStyle {
- private EmptyKeyStyle() {
- // Nothing to do.
+ public abstract class KeyStyle {
+ public abstract String[] getStringArray(TypedArray a, int index);
+ public abstract String getString(TypedArray a, int index);
+ public abstract int getInt(TypedArray a, int index, int defaultValue);
+ public abstract int getFlag(TypedArray a, int index);
+
+ protected String parseString(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
+ }
+ return null;
}
- @Override
- public CharSequence[] getTextArray(TypedArray a, int index) {
- return parseTextArray(a, index);
+ protected String[] parseStringArray(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ return KeySpecParser.parseCsvString(a.getString(index), mTextsSet);
+ }
+ return null;
}
+ }
+ class EmptyKeyStyle extends KeyStyle {
@Override
- public CharSequence getText(TypedArray a, int index) {
- return a.getText(index);
+ public String[] getStringArray(TypedArray a, int index) {
+ return parseStringArray(a, index);
}
@Override
- public int getInt(TypedArray a, int index, int defaultValue) {
- return a.getInt(index, defaultValue);
+ public String getString(TypedArray a, int index) {
+ return parseString(a, index);
}
@Override
- public int getFlag(TypedArray a, int index, int defaultValue) {
+ public int getInt(TypedArray a, int index, int defaultValue) {
return a.getInt(index, defaultValue);
}
@Override
- public boolean getBoolean(TypedArray a, int index, boolean defaultValue) {
- return a.getBoolean(index, defaultValue);
- }
-
- protected static CharSequence[] parseTextArray(TypedArray a, int index) {
- if (!a.hasValue(index))
- return null;
- final CharSequence text = a.getText(index);
- return parseCsvText(text);
- }
-
- /* package */ static CharSequence[] parseCsvText(CharSequence text) {
- final int size = text.length();
- if (size == 0) return null;
- if (size == 1) return new CharSequence[] { text };
- final StringBuilder sb = new StringBuilder();
- ArrayList<CharSequence> list = null;
- int start = 0;
- for (int pos = 0; pos < size; pos++) {
- final char c = text.charAt(pos);
- if (c == ',') {
- if (list == null) list = new ArrayList<CharSequence>();
- if (sb.length() == 0) {
- list.add(text.subSequence(start, pos));
- } else {
- list.add(sb.toString());
- sb.setLength(0);
- }
- start = pos + 1;
- continue;
- } else if (c == '\\') {
- if (start == pos) {
- // Skip escape character at the beginning of the value.
- start++;
- pos++;
- } else {
- if (start < pos && sb.length() == 0)
- sb.append(text.subSequence(start, pos));
- pos++;
- if (pos < size)
- sb.append(text.charAt(pos));
- }
- } else if (sb.length() > 0) {
- sb.append(c);
- }
- }
- if (list == null) {
- return new CharSequence[] { sb.length() > 0 ? sb : text.subSequence(start, size) };
- } else {
- list.add(sb.length() > 0 ? sb : text.subSequence(start, size));
- return list.toArray(new CharSequence[list.size()]);
- }
+ public int getFlag(TypedArray a, int index) {
+ return a.getInt(index, 0);
}
}
- private static class DeclaredKeyStyle extends EmptyKeyStyle {
- private final HashMap<Integer, Object> mAttributes = new HashMap<Integer, Object>();
+ private class DeclaredKeyStyle extends KeyStyle {
+ private final String mParentStyleName;
+ private final HashMap<Integer, Object> mStyleAttributes = new HashMap<Integer, Object>();
- @Override
- public CharSequence[] getTextArray(TypedArray a, int index) {
- return a.hasValue(index)
- ? super.getTextArray(a, index) : (CharSequence[])mAttributes.get(index);
+ public DeclaredKeyStyle(String parentStyleName) {
+ mParentStyleName = parentStyleName;
}
@Override
- public CharSequence getText(TypedArray a, int index) {
- return a.hasValue(index)
- ? super.getText(a, index) : (CharSequence)mAttributes.get(index);
+ public String[] getStringArray(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ return parseStringArray(a, index);
+ }
+ if (mStyleAttributes.containsKey(index)) {
+ return (String[])mStyleAttributes.get(index);
+ }
+ final KeyStyle parentStyle = mStyles.get(mParentStyleName);
+ return parentStyle.getStringArray(a, index);
}
@Override
- public int getInt(TypedArray a, int index, int defaultValue) {
- final Integer value = (Integer)mAttributes.get(index);
- return super.getInt(a, index, (value != null) ? value : defaultValue);
+ public String getString(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ return parseString(a, index);
+ }
+ if (mStyleAttributes.containsKey(index)) {
+ return (String)mStyleAttributes.get(index);
+ }
+ final KeyStyle parentStyle = mStyles.get(mParentStyleName);
+ return parentStyle.getString(a, index);
}
@Override
- public int getFlag(TypedArray a, int index, int defaultValue) {
- final Integer value = (Integer)mAttributes.get(index);
- return super.getFlag(a, index, defaultValue) | (value != null ? value : 0);
+ public int getInt(TypedArray a, int index, int defaultValue) {
+ if (a.hasValue(index)) {
+ return a.getInt(index, defaultValue);
+ }
+ if (mStyleAttributes.containsKey(index)) {
+ return (Integer)mStyleAttributes.get(index);
+ }
+ final KeyStyle parentStyle = mStyles.get(mParentStyleName);
+ return parentStyle.getInt(a, index, defaultValue);
}
@Override
- public boolean getBoolean(TypedArray a, int index, boolean defaultValue) {
- final Boolean value = (Boolean)mAttributes.get(index);
- return super.getBoolean(a, index, (value != null) ? value : defaultValue);
- }
-
- private DeclaredKeyStyle() {
- super();
+ public int getFlag(TypedArray a, int index) {
+ int value = a.getInt(index, 0);
+ if (mStyleAttributes.containsKey(index)) {
+ value |= (Integer)mStyleAttributes.get(index);
+ }
+ final KeyStyle parentStyle = mStyles.get(mParentStyleName);
+ return value | parentStyle.getFlag(a, index);
}
- private void parseKeyStyleAttributes(TypedArray keyAttr) {
+ void readKeyAttributes(TypedArray keyAttr) {
// TODO: Currently not all Key attributes can be declared as style.
- readInt(keyAttr, R.styleable.Keyboard_Key_code);
- readText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
- readText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
- readText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
- readTextArray(keyAttr, R.styleable.Keyboard_Key_popupCharacters);
- readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption);
- readInt(keyAttr, R.styleable.Keyboard_Key_keyIcon);
- readInt(keyAttr, R.styleable.Keyboard_Key_keyIconPreview);
- readInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted);
- readInt(keyAttr, R.styleable.Keyboard_Key_maxPopupKeyboardColumn);
- readBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional);
- readBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky);
- readBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable);
- readBoolean(keyAttr, R.styleable.Keyboard_Key_enabled);
- }
-
- private void readText(TypedArray a, int index) {
- if (a.hasValue(index))
- mAttributes.put(index, a.getText(index));
+ readString(keyAttr, R.styleable.Keyboard_Key_code);
+ readString(keyAttr, R.styleable.Keyboard_Key_altCode);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyLabel);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
+ readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
+ readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
+ readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyIcon);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled);
+ readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview);
+ readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn);
+ readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
+ readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ }
+
+ private void readString(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ mStyleAttributes.put(index, parseString(a, index));
+ }
}
private void readInt(TypedArray a, int index) {
- if (a.hasValue(index))
- mAttributes.put(index, a.getInt(index, 0));
+ if (a.hasValue(index)) {
+ mStyleAttributes.put(index, a.getInt(index, 0));
+ }
}
private void readFlag(TypedArray a, int index) {
- final Integer value = (Integer)mAttributes.get(index);
- if (a.hasValue(index))
- mAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
- }
-
- private void readBoolean(TypedArray a, int index) {
- if (a.hasValue(index))
- mAttributes.put(index, a.getBoolean(index, false));
- }
-
- private void readTextArray(TypedArray a, int index) {
- final CharSequence[] value = parseTextArray(a, index);
- if (value != null)
- mAttributes.put(index, value);
+ if (a.hasValue(index)) {
+ final Integer value = (Integer)mStyleAttributes.get(index);
+ mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
+ }
}
- private void addParent(DeclaredKeyStyle parentStyle) {
- mAttributes.putAll(parentStyle.mAttributes);
+ private void readStringArray(TypedArray a, int index) {
+ if (a.hasValue(index)) {
+ mStyleAttributes.put(index, parseStringArray(a, index));
+ }
}
}
public void parseKeyStyleAttributes(TypedArray keyStyleAttr, TypedArray keyAttrs,
- XmlResourceParser parser) {
- String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
- if (DEBUG) Log.d(TAG, String.format("<%s styleName=%s />",
- KeyboardParser.TAG_KEY_STYLE, styleName));
- if (mStyles.containsKey(styleName))
- throw new ParseException("duplicate key style declared: " + styleName, parser);
-
- final DeclaredKeyStyle style = new DeclaredKeyStyle();
+ XmlPullParser parser) throws XmlPullParserException {
+ final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
+ if (DEBUG) {
+ Log.d(TAG, String.format("<%s styleName=%s />",
+ Keyboard.Builder.TAG_KEY_STYLE, styleName));
+ if (mStyles.containsKey(styleName)) {
+ Log.d(TAG, "key-style " + styleName + " is overridden at "
+ + parser.getPositionDescription());
+ }
+ }
+
+ String parentStyleName = EMPTY_STYLE_NAME;
if (keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_parentStyle)) {
- String parentStyle = keyStyleAttr.getString(
- R.styleable.Keyboard_KeyStyle_parentStyle);
- final DeclaredKeyStyle parent = mStyles.get(parentStyle);
- if (parent == null)
- throw new ParseException("Unknown parentStyle " + parent, parser);
- style.addParent(parent);
- }
- style.parseKeyStyleAttributes(keyAttrs);
+ parentStyleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_parentStyle);
+ if (!mStyles.containsKey(parentStyleName)) {
+ throw new XmlParseUtils.ParseException(
+ "Unknown parentStyle " + parentStyleName, parser);
+ }
+ }
+ final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName);
+ style.readKeyAttributes(keyAttrs);
mStyles.put(styleName, style);
}
- public KeyStyle getKeyStyle(String styleName) {
+ public KeyStyle getKeyStyle(TypedArray keyAttr, XmlPullParser parser)
+ throws XmlParseUtils.ParseException {
+ if (!keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
+ return mEmptyKeyStyle;
+ }
+ final String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
+ if (!mStyles.containsKey(styleName)) {
+ throw new XmlParseUtils.ParseException("Unknown key style: " + styleName, parser);
+ }
return mStyles.get(styleName);
}
-
- public KeyStyle getEmptyKeyStyle() {
- return EMPTY_KEY_STYLE;
- }
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
new file mode 100644
index 000000000..f7981a320
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -0,0 +1,151 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.keyboard.Keyboard;
+
+import java.util.HashMap;
+
+public class KeyboardCodesSet {
+ private static final HashMap<String, int[]> sLanguageToCodesMap =
+ new HashMap<String, int[]>();
+ private static final HashMap<String, Integer> sNameToIdMap = new HashMap<String, Integer>();
+
+ private int[] mCodes = DEFAULT;
+
+ public void setLanguage(final String language) {
+ final int[] codes = sLanguageToCodesMap.get(language);
+ mCodes = (codes != null) ? codes : DEFAULT;
+ }
+
+ public int getCode(final String name) {
+ Integer id = sNameToIdMap.get(name);
+ if (id == null) throw new RuntimeException("Unknown key code: " + name);
+ return mCodes[id];
+ }
+
+ private static final String[] ID_TO_NAME = {
+ "key_tab",
+ "key_enter",
+ "key_space",
+ "key_shift",
+ "key_switch_alpha_symbol",
+ "key_output_text",
+ "key_delete",
+ "key_settings",
+ "key_shortcut",
+ "key_action_enter",
+ "key_action_next",
+ "key_action_previous",
+ "key_language_switch",
+ "key_research",
+ "key_unspecified",
+ "key_left_parenthesis",
+ "key_right_parenthesis",
+ "key_less_than",
+ "key_greater_than",
+ "key_left_square_bracket",
+ "key_right_square_bracket",
+ "key_left_curly_bracket",
+ "key_right_curly_bracket",
+ };
+
+ private static final int CODE_LEFT_PARENTHESIS = '(';
+ private static final int CODE_RIGHT_PARENTHESIS = ')';
+ private static final int CODE_LESS_THAN_SIGN = '<';
+ private static final int CODE_GREATER_THAN_SIGN = '>';
+ private static final int CODE_LEFT_SQUARE_BRACKET = '[';
+ private static final int CODE_RIGHT_SQUARE_BRACKET = ']';
+ private static final int CODE_LEFT_CURLY_BRACKET = '{';
+ private static final int CODE_RIGHT_CURLY_BRACKET = '}';
+
+ private static final int[] DEFAULT = {
+ Keyboard.CODE_TAB,
+ Keyboard.CODE_ENTER,
+ Keyboard.CODE_SPACE,
+ Keyboard.CODE_SHIFT,
+ Keyboard.CODE_SWITCH_ALPHA_SYMBOL,
+ Keyboard.CODE_OUTPUT_TEXT,
+ Keyboard.CODE_DELETE,
+ Keyboard.CODE_SETTINGS,
+ Keyboard.CODE_SHORTCUT,
+ Keyboard.CODE_ACTION_ENTER,
+ Keyboard.CODE_ACTION_NEXT,
+ Keyboard.CODE_ACTION_PREVIOUS,
+ Keyboard.CODE_LANGUAGE_SWITCH,
+ Keyboard.CODE_RESEARCH,
+ Keyboard.CODE_UNSPECIFIED,
+ CODE_LEFT_PARENTHESIS,
+ CODE_RIGHT_PARENTHESIS,
+ CODE_LESS_THAN_SIGN,
+ CODE_GREATER_THAN_SIGN,
+ CODE_LEFT_SQUARE_BRACKET,
+ CODE_RIGHT_SQUARE_BRACKET,
+ CODE_LEFT_CURLY_BRACKET,
+ CODE_RIGHT_CURLY_BRACKET,
+ };
+
+ private static final int[] RTL = {
+ DEFAULT[0],
+ DEFAULT[1],
+ DEFAULT[2],
+ DEFAULT[3],
+ DEFAULT[4],
+ DEFAULT[5],
+ DEFAULT[6],
+ DEFAULT[7],
+ DEFAULT[8],
+ DEFAULT[9],
+ DEFAULT[10],
+ DEFAULT[11],
+ DEFAULT[12],
+ DEFAULT[13],
+ DEFAULT[14],
+ CODE_RIGHT_PARENTHESIS,
+ CODE_LEFT_PARENTHESIS,
+ CODE_GREATER_THAN_SIGN,
+ CODE_LESS_THAN_SIGN,
+ CODE_RIGHT_SQUARE_BRACKET,
+ CODE_LEFT_SQUARE_BRACKET,
+ CODE_RIGHT_CURLY_BRACKET,
+ CODE_LEFT_CURLY_BRACKET,
+ };
+
+ private static final String LANGUAGE_DEFAULT = "DEFAULT";
+ private static final String LANGUAGE_ARABIC = "ar";
+ private static final String LANGUAGE_PERSIAN = "fa";
+ private static final String LANGUAGE_HEBREW = "iw";
+
+ private static final Object[] LANGUAGE_AND_CODES = {
+ LANGUAGE_DEFAULT, DEFAULT,
+ LANGUAGE_ARABIC, RTL,
+ LANGUAGE_PERSIAN, RTL,
+ LANGUAGE_HEBREW, RTL,
+ };
+
+ static {
+ for (int i = 0; i < ID_TO_NAME.length; i++) {
+ sNameToIdMap.put(ID_TO_NAME[i], i);
+ }
+
+ for (int i = 0; i < LANGUAGE_AND_CODES.length; i += 2) {
+ final String language = (String)LANGUAGE_AND_CODES[i];
+ final int[] codes = (int[])LANGUAGE_AND_CODES[i + 1];
+ sLanguageToCodesMap.put(language, codes);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 1530fed6f..540e63b3f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -21,96 +21,101 @@ import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.Log;
-import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.R;
+import java.util.HashMap;
+
public class KeyboardIconsSet {
private static final String TAG = KeyboardIconsSet.class.getSimpleName();
public static final int ICON_UNDEFINED = 0;
+ private static final int ATTR_UNDEFINED = 0;
+
+ private static final HashMap<Integer, Integer> ATTR_ID_TO_ICON_ID
+ = new HashMap<Integer, Integer>();
+
+ // Icon name to icon id map.
+ private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<String, Integer>();
+
+ private static final Object[] NAMES_AND_ATTR_IDS = {
+ "undefined", ATTR_UNDEFINED,
+ "shift_key", R.styleable.Keyboard_iconShiftKey,
+ "delete_key", R.styleable.Keyboard_iconDeleteKey,
+ "settings_key", R.styleable.Keyboard_iconSettingsKey,
+ "space_key", R.styleable.Keyboard_iconSpaceKey,
+ "enter_key", R.styleable.Keyboard_iconEnterKey,
+ "search_key", R.styleable.Keyboard_iconSearchKey,
+ "tab_key", R.styleable.Keyboard_iconTabKey,
+ "shortcut_key", R.styleable.Keyboard_iconShortcutKey,
+ "shortcut_for_label", R.styleable.Keyboard_iconShortcutForLabel,
+ "space_key_for_number_layout", R.styleable.Keyboard_iconSpaceKeyForNumberLayout,
+ "shift_key_shifted", R.styleable.Keyboard_iconShiftKeyShifted,
+ "shortcut_key_disabled", R.styleable.Keyboard_iconShortcutKeyDisabled,
+ "tab_key_preview", R.styleable.Keyboard_iconTabKeyPreview,
+ "language_switch_key", R.styleable.Keyboard_iconLanguageSwitchKey,
+ "zwnj_key", R.styleable.Keyboard_iconZwnjKey,
+ "zwj_key", R.styleable.Keyboard_iconZwjKey,
+ };
+
+ private static int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2;
+ private static final String[] ICON_NAMES = new String[NUM_ICONS];
+ private final Drawable[] mIcons = new Drawable[NUM_ICONS];
- // This should be aligned with Keyboard.keyIcon enum.
- private static final int ICON_SHIFT_KEY = 1;
- private static final int ICON_TO_SYMBOL_KEY = 2;
- private static final int ICON_TO_SYMBOL_KEY_WITH_SHORTCUT = 3;
- private static final int ICON_DELETE_KEY = 4;
- private static final int ICON_SETTINGS_KEY = 5;
- private static final int ICON_SHORTCUT_KEY = 6;
- private static final int ICON_SPACE_KEY = 7;
- private static final int ICON_RETURN_KEY = 8;
- private static final int ICON_SEARCH_KEY = 9;
- private static final int ICON_TAB_KEY = 10;
- // This should be aligned with Keyboard.keyIconShifted enum.
- private static final int ICON_SHIFTED_SHIFT_KEY = 11;
- // This should be aligned with Keyboard.keyIconPreview enum.
- private static final int ICON_PREVIEW_SPACE_KEY = 12;
- private static final int ICON_PREVIEW_TAB_KEY = 13;
- private static final int ICON_PREVIEW_SETTINGS_KEY = 14;
- private static final int ICON_PREVIEW_SHORTCUT_KEY = 15;
-
- private static final int ICON_LAST = 15;
-
- private final Drawable mIcons[] = new Drawable[ICON_LAST + 1];
-
- private static final int getIconId(int attrIndex) {
- switch (attrIndex) {
- case R.styleable.Keyboard_iconShiftKey:
- return ICON_SHIFT_KEY;
- case R.styleable.Keyboard_iconToSymbolKey:
- return ICON_TO_SYMBOL_KEY;
- case R.styleable.Keyboard_iconToSymbolKeyWithShortcut:
- return ICON_TO_SYMBOL_KEY_WITH_SHORTCUT;
- case R.styleable.Keyboard_iconDeleteKey:
- return ICON_DELETE_KEY;
- case R.styleable.Keyboard_iconSettingsKey:
- return ICON_SETTINGS_KEY;
- case R.styleable.Keyboard_iconShortcutKey:
- return ICON_SHORTCUT_KEY;
- case R.styleable.Keyboard_iconSpaceKey:
- return ICON_SPACE_KEY;
- case R.styleable.Keyboard_iconReturnKey:
- return ICON_RETURN_KEY;
- case R.styleable.Keyboard_iconSearchKey:
- return ICON_SEARCH_KEY;
- case R.styleable.Keyboard_iconTabKey:
- return ICON_TAB_KEY;
- case R.styleable.Keyboard_iconShiftedShiftKey:
- return ICON_SHIFTED_SHIFT_KEY;
- case R.styleable.Keyboard_iconPreviewSpaceKey:
- return ICON_PREVIEW_SPACE_KEY;
- case R.styleable.Keyboard_iconPreviewTabKey:
- return ICON_PREVIEW_TAB_KEY;
- case R.styleable.Keyboard_iconPreviewSettingsKey:
- return ICON_PREVIEW_SETTINGS_KEY;
- case R.styleable.Keyboard_iconPreviewShortcutKey:
- return ICON_PREVIEW_SHORTCUT_KEY;
- default:
- return ICON_UNDEFINED;
+ static {
+ int iconId = ICON_UNDEFINED;
+ for (int i = 0; i < NAMES_AND_ATTR_IDS.length; i += 2) {
+ final String name = (String)NAMES_AND_ATTR_IDS[i];
+ final Integer attrId = (Integer)NAMES_AND_ATTR_IDS[i + 1];
+ if (attrId != ATTR_UNDEFINED) {
+ ATTR_ID_TO_ICON_ID.put(attrId, iconId);
+ }
+ sNameToIdsMap.put(name, iconId);
+ ICON_NAMES[iconId] = name;
+ iconId++;
}
}
- public void loadIcons(TypedArray keyboardAttrs) {
- final int count = keyboardAttrs.getIndexCount();
- for (int i = 0; i < count; i++) {
- final int attrIndex = keyboardAttrs.getIndex(i);
- final int iconId = getIconId(attrIndex);
- if (iconId != ICON_UNDEFINED) {
- try {
- final Drawable icon = keyboardAttrs.getDrawable(attrIndex);
- Keyboard.setDefaultBounds(icon);
- mIcons[iconId] = icon;
- } catch (Resources.NotFoundException e) {
- Log.w(TAG, "Drawable resource for icon #" + iconId + " not found");
- }
+ public void loadIcons(final TypedArray keyboardAttrs) {
+ for (final Integer attrId : ATTR_ID_TO_ICON_ID.keySet()) {
+ try {
+ final Drawable icon = keyboardAttrs.getDrawable(attrId);
+ setDefaultBounds(icon);
+ final Integer iconId = ATTR_ID_TO_ICON_ID.get(attrId);
+ mIcons[iconId] = icon;
+ } catch (Resources.NotFoundException e) {
+ Log.w(TAG, "Drawable resource for icon #"
+ + keyboardAttrs.getResources().getResourceEntryName(attrId)
+ + " not found");
}
}
}
- public Drawable getIcon(int iconId) {
- if (iconId == ICON_UNDEFINED)
- return null;
- if (iconId < 0 || iconId >= mIcons.length)
- throw new IllegalArgumentException("icon id is out of range: " + iconId);
- return mIcons[iconId];
+ private static boolean isValidIconId(final int iconId) {
+ return iconId >= 0 && iconId < ICON_NAMES.length;
+ }
+
+ public static String getIconName(final int iconId) {
+ return isValidIconId(iconId) ? ICON_NAMES[iconId] : "unknown<" + iconId + ">";
+ }
+
+ static int getIconId(final String name) {
+ Integer iconId = sNameToIdsMap.get(name);
+ if (iconId != null) {
+ return iconId;
+ }
+ throw new RuntimeException("unknown icon name: " + name);
+ }
+
+ public Drawable getIconDrawable(final int iconId) {
+ if (isValidIconId(iconId)) {
+ return mIcons[iconId];
+ }
+ throw new RuntimeException("unknown icon id: " + getIconName(iconId));
+ }
+
+ private static void setDefaultBounds(final Drawable icon) {
+ if (icon != null) {
+ icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+ }
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java
deleted file mode 100644
index a6708171f..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java
+++ /dev/null
@@ -1,726 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.util.Log;
-import android.util.TypedValue;
-import android.util.Xml;
-import android.view.InflateException;
-
-import com.android.inputmethod.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.latin.R;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Parser for BaseKeyboard.
- *
- * This class parses Keyboard XML file and fill out keys in Keyboard.
- * The Keyboard XML file looks like:
- * <pre>
- * &gt;!-- xml/keyboard.xml --&lt;
- * &gt;Keyboard keyboard_attributes*&lt;
- * &gt;!-- Keyboard Content --&lt;
- * &gt;Row row_attributes*&lt;
- * &gt;!-- Row Content --&lt;
- * &gt;Key key_attributes* /&lt;
- * &gt;Spacer horizontalGap="0.2in" /&lt;
- * &gt;include keyboardLayout="@xml/other_keys"&lt;
- * ...
- * &gt;/Row&lt;
- * &gt;include keyboardLayout="@xml/other_rows"&lt;
- * ...
- * &gt;/Keyboard&lt;
- * </pre>
- * The XML file which is included in other file must have &gt;merge&lt; as root element, such as:
- * <pre>
- * &gt;!-- xml/other_keys.xml --&lt;
- * &gt;merge&lt;
- * &gt;Key key_attributes* /&lt;
- * ...
- * &gt;/merge&lt;
- * </pre>
- * and
- * <pre>
- * &gt;!-- xml/other_rows.xml --&lt;
- * &gt;merge&lt;
- * &gt;Row row_attributes*&lt;
- * &gt;Key key_attributes* /&lt;
- * &gt;/Row&lt;
- * ...
- * &gt;/merge&lt;
- * </pre>
- * You can also use switch-case-default tags to select Rows and Keys.
- * <pre>
- * &gt;switch&lt;
- * &gt;case case_attribute*&lt;
- * &gt;!-- Any valid tags at switch position --&lt;
- * &gt;/case&lt;
- * ...
- * &gt;default&lt;
- * &gt;!-- Any valid tags at switch position --&lt;
- * &gt;/default&lt;
- * &gt;/switch&lt;
- * </pre>
- * You can declare Key style and specify styles within Key tags.
- * <pre>
- * &gt;switch&lt;
- * &gt;case mode="email"&lt;
- * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
- * keyLabel=".com"
- * /&lt;
- * &gt;/case&lt;
- * &gt;case mode="url"&lt;
- * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
- * keyLabel="http://"
- * /&lt;
- * &gt;/case&lt;
- * &gt;/switch&lt;
- * ...
- * &gt;Key keyStyle="shift-key" ... /&lt;
- * </pre>
- */
-
-public class KeyboardParser {
- private static final String TAG = KeyboardParser.class.getSimpleName();
- private static final boolean DEBUG = false;
-
- // Keyboard XML Tags
- private static final String TAG_KEYBOARD = "Keyboard";
- private static final String TAG_ROW = "Row";
- private static final String TAG_KEY = "Key";
- private static final String TAG_SPACER = "Spacer";
- private static final String TAG_INCLUDE = "include";
- private static final String TAG_MERGE = "merge";
- private static final String TAG_SWITCH = "switch";
- private static final String TAG_CASE = "case";
- private static final String TAG_DEFAULT = "default";
- public static final String TAG_KEY_STYLE = "key-style";
-
- private final Keyboard mKeyboard;
- private final Context mContext;
- private final Resources mResources;
-
- private int mKeyboardTopPadding;
- private int mKeyboardBottomPadding;
- private int mHorizontalEdgesPadding;
- private int mCurrentX = 0;
- private int mCurrentY = 0;
- private int mMaxRowWidth = 0;
- private int mTotalHeight = 0;
- private Row mCurrentRow = null;
- private final KeyStyles mKeyStyles = new KeyStyles();
-
- public KeyboardParser(Keyboard keyboard, Context context) {
- mKeyboard = keyboard;
- mContext = context;
- final Resources res = context.getResources();
- mResources = res;
- mHorizontalEdgesPadding = (int)res.getDimension(R.dimen.keyboard_horizontal_edges_padding);
- }
-
- public int getMaxRowWidth() {
- return mMaxRowWidth;
- }
-
- public int getTotalHeight() {
- return mTotalHeight;
- }
-
- public void parseKeyboard(int resId) throws XmlPullParserException, IOException {
- if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mKeyboard.mId));
- final XmlResourceParser parser = mResources.getXml(resId);
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_KEYBOARD.equals(tag)) {
- parseKeyboardAttributes(parser);
- startKeyboard();
- parseKeyboardContent(parser, mKeyboard.getKeys());
- break;
- } else {
- throw new IllegalStartTag(parser, TAG_KEYBOARD);
- }
- }
- }
- }
-
- public static String parseKeyboardLocale(
- Context context, int resId) throws XmlPullParserException, IOException {
- final Resources res = context.getResources();
- final XmlResourceParser parser = res.getXml(resId);
- if (parser == null) return "";
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_KEYBOARD.equals(tag)) {
- final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- return keyboardAttr.getString(R.styleable.Keyboard_keyboardLocale);
- } else {
- throw new IllegalStartTag(parser, TAG_KEYBOARD);
- }
- }
- }
- return "";
- }
-
- private void parseKeyboardAttributes(XmlResourceParser parser) {
- final Keyboard keyboard = mKeyboard;
- final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
- Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
- R.style.Keyboard);
- final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
- try {
- final int displayHeight = keyboard.getDisplayHeight();
- final int keyboardHeight = (int)keyboardAttr.getDimension(
- R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
- final int maxKeyboardHeight = getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
- int minKeyboardHeight = getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
- if (minKeyboardHeight < 0) {
- // Specified fraction was negative, so it should be calculated against display
- // width.
- final int displayWidth = keyboard.getDisplayWidth();
- minKeyboardHeight = -getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
- }
- // Keyboard height will not exceed maxKeyboardHeight and will not be less than
- // minKeyboardHeight.
- final int height = Math.max(
- Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
- final int width = keyboard.getDisplayWidth();
-
- keyboard.setKeyboardHeight(height);
- keyboard.setKeyWidth(getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyWidth, width, width / 10));
- keyboard.setRowHeight(getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight, height, 50));
- keyboard.setHorizontalGap(getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_horizontalGap, width, 0));
- keyboard.setVerticalGap(getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_verticalGap, height, 0));
- keyboard.setPopupKeyboardResId(keyboardAttr.getResourceId(
- R.styleable.Keyboard_popupKeyboardTemplate, 0));
-
- keyboard.setMaxPopupKeyboardColumn(keyAttr.getInt(
- R.styleable.Keyboard_Key_maxPopupKeyboardColumn, 5));
-
- mKeyboard.mIconsSet.loadIcons(keyboardAttr);
- mKeyboardTopPadding = keyboardAttr.getDimensionPixelSize(
- R.styleable.Keyboard_keyboardTopPadding, 0);
- mKeyboardBottomPadding = keyboardAttr.getDimensionPixelSize(
- R.styleable.Keyboard_keyboardBottomPadding, 0);
- } finally {
- keyAttr.recycle();
- keyboardAttr.recycle();
- }
- }
-
- private void parseKeyboardContent(XmlResourceParser parser, List<Key> keys)
- throws XmlPullParserException, IOException {
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_ROW.equals(tag)) {
- Row row = new Row(mResources, mKeyboard, parser);
- if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW));
- if (keys != null)
- startRow(row);
- parseRowContent(parser, row, keys);
- } else if (TAG_INCLUDE.equals(tag)) {
- parseIncludeKeyboardContent(parser, keys);
- } else if (TAG_SWITCH.equals(tag)) {
- parseSwitchKeyboardContent(parser, keys);
- } else if (TAG_KEY_STYLE.equals(tag)) {
- parseKeyStyle(parser, keys);
- } else {
- throw new IllegalStartTag(parser, TAG_ROW);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- if (TAG_KEYBOARD.equals(tag)) {
- endKeyboard(mKeyboard.getVerticalGap());
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
- break;
- } else if (TAG_KEY_STYLE.equals(tag)) {
- continue;
- } else {
- throw new IllegalEndTag(parser, TAG_ROW);
- }
- }
- }
- }
-
- private void parseRowContent(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_KEY.equals(tag)) {
- parseKey(parser, row, keys);
- } else if (TAG_SPACER.equals(tag)) {
- parseSpacer(parser, row, keys);
- } else if (TAG_INCLUDE.equals(tag)) {
- parseIncludeRowContent(parser, row, keys);
- } else if (TAG_SWITCH.equals(tag)) {
- parseSwitchRowContent(parser, row, keys);
- } else if (TAG_KEY_STYLE.equals(tag)) {
- parseKeyStyle(parser, keys);
- } else {
- throw new IllegalStartTag(parser, TAG_KEY);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- if (TAG_ROW.equals(tag)) {
- if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW));
- if (keys != null)
- endRow();
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- if (DEBUG) Log.d(TAG, String.format("</%s>", tag));
- break;
- } else if (TAG_KEY_STYLE.equals(tag)) {
- continue;
- } else {
- throw new IllegalEndTag(parser, TAG_KEY);
- }
- }
- }
- }
-
- private void parseKey(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- if (keys == null) {
- checkEndTag(TAG_KEY, parser);
- } else {
- Key key = new Key(mResources, row, mCurrentX, mCurrentY, parser, mKeyStyles);
- if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d popupCharacters=%s />",
- TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode,
- Arrays.toString(key.mPopupCharacters)));
- checkEndTag(TAG_KEY, parser);
- keys.add(key);
- if (key.mCode == Keyboard.CODE_SHIFT)
- mKeyboard.getShiftKeys().add(key);
- endKey(key);
- }
- }
-
- private void parseSpacer(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- if (keys == null) {
- checkEndTag(TAG_SPACER, parser);
- } else {
- if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER));
- final TypedArray keyboardAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap))
- throw new IllegalAttribute(parser, "horizontalGap");
- final int defaultWidth = (row != null) ? row.mDefaultWidth : 0;
- final int keyWidth = getDimensionOrFraction(keyboardAttr, R.styleable.Keyboard_keyWidth,
- mKeyboard.getDisplayWidth(), defaultWidth);
- keyboardAttr.recycle();
-
- final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
- int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyXPos, mKeyboard.getDisplayWidth(), mCurrentX);
- if (keyXPos < 0) {
- // If keyXPos is negative, the actual x-coordinate will be display_width + keyXPos.
- keyXPos += mKeyboard.getDisplayWidth();
- }
-
- checkEndTag(TAG_SPACER, parser);
- setSpacer(keyXPos, keyWidth);
- }
- }
-
- private void parseIncludeKeyboardContent(XmlResourceParser parser, List<Key> keys)
- throws XmlPullParserException, IOException {
- parseIncludeInternal(parser, null, keys);
- }
-
- private void parseIncludeRowContent(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- parseIncludeInternal(parser, row, keys);
- }
-
- private void parseIncludeInternal(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- if (keys == null) {
- checkEndTag(TAG_INCLUDE, parser);
- } else {
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Include);
- final int keyboardLayout = a.getResourceId(
- R.styleable.Keyboard_Include_keyboardLayout, 0);
- a.recycle();
-
- checkEndTag(TAG_INCLUDE, parser);
- if (keyboardLayout == 0)
- throw new ParseException("No keyboardLayout attribute in <include/>", parser);
- if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />",
- TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout)));
- parseMerge(mResources.getLayout(keyboardLayout), row, keys);
- }
- }
-
- private void parseMerge(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_MERGE.equals(tag)) {
- if (row == null) {
- parseKeyboardContent(parser, keys);
- } else {
- parseRowContent(parser, row, keys);
- }
- break;
- } else {
- throw new ParseException(
- "Included keyboard layout must have <merge> root element", parser);
- }
- }
- }
- }
-
- private void parseSwitchKeyboardContent(XmlResourceParser parser, List<Key> keys)
- throws XmlPullParserException, IOException {
- parseSwitchInternal(parser, null, keys);
- }
-
- private void parseSwitchRowContent(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- parseSwitchInternal(parser, row, keys);
- }
-
- private void parseSwitchInternal(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mKeyboard.mId));
- boolean selected = false;
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_CASE.equals(tag)) {
- selected |= parseCase(parser, row, selected ? null : keys);
- } else if (TAG_DEFAULT.equals(tag)) {
- selected |= parseDefault(parser, row, selected ? null : keys);
- } else {
- throw new IllegalStartTag(parser, TAG_KEY);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- if (TAG_SWITCH.equals(tag)) {
- if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH));
- break;
- } else {
- throw new IllegalEndTag(parser, TAG_KEY);
- }
- }
- }
- }
-
- private boolean parseCase(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- final boolean selected = parseCaseCondition(parser);
- if (row == null) {
- // Processing Rows.
- parseKeyboardContent(parser, selected ? keys : null);
- } else {
- // Processing Keys.
- parseRowContent(parser, row, selected ? keys : null);
- }
- return selected;
- }
-
- private boolean parseCaseCondition(XmlResourceParser parser) {
- final KeyboardId id = mKeyboard.mId;
- if (id == null)
- return true;
-
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Case);
- final TypedArray viewAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.KeyboardView);
- try {
- final boolean modeMatched = matchTypedValue(a,
- R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
- final boolean navigateActionMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_navigateAction, id.mNavigateAction);
- final boolean passwordInputMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_passwordInput, id.mPasswordInput);
- final boolean hasSettingsKeyMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_hasSettingsKey, id.mHasSettingsKey);
- final boolean f2KeyModeMatched = matchInteger(a,
- R.styleable.Keyboard_Case_f2KeyMode, id.mF2KeyMode);
- final boolean clobberSettingsKeyMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
- final boolean voiceEnabledMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_voiceKeyEnabled, id.mVoiceKeyEnabled);
- final boolean voiceKeyMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_hasVoiceKey, id.mHasVoiceKey);
- // As noted at {@link KeyboardId} class, we are interested only in enum value masked by
- // {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and
- // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching
- // this attribute with id.mImeOptions as integer value is enough for our purpose.
- final boolean imeActionMatched = matchInteger(a,
- R.styleable.Keyboard_Case_imeAction, id.mImeAction);
- final boolean localeCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
- final boolean languageCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
- final boolean countryCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
- final boolean selected = modeMatched && navigateActionMatched && passwordInputMatched
- && hasSettingsKeyMatched && f2KeyModeMatched && clobberSettingsKeyMatched
- && voiceEnabledMatched && voiceKeyMatched && imeActionMatched &&
- localeCodeMatched && languageCodeMatched && countryCodeMatched;
-
- if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE,
- textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"),
- booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"),
- booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"),
- textAttr(KeyboardId.f2KeyModeName(
- a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"),
- booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
- "clobberSettingsKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_voiceKeyEnabled, "voiceKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_hasVoiceKey, "hasVoiceKey"),
- textAttr(EditorInfoCompatUtils.imeOptionsName(
- a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"),
- textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), "languageCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"),
- Boolean.toString(selected)));
-
- return selected;
- } finally {
- a.recycle();
- viewAttr.recycle();
- }
- }
-
- private static boolean matchInteger(TypedArray a, int index, int value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for the
- // attribute.
- return !a.hasValue(index) || a.getInt(index, 0) == value;
- }
-
- private static boolean matchBoolean(TypedArray a, int index, boolean value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for the
- // attribute.
- return !a.hasValue(index) || a.getBoolean(index, false) == value;
- }
-
- private static boolean matchString(TypedArray a, int index, String value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for the
- // attribute.
- return !a.hasValue(index) || stringArrayContains(a.getString(index).split("\\|"), value);
- }
-
- private static boolean matchTypedValue(TypedArray a, int index, int intValue, String strValue) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for the
- // attribute.
- final TypedValue v = a.peekValue(index);
- if (v == null)
- return true;
-
- if (isIntegerValue(v)) {
- return intValue == a.getInt(index, 0);
- } else if (isStringValue(v)) {
- return stringArrayContains(a.getString(index).split("\\|"), strValue);
- }
- return false;
- }
-
- private static boolean stringArrayContains(String[] array, String value) {
- for (final String elem : array) {
- if (elem.equals(value))
- return true;
- }
- return false;
- }
-
- private boolean parseDefault(XmlResourceParser parser, Row row, List<Key> keys)
- throws XmlPullParserException, IOException {
- if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT));
- if (row == null) {
- parseKeyboardContent(parser, keys);
- } else {
- parseRowContent(parser, row, keys);
- }
- return true;
- }
-
- private void parseKeyStyle(XmlResourceParser parser, List<Key> keys) {
- TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_KeyStyle);
- TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
- try {
- if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
- throw new ParseException("<" + TAG_KEY_STYLE
- + "/> needs styleName attribute", parser);
- if (keys != null)
- mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
- } finally {
- keyStyleAttr.recycle();
- keyAttrs.recycle();
- }
- }
-
- private static void checkEndTag(String tag, XmlResourceParser parser)
- throws XmlPullParserException, IOException {
- if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
- return;
- throw new NonEmptyTag(tag, parser);
- }
-
- private void startKeyboard() {
- mCurrentY += mKeyboardTopPadding;
- }
-
- private void startRow(Row row) {
- mCurrentX = 0;
- setSpacer(mCurrentX, mHorizontalEdgesPadding);
- mCurrentRow = row;
- }
-
- private void endRow() {
- if (mCurrentRow == null)
- throw new InflateException("orphant end row tag");
- setSpacer(mCurrentX, mHorizontalEdgesPadding);
- if (mCurrentX > mMaxRowWidth)
- mMaxRowWidth = mCurrentX;
- mCurrentY += mCurrentRow.mDefaultHeight;
- mCurrentRow = null;
- }
-
- private void endKey(Key key) {
- mCurrentX = key.mX - key.mGap / 2 + key.mWidth + key.mGap;
- }
-
- private void endKeyboard(int defaultVerticalGap) {
- mCurrentY += mKeyboardBottomPadding;
- mTotalHeight = mCurrentY - defaultVerticalGap;
- }
-
- private void setSpacer(int keyXPos, int width) {
- mCurrentX = keyXPos + width;
- }
-
- public static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
- final TypedValue value = a.peekValue(index);
- if (value == null)
- return defValue;
- if (isFractionValue(value)) {
- // Round it to avoid values like 47.9999 from getting truncated
- return Math.round(a.getFraction(index, base, base, defValue));
- } else if (isDimensionValue(value)) {
- return a.getDimensionPixelOffset(index, defValue);
- } else if (isIntegerValue(value)) {
- // For enum value.
- return a.getInt(index, defValue);
- }
- return defValue;
- }
-
- private static boolean isFractionValue(TypedValue v) {
- return v.type == TypedValue.TYPE_FRACTION;
- }
-
- private static boolean isDimensionValue(TypedValue v) {
- return v.type == TypedValue.TYPE_DIMENSION;
- }
-
- private static boolean isIntegerValue(TypedValue v) {
- return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
- }
-
- private static boolean isStringValue(TypedValue v) {
- return v.type == TypedValue.TYPE_STRING;
- }
-
- @SuppressWarnings("serial")
- public static class ParseException extends InflateException {
- public ParseException(String msg, XmlResourceParser parser) {
- super(msg + " at line " + parser.getLineNumber());
- }
- }
-
- @SuppressWarnings("serial")
- private static class IllegalStartTag extends ParseException {
- public IllegalStartTag(XmlResourceParser parser, String parent) {
- super("Illegal start tag " + parser.getName() + " in " + parent, parser);
- }
- }
-
- @SuppressWarnings("serial")
- private static class IllegalEndTag extends ParseException {
- public IllegalEndTag(XmlResourceParser parser, String parent) {
- super("Illegal end tag " + parser.getName() + " in " + parent, parser);
- }
- }
-
- @SuppressWarnings("serial")
- private static class IllegalAttribute extends ParseException {
- public IllegalAttribute(XmlResourceParser parser, String attribute) {
- super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
- }
- }
-
- @SuppressWarnings("serial")
- private static class NonEmptyTag extends ParseException {
- public NonEmptyTag(String tag, XmlResourceParser parser) {
- super(tag + " must be empty tag", parser);
- }
- }
-
- private static String textAttr(String value, String name) {
- return value != null ? String.format(" %s=%s", name, value) : "";
- }
-
- private static String booleanAttr(TypedArray a, int index, String name) {
- return a.hasValue(index) ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
new file mode 100644
index 000000000..4ab6832c3
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2011 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.keyboard.internal;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.Constants;
+
+/**
+ * Keyboard state machine.
+ *
+ * This class contains all keyboard state transition logic.
+ *
+ * The input events are {@link #onLoadKeyboard(String)}, {@link #onSaveKeyboardState()},
+ * {@link #onPressKey(int, boolean, int)}, {@link #onReleaseKey(int, boolean)},
+ * {@link #onCodeInput(int, boolean, int)}, {@link #onCancelInput(boolean)},
+ * {@link #onUpdateShiftState(int)}, {@link #onLongPressTimeout(int)}.
+ *
+ * The actions are {@link SwitchActions}'s methods.
+ */
+public class KeyboardState {
+ private static final String TAG = KeyboardState.class.getSimpleName();
+ private static final boolean DEBUG_EVENT = false;
+ private static final boolean DEBUG_ACTION = false;
+
+ public interface SwitchActions {
+ public void setAlphabetKeyboard();
+ public void setAlphabetManualShiftedKeyboard();
+ public void setAlphabetAutomaticShiftedKeyboard();
+ public void setAlphabetShiftLockedKeyboard();
+ public void setAlphabetShiftLockShiftedKeyboard();
+ public void setSymbolsKeyboard();
+ public void setSymbolsShiftedKeyboard();
+
+ /**
+ * Request to call back {@link KeyboardState#onUpdateShiftState(int)}.
+ */
+ public void requestUpdatingShiftState();
+
+ public void startDoubleTapTimer();
+ public boolean isInDoubleTapTimeout();
+ public void cancelDoubleTapTimer();
+ public void startLongPressTimer(int code);
+ public void cancelLongPressTimer();
+ public void hapticAndAudioFeedback(int code);
+ }
+
+ private final SwitchActions mSwitchActions;
+
+ private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
+ private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
+
+ // TODO: Merge {@link #mSwitchState}, {@link #mIsAlphabetMode}, {@link #mAlphabetShiftState},
+ // {@link #mIsSymbolShifted}, {@link #mPrevMainKeyboardWasShiftLocked}, and
+ // {@link #mPrevSymbolsKeyboardWasShifted} into single state variable.
+ private static final int SWITCH_STATE_ALPHA = 0;
+ private static final int SWITCH_STATE_SYMBOL_BEGIN = 1;
+ private static final int SWITCH_STATE_SYMBOL = 2;
+ private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
+ private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
+ private int mSwitchState = SWITCH_STATE_ALPHA;
+ private String mLayoutSwitchBackSymbols;
+
+ private boolean mIsAlphabetMode;
+ private AlphabetShiftState mAlphabetShiftState = new AlphabetShiftState();
+ private boolean mIsSymbolShifted;
+ private boolean mPrevMainKeyboardWasShiftLocked;
+ private boolean mPrevSymbolsKeyboardWasShifted;
+
+ // For handling long press.
+ private boolean mLongPressShiftLockFired;
+
+ // For handling double tap.
+ private boolean mIsInAlphabetUnshiftedFromShifted;
+ private boolean mIsInDoubleTapShiftKey;
+
+ private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
+
+ static class SavedKeyboardState {
+ public boolean mIsValid;
+ public boolean mIsAlphabetMode;
+ public boolean mIsAlphabetShiftLocked;
+ public boolean mIsShifted;
+
+ @Override
+ public String toString() {
+ if (!mIsValid) return "INVALID";
+ if (mIsAlphabetMode) {
+ if (mIsAlphabetShiftLocked) return "ALPHABET_SHIFT_LOCKED";
+ return mIsShifted ? "ALPHABET_SHIFTED" : "ALPHABET";
+ } else {
+ return mIsShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS";
+ }
+ }
+ }
+
+ public KeyboardState(SwitchActions switchActions) {
+ mSwitchActions = switchActions;
+ }
+
+ public void onLoadKeyboard(String layoutSwitchBackSymbols) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onLoadKeyboard: " + this);
+ }
+ mLayoutSwitchBackSymbols = layoutSwitchBackSymbols;
+ // Reset alphabet shift state.
+ mAlphabetShiftState.setShiftLocked(false);
+ mPrevMainKeyboardWasShiftLocked = false;
+ mPrevSymbolsKeyboardWasShifted = false;
+ mShiftKeyState.onRelease();
+ mSymbolKeyState.onRelease();
+ onRestoreKeyboardState();
+ }
+
+ public void onSaveKeyboardState() {
+ final SavedKeyboardState state = mSavedKeyboardState;
+ state.mIsAlphabetMode = mIsAlphabetMode;
+ if (mIsAlphabetMode) {
+ state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
+ state.mIsShifted = !state.mIsAlphabetShiftLocked
+ && mAlphabetShiftState.isShiftedOrShiftLocked();
+ } else {
+ state.mIsAlphabetShiftLocked = mPrevMainKeyboardWasShiftLocked;
+ state.mIsShifted = mIsSymbolShifted;
+ }
+ state.mIsValid = true;
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onSaveKeyboardState: saved=" + state + " " + this);
+ }
+ }
+
+ private void onRestoreKeyboardState() {
+ final SavedKeyboardState state = mSavedKeyboardState;
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onRestoreKeyboardState: saved=" + state + " " + this);
+ }
+ if (!state.mIsValid || state.mIsAlphabetMode) {
+ setAlphabetKeyboard();
+ } else {
+ if (state.mIsShifted) {
+ setSymbolsShiftedKeyboard();
+ } else {
+ setSymbolsKeyboard();
+ }
+ }
+
+ if (!state.mIsValid) return;
+ state.mIsValid = false;
+
+ if (state.mIsAlphabetMode) {
+ setShiftLocked(state.mIsAlphabetShiftLocked);
+ if (!state.mIsAlphabetShiftLocked) {
+ setShifted(state.mIsShifted ? MANUAL_SHIFT : UNSHIFT);
+ }
+ } else {
+ mPrevMainKeyboardWasShiftLocked = state.mIsAlphabetShiftLocked;
+ }
+ }
+
+ private static final int UNSHIFT = 0;
+ private static final int MANUAL_SHIFT = 1;
+ private static final int AUTOMATIC_SHIFT = 2;
+ private static final int SHIFT_LOCK_SHIFTED = 3;
+
+ private void setShifted(int shiftMode) {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
+ }
+ if (!mIsAlphabetMode) return;
+ final int prevShiftMode;
+ if (mAlphabetShiftState.isAutomaticShifted()) {
+ prevShiftMode = AUTOMATIC_SHIFT;
+ } else if (mAlphabetShiftState.isManualShifted()) {
+ prevShiftMode = MANUAL_SHIFT;
+ } else {
+ prevShiftMode = UNSHIFT;
+ }
+ switch (shiftMode) {
+ case AUTOMATIC_SHIFT:
+ mAlphabetShiftState.setAutomaticShifted();
+ if (shiftMode != prevShiftMode) {
+ mSwitchActions.setAlphabetAutomaticShiftedKeyboard();
+ }
+ break;
+ case MANUAL_SHIFT:
+ mAlphabetShiftState.setShifted(true);
+ if (shiftMode != prevShiftMode) {
+ mSwitchActions.setAlphabetManualShiftedKeyboard();
+ }
+ break;
+ case UNSHIFT:
+ mAlphabetShiftState.setShifted(false);
+ if (shiftMode != prevShiftMode) {
+ mSwitchActions.setAlphabetKeyboard();
+ }
+ break;
+ case SHIFT_LOCK_SHIFTED:
+ mAlphabetShiftState.setShifted(true);
+ mSwitchActions.setAlphabetShiftLockShiftedKeyboard();
+ break;
+ }
+ }
+
+ private void setShiftLocked(boolean shiftLocked) {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked + " " + this);
+ }
+ if (!mIsAlphabetMode) return;
+ if (shiftLocked && (!mAlphabetShiftState.isShiftLocked()
+ || mAlphabetShiftState.isShiftLockShifted())) {
+ mSwitchActions.setAlphabetShiftLockedKeyboard();
+ }
+ if (!shiftLocked && mAlphabetShiftState.isShiftLocked()) {
+ mSwitchActions.setAlphabetKeyboard();
+ }
+ mAlphabetShiftState.setShiftLocked(shiftLocked);
+ }
+
+ private void toggleAlphabetAndSymbols() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "toggleAlphabetAndSymbols: " + this);
+ }
+ if (mIsAlphabetMode) {
+ mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
+ if (mPrevSymbolsKeyboardWasShifted) {
+ setSymbolsShiftedKeyboard();
+ } else {
+ setSymbolsKeyboard();
+ }
+ mPrevSymbolsKeyboardWasShifted = false;
+ } else {
+ mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
+ setAlphabetKeyboard();
+ if (mPrevMainKeyboardWasShiftLocked) {
+ setShiftLocked(true);
+ }
+ mPrevMainKeyboardWasShiftLocked = false;
+ }
+ }
+
+ private void toggleShiftInSymbols() {
+ if (mIsSymbolShifted) {
+ setSymbolsKeyboard();
+ } else {
+ setSymbolsShiftedKeyboard();
+ }
+ }
+
+ private void setAlphabetKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setAlphabetKeyboard");
+ }
+
+ mSwitchActions.setAlphabetKeyboard();
+ mIsAlphabetMode = true;
+ mIsSymbolShifted = false;
+ mSwitchState = SWITCH_STATE_ALPHA;
+ mSwitchActions.requestUpdatingShiftState();
+ }
+
+ private void setSymbolsKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setSymbolsKeyboard");
+ }
+ mSwitchActions.setSymbolsKeyboard();
+ mIsAlphabetMode = false;
+ mIsSymbolShifted = false;
+ // Reset alphabet shift state.
+ mAlphabetShiftState.setShiftLocked(false);
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ }
+
+ private void setSymbolsShiftedKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setSymbolsShiftedKeyboard");
+ }
+ mSwitchActions.setSymbolsShiftedKeyboard();
+ mIsAlphabetMode = false;
+ mIsSymbolShifted = true;
+ // Reset alphabet shift state.
+ mAlphabetShiftState.setShiftLocked(false);
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ }
+
+ public void onPressKey(int code, boolean isSinglePointer, int autoCaps) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code)
+ + " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this);
+ }
+ if (code == Keyboard.CODE_SHIFT) {
+ onPressShift();
+ } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ onPressSymbol();
+ } else {
+ mSwitchActions.cancelDoubleTapTimer();
+ mSwitchActions.cancelLongPressTimer();
+ mLongPressShiftLockFired = false;
+ mShiftKeyState.onOtherKeyPressed();
+ mSymbolKeyState.onOtherKeyPressed();
+ // It is required to reset the auto caps state when all of the following conditions
+ // are met:
+ // 1) two or more fingers are in action
+ // 2) in alphabet layout
+ // 3) not in all characters caps mode
+ // As for #3, please note that it's required to check even when the auto caps mode is
+ // off because, for example, we may be in the #1 state within the manual temporary
+ // shifted mode.
+ if (!isSinglePointer && mIsAlphabetMode && autoCaps != TextUtils.CAP_MODE_CHARACTERS) {
+ final boolean needsToResetAutoCaps = mAlphabetShiftState.isAutomaticShifted()
+ || (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing());
+ if (needsToResetAutoCaps) {
+ mSwitchActions.setAlphabetKeyboard();
+ }
+ }
+ }
+ }
+
+ public void onReleaseKey(int code, boolean withSliding) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
+ + " sliding=" + withSliding + " " + this);
+ }
+ if (code == Keyboard.CODE_SHIFT) {
+ onReleaseShift(withSliding);
+ } else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ onReleaseSymbol(withSliding);
+ }
+ }
+
+ private void onPressSymbol() {
+ toggleAlphabetAndSymbols();
+ mSymbolKeyState.onPress();
+ mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
+ }
+
+ private void onReleaseSymbol(boolean withSliding) {
+ if (mSymbolKeyState.isChording()) {
+ // Switch back to the previous keyboard mode if the user chords the mode change key and
+ // another key, then releases the mode change key.
+ toggleAlphabetAndSymbols();
+ } else if (!withSliding) {
+ // If the mode change key is being released without sliding, we should forget the
+ // previous symbols keyboard shift state and simply switch back to symbols layout
+ // (never symbols shifted) next time the mode gets changed to symbols layout.
+ mPrevSymbolsKeyboardWasShifted = false;
+ }
+ mSymbolKeyState.onRelease();
+ }
+
+ public void onLongPressTimeout(int code) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " " + this);
+ }
+ if (mIsAlphabetMode && code == Keyboard.CODE_SHIFT) {
+ mLongPressShiftLockFired = true;
+ mSwitchActions.hapticAndAudioFeedback(code);
+ }
+ }
+
+ public void onUpdateShiftState(int autoCaps) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + " " + this);
+ }
+ updateAlphabetShiftState(autoCaps);
+ }
+
+ private void updateAlphabetShiftState(int autoCaps) {
+ if (!mIsAlphabetMode) return;
+ if (!mShiftKeyState.isReleasing()) {
+ // Ignore update shift state event while the shift key is being pressed (including
+ // chording).
+ return;
+ }
+ if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
+ if (mShiftKeyState.isReleasing() && autoCaps != Constants.TextUtils.CAP_MODE_OFF) {
+ // Only when shift key is releasing, automatic temporary upper case will be set.
+ setShifted(AUTOMATIC_SHIFT);
+ } else {
+ setShifted(mShiftKeyState.isChording() ? MANUAL_SHIFT : UNSHIFT);
+ }
+ }
+ }
+
+ private void onPressShift() {
+ mLongPressShiftLockFired = false;
+ if (mIsAlphabetMode) {
+ mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapTimeout();
+ if (!mIsInDoubleTapShiftKey) {
+ // This is first tap.
+ mSwitchActions.startDoubleTapTimer();
+ }
+ if (mIsInDoubleTapShiftKey) {
+ if (mAlphabetShiftState.isManualShifted() || mIsInAlphabetUnshiftedFromShifted) {
+ // Shift key has been double tapped while in manual shifted or automatic
+ // shifted state.
+ setShiftLocked(true);
+ } else {
+ // Shift key has been double tapped while in normal state. This is the second
+ // tap to disable shift locked state, so just ignore this.
+ }
+ } else {
+ if (mAlphabetShiftState.isShiftLocked()) {
+ // Shift key is pressed while shift locked state, we will treat this state as
+ // shift lock shifted state and mark as if shift key pressed while normal state.
+ setShifted(SHIFT_LOCK_SHIFTED);
+ mShiftKeyState.onPress();
+ } else if (mAlphabetShiftState.isAutomaticShifted()) {
+ // Shift key is pressed while automatic shifted, we have to move to manual
+ // shifted.
+ setShifted(MANUAL_SHIFT);
+ mShiftKeyState.onPress();
+ } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
+ // In manual shifted state, we just record shift key has been pressing while
+ // shifted state.
+ mShiftKeyState.onPressOnShifted();
+ } else {
+ // In base layout, chording or manual shifted mode is started.
+ setShifted(MANUAL_SHIFT);
+ mShiftKeyState.onPress();
+ }
+ mSwitchActions.startLongPressTimer(Keyboard.CODE_SHIFT);
+ }
+ } else {
+ // In symbol mode, just toggle symbol and symbol more keyboard.
+ toggleShiftInSymbols();
+ mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
+ mShiftKeyState.onPress();
+ }
+ }
+
+ private void onReleaseShift(boolean withSliding) {
+ if (mIsAlphabetMode) {
+ final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
+ mIsInAlphabetUnshiftedFromShifted = false;
+ if (mIsInDoubleTapShiftKey) {
+ // Double tap shift key has been handled in {@link #onPressShift}, so that just
+ // ignore this release shift key here.
+ mIsInDoubleTapShiftKey = false;
+ } else if (mLongPressShiftLockFired) {
+ setShiftLocked(!mAlphabetShiftState.isShiftLocked());
+ } else if (mShiftKeyState.isChording()) {
+ if (mAlphabetShiftState.isShiftLockShifted()) {
+ // After chording input while shift locked state.
+ setShiftLocked(true);
+ } else {
+ // After chording input while normal state.
+ setShifted(UNSHIFT);
+ }
+ } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) {
+ // In shift locked state, shift has been pressed and slid out to other key.
+ setShiftLocked(true);
+ } else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
+ && (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
+ && !withSliding) {
+ // Shift has been long pressed, ignore this release.
+ } else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
+ // Shift has been pressed without chording while shift locked state.
+ setShiftLocked(false);
+ } else if (mAlphabetShiftState.isShiftedOrShiftLocked()
+ && mShiftKeyState.isPressingOnShifted() && !withSliding) {
+ // Shift has been pressed without chording while shifted state.
+ setShifted(UNSHIFT);
+ mIsInAlphabetUnshiftedFromShifted = true;
+ } else if (mAlphabetShiftState.isManualShiftedFromAutomaticShifted()
+ && mShiftKeyState.isPressing() && !withSliding) {
+ // Shift has been pressed without chording while manual shifted transited from
+ // automatic shifted
+ setShifted(UNSHIFT);
+ mIsInAlphabetUnshiftedFromShifted = true;
+ }
+ } else {
+ // In symbol mode, switch back to the previous keyboard mode if the user chords the
+ // shift key and another key, then releases the shift key.
+ if (mShiftKeyState.isChording()) {
+ toggleShiftInSymbols();
+ }
+ }
+ mShiftKeyState.onRelease();
+ }
+
+ public void onCancelInput(boolean isSinglePointer) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
+ }
+ // Switch back to the previous keyboard mode if the user cancels sliding input.
+ if (isSinglePointer) {
+ if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
+ toggleAlphabetAndSymbols();
+ } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
+ toggleShiftInSymbols();
+ }
+ }
+ }
+
+ public boolean isInMomentarySwitchState() {
+ return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
+ || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
+ }
+
+ private static boolean isSpaceCharacter(int c) {
+ return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER;
+ }
+
+ private boolean isLayoutSwitchBackCharacter(int c) {
+ if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false;
+ if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true;
+ return false;
+ }
+
+ public void onCodeInput(int code, boolean isSinglePointer, int autoCaps) {
+ if (DEBUG_EVENT) {
+ Log.d(TAG, "onCodeInput: code=" + Keyboard.printableCode(code)
+ + " single=" + isSinglePointer
+ + " autoCaps=" + autoCaps + " " + this);
+ }
+
+ switch (mSwitchState) {
+ case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
+ if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
+ // Detected only the mode change key has been pressed, and then released.
+ if (mIsAlphabetMode) {
+ mSwitchState = SWITCH_STATE_ALPHA;
+ } else {
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ }
+ } else if (isSinglePointer) {
+ // Switch back to the previous keyboard mode if the user pressed the mode change key
+ // and slid to other key, then released the finger.
+ // If the user cancels the sliding input, switching back to the previous keyboard
+ // mode is handled by {@link #onCancelInput}.
+ toggleAlphabetAndSymbols();
+ }
+ break;
+ case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
+ if (code == Keyboard.CODE_SHIFT) {
+ // Detected only the shift key has been pressed on symbol layout, and then released.
+ mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
+ } else if (isSinglePointer) {
+ // Switch back to the previous keyboard mode if the user pressed the shift key on
+ // symbol mode and slid to other key, then released the finger.
+ toggleShiftInSymbols();
+ mSwitchState = SWITCH_STATE_SYMBOL;
+ }
+ break;
+ case SWITCH_STATE_SYMBOL_BEGIN:
+ if (!isSpaceCharacter(code) && (Keyboard.isLetterCode(code)
+ || code == Keyboard.CODE_OUTPUT_TEXT)) {
+ mSwitchState = SWITCH_STATE_SYMBOL;
+ }
+ // Switch back to alpha keyboard mode immediately if user types one of the switch back
+ // characters.
+ if (isLayoutSwitchBackCharacter(code)) {
+ toggleAlphabetAndSymbols();
+ mPrevSymbolsKeyboardWasShifted = false;
+ }
+ break;
+ case SWITCH_STATE_SYMBOL:
+ // Switch back to alpha keyboard mode if user types one or more non-space/enter
+ // characters followed by a space/enter or one of the switch back characters.
+ if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
+ toggleAlphabetAndSymbols();
+ mPrevSymbolsKeyboardWasShifted = false;
+ }
+ break;
+ }
+
+ // If the code is a letter, update keyboard shift state.
+ if (Keyboard.isLetterCode(code)) {
+ updateAlphabetShiftState(autoCaps);
+ }
+ }
+
+ private static String shiftModeToString(int shiftMode) {
+ switch (shiftMode) {
+ case UNSHIFT: return "UNSHIFT";
+ case MANUAL_SHIFT: return "MANUAL";
+ case AUTOMATIC_SHIFT: return "AUTOMATIC";
+ default: return null;
+ }
+ }
+
+ private static String switchStateToString(int switchState) {
+ switch (switchState) {
+ case SWITCH_STATE_ALPHA: return "ALPHA";
+ case SWITCH_STATE_SYMBOL_BEGIN: return "SYMBOL-BEGIN";
+ case SWITCH_STATE_SYMBOL: return "SYMBOL";
+ case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
+ case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
+ default: return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
+ : (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
+ + " shift=" + mShiftKeyState
+ + " symbol=" + mSymbolKeyState
+ + " switch=" + switchStateToString(mSwitchState) + "]";
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
new file mode 100644
index 000000000..8c218c6d3
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -0,0 +1,2511 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.inputmethod.latin.R;
+
+import java.util.HashMap;
+
+/**
+ * !!!!! DO NOT EDIT THIS FILE !!!!!
+ *
+ * This file is generated by tools/maketext. The base template file is
+ * tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+ *
+ * This file must be updated when any text resources in keyboard layout files have been changed.
+ * These text resources are referred as "!text/<resource_name>" in keyboard XML definitions,
+ * and should be defined in
+ * tools/maketext/res/values-<locale>/donottranslate-more-keys.xml
+ *
+ * To update this file, please run the following commands.
+ * $ cd $ANDROID_BUILD_TOP
+ * $ mmm packages/inputmethods/LatinIME/tools/maketext
+ * $ maketext -java packages/inputmethods/LatinIME/java/src
+ *
+ * The updated source file will be generated to the following path (this file).
+ * packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/
+ * KeyboardTextsSet.java
+ */
+public final class KeyboardTextsSet {
+ // Language to texts map.
+ private static final HashMap<String, String[]> sLocaleToTextsMap =
+ new HashMap<String, String[]>();
+ private static final HashMap<String, Integer> sNameToIdsMap =
+ new HashMap<String, Integer>();
+
+ private String[] mTexts;
+ // Resource name to text map.
+ private HashMap<String, String> mResourceNameToTextsMap = new HashMap<String, String>();
+
+ public void setLanguage(final String language) {
+ mTexts = sLocaleToTextsMap.get(language);
+ if (mTexts == null) {
+ mTexts = LANGUAGE_DEFAULT;
+ }
+ }
+
+ public void loadStringResources(Context context) {
+ loadStringResourcesInternal(context, RESOURCE_NAMES, R.string.english_ime_name);
+ }
+
+ /* package for test */
+ void loadStringResourcesInternal(Context context, final String[] resourceNames,
+ int referenceId) {
+ final Resources res = context.getResources();
+ final String packageName = res.getResourcePackageName(referenceId);
+ for (final String resName : resourceNames) {
+ final int resId = res.getIdentifier(resName, "string", packageName);
+ mResourceNameToTextsMap.put(resName, res.getString(resId));
+ }
+ }
+
+ public String getText(final String name) {
+ String text = mResourceNameToTextsMap.get(name);
+ if (text != null) {
+ return text;
+ }
+ final Integer id = sNameToIdsMap.get(name);
+ if (id == null) throw new RuntimeException("Unknown label: " + name);
+ text = (id < mTexts.length) ? mTexts[id] : null;
+ return (text == null) ? LANGUAGE_DEFAULT[id] : text;
+ }
+
+ private static final String[] RESOURCE_NAMES = {
+ // These texts' name should be aligned with the @string/<name> in values/strings.xml.
+ // Labels for action.
+ "label_go_key",
+ // "label_search_key",
+ "label_send_key",
+ "label_next_key",
+ "label_done_key",
+ "label_previous_key",
+ // Other labels.
+ "label_to_alpha_key",
+ "label_to_symbol_key",
+ "label_to_symbol_with_microphone_key",
+ "label_pause_key",
+ "label_wait_key",
+ };
+
+ private static final String[] NAMES = {
+ /* 0 */ "more_keys_for_a",
+ /* 1 */ "more_keys_for_e",
+ /* 2 */ "more_keys_for_i",
+ /* 3 */ "more_keys_for_o",
+ /* 4 */ "more_keys_for_u",
+ /* 5 */ "more_keys_for_s",
+ /* 6 */ "more_keys_for_n",
+ /* 7 */ "more_keys_for_c",
+ /* 8 */ "more_keys_for_y",
+ /* 9 */ "more_keys_for_d",
+ /* 10 */ "more_keys_for_r",
+ /* 11 */ "more_keys_for_t",
+ /* 12 */ "more_keys_for_z",
+ /* 13 */ "more_keys_for_k",
+ /* 14 */ "more_keys_for_l",
+ /* 15 */ "more_keys_for_g",
+ /* 16 */ "more_keys_for_v",
+ /* 17 */ "more_keys_for_h",
+ /* 18 */ "more_keys_for_j",
+ /* 19 */ "more_keys_for_w",
+ /* 20 */ "keylabel_for_nordic_row1_11",
+ /* 21 */ "keylabel_for_nordic_row2_10",
+ /* 22 */ "keylabel_for_nordic_row2_11",
+ /* 23 */ "more_keys_for_nordic_row2_10",
+ /* 24 */ "more_keys_for_nordic_row2_11",
+ /* 25 */ "keylabel_for_east_slavic_row1_9",
+ /* 26 */ "keylabel_for_east_slavic_row2_1",
+ /* 27 */ "keylabel_for_east_slavic_row3_5",
+ /* 28 */ "more_keys_for_cyrillic_u",
+ /* 29 */ "more_keys_for_cyrillic_ye",
+ /* 30 */ "more_keys_for_cyrillic_en",
+ /* 31 */ "more_keys_for_cyrillic_ha",
+ /* 32 */ "more_keys_for_east_slavic_row2_1",
+ /* 33 */ "more_keys_for_cyrillic_o",
+ /* 34 */ "more_keys_for_cyrillic_soft_sign",
+ /* 35 */ "keylabel_for_south_slavic_row1_6",
+ /* 36 */ "keylabel_for_south_slavic_row2_11",
+ /* 37 */ "keylabel_for_south_slavic_row3_1",
+ /* 38 */ "keylabel_for_south_slavic_row3_8",
+ /* 39 */ "more_keys_for_cyrillic_ie",
+ /* 40 */ "more_keys_for_cyrillic_i",
+ /* 41 */ "more_keys_for_single_quote",
+ /* 42 */ "more_keys_for_double_quote",
+ /* 43 */ "more_keys_for_tablet_double_quote",
+ /* 44 */ "more_keys_for_currency_dollar",
+ /* 45 */ "more_keys_for_currency_euro",
+ /* 46 */ "more_keys_for_currency_pound",
+ /* 47 */ "more_keys_for_currency_general",
+ /* 48 */ "more_keys_for_punctuation",
+ /* 49 */ "more_keys_for_star",
+ /* 50 */ "more_keys_for_bullet",
+ /* 51 */ "more_keys_for_plus",
+ /* 52 */ "more_keys_for_left_parenthesis",
+ /* 53 */ "more_keys_for_right_parenthesis",
+ /* 54 */ "more_keys_for_less_than",
+ /* 55 */ "more_keys_for_greater_than",
+ /* 56 */ "more_keys_for_arabic_diacritics",
+ /* 57 */ "keyhintlabel_for_arabic_diacritics",
+ /* 58 */ "keylabel_for_symbols_1",
+ /* 59 */ "keylabel_for_symbols_2",
+ /* 60 */ "keylabel_for_symbols_3",
+ /* 61 */ "keylabel_for_symbols_4",
+ /* 62 */ "keylabel_for_symbols_5",
+ /* 63 */ "keylabel_for_symbols_6",
+ /* 64 */ "keylabel_for_symbols_7",
+ /* 65 */ "keylabel_for_symbols_8",
+ /* 66 */ "keylabel_for_symbols_9",
+ /* 67 */ "keylabel_for_symbols_0",
+ /* 68 */ "additional_more_keys_for_symbols_1",
+ /* 69 */ "additional_more_keys_for_symbols_2",
+ /* 70 */ "additional_more_keys_for_symbols_3",
+ /* 71 */ "additional_more_keys_for_symbols_4",
+ /* 72 */ "additional_more_keys_for_symbols_5",
+ /* 73 */ "additional_more_keys_for_symbols_6",
+ /* 74 */ "additional_more_keys_for_symbols_7",
+ /* 75 */ "additional_more_keys_for_symbols_8",
+ /* 76 */ "additional_more_keys_for_symbols_9",
+ /* 77 */ "additional_more_keys_for_symbols_0",
+ /* 78 */ "more_keys_for_symbols_1",
+ /* 79 */ "more_keys_for_symbols_2",
+ /* 80 */ "more_keys_for_symbols_3",
+ /* 81 */ "more_keys_for_symbols_4",
+ /* 82 */ "more_keys_for_symbols_5",
+ /* 83 */ "more_keys_for_symbols_6",
+ /* 84 */ "more_keys_for_symbols_7",
+ /* 85 */ "more_keys_for_symbols_8",
+ /* 86 */ "more_keys_for_symbols_9",
+ /* 87 */ "more_keys_for_symbols_0",
+ /* 88 */ "keylabel_for_comma",
+ /* 89 */ "more_keys_for_comma",
+ /* 90 */ "keylabel_for_symbols_exclamation",
+ /* 91 */ "keylabel_for_symbols_question",
+ /* 92 */ "keylabel_for_symbols_semicolon",
+ /* 93 */ "keylabel_for_symbols_percent",
+ /* 94 */ "more_keys_for_symbols_exclamation",
+ /* 95 */ "more_keys_for_symbols_question",
+ /* 96 */ "more_keys_for_symbols_semicolon",
+ /* 97 */ "more_keys_for_symbols_percent",
+ /* 98 */ "keylabel_for_tablet_comma",
+ /* 99 */ "keyhintlabel_for_tablet_comma",
+ /* 100 */ "more_keys_for_tablet_comma",
+ /* 101 */ "keyhintlabel_for_tablet_period",
+ /* 102 */ "more_keys_for_tablet_period",
+ /* 103 */ "keylabel_for_apostrophe",
+ /* 104 */ "keyhintlabel_for_apostrophe",
+ /* 105 */ "more_keys_for_apostrophe",
+ /* 106 */ "more_keys_for_am_pm",
+ /* 107 */ "settings_as_more_key",
+ /* 108 */ "shortcut_as_more_key",
+ /* 109 */ "action_next_as_more_key",
+ /* 110 */ "action_previous_as_more_key",
+ /* 111 */ "label_to_more_symbol_key",
+ /* 112 */ "label_to_more_symbol_for_tablet_key",
+ /* 113 */ "label_tab_key",
+ /* 114 */ "label_to_phone_numeric_key",
+ /* 115 */ "label_to_phone_symbols_key",
+ /* 116 */ "label_time_am",
+ /* 117 */ "label_time_pm",
+ /* 118 */ "label_to_symbol_key_pcqwerty",
+ /* 119 */ "keylabel_for_popular_domain",
+ /* 120 */ "more_keys_for_popular_domain",
+ /* 121 */ "more_keys_for_smiley",
+ };
+
+ private static final String EMPTY = "";
+
+ /* Default texts */
+ private static final String[] LANGUAGE_DEFAULT = {
+ /* 0~ */
+ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
+ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
+ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
+ EMPTY, EMPTY,
+ /* ~40 */
+ /* 41 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;</string>
+ /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ // U+00A2: "¢" CENT SIGN
+ // U+00A3: "£" POUND SIGN
+ // U+20AC: "€" EURO SIGN
+ // U+00A5: "¥" YEN SIGN
+ // U+20B1: "₱" PESO SIGN
+ /* 44 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
+ /* 45 */ "\u00A2,\u00A3,$,\u00A5,\u20B1",
+ /* 46 */ "\u00A2,$,\u20AC,\u00A5,\u20B1",
+ /* 47 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1",
+ /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+ // U+2020: "†" DAGGER
+ // U+2021: "‡" DOUBLE DAGGER
+ // U+2605: "★" BLACK STAR
+ /* 49 */ "\u2020,\u2021,\u2605",
+ // U+266A: "♪" EIGHTH NOTE
+ // U+2665: "♥" BLACK HEART SUIT
+ // U+2660: "♠" BLACK SPADE SUIT
+ // U+2666: "♦" BLACK DIAMOND SUIT
+ // U+2663: "♣" BLACK CLUB SUIT
+ /* 50 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
+ // U+00B1: "±" PLUS-MINUS SIGN
+ /* 51 */ "\u00B1",
+ // The all letters need to be mirrored are found at
+ // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
+ /* 52 */ "!fixedColumnOrder!3,<,{,[",
+ /* 53 */ "!fixedColumnOrder!3,>,},]",
+ // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ // U+2264: "≤" LESS-THAN OR EQUAL TO
+ // U+2265: "≥" GREATER-THAN EQUAL TO
+ // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // The following characters don't need BIDI mirroring.
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ /* 54 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
+ /* 55 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
+ /* 56 */ EMPTY,
+ /* 57 */ EMPTY,
+ /* 58 */ "1",
+ /* 59 */ "2",
+ /* 60 */ "3",
+ /* 61 */ "4",
+ /* 62 */ "5",
+ /* 63 */ "6",
+ /* 64 */ "7",
+ /* 65 */ "8",
+ /* 66 */ "9",
+ /* 67 */ "0",
+ /* 68~ */
+ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
+ /* ~77 */
+ // U+00B9: "¹" SUPERSCRIPT ONE
+ // U+00BD: "½" VULGAR FRACTION ONE HALF
+ // U+2153: "⅓" VULGAR FRACTION ONE THIRD
+ // U+00BC: "¼" VULGAR FRACTION ONE QUARTER
+ // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH
+ /* 78 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
+ // U+00B2: "²" SUPERSCRIPT TWO
+ // U+2154: "⅔" VULGAR FRACTION TWO THIRDS
+ /* 79 */ "\u00B2,\u2154",
+ // U+00B3: "³" SUPERSCRIPT THREE
+ // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS
+ // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS
+ /* 80 */ "\u00B3,\u00BE,\u215C",
+ // U+2074: "⁴" SUPERSCRIPT FOUR
+ /* 81 */ "\u2074",
+ // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS
+ /* 82 */ "\u215D",
+ /* 83 */ EMPTY,
+ // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS
+ /* 84 */ "\u215E",
+ /* 85 */ EMPTY,
+ /* 86 */ EMPTY,
+ // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N
+ // U+2205: "∅" EMPTY SET
+ /* 87 */ "\u207F,\u2205",
+ /* 88 */ ",",
+ /* 89 */ EMPTY,
+ /* 90 */ "!",
+ /* 91 */ "?",
+ /* 92 */ ";",
+ /* 93 */ "%",
+ // U+00A1: "¡" INVERTED EXCLAMATION MARK
+ /* 94 */ "\u00A1",
+ // U+00BF: "¿" INVERTED QUESTION MARK
+ /* 95 */ "\u00BF",
+ /* 96 */ EMPTY,
+ // U+2030: "‰" PER MILLE SIGN
+ /* 97 */ "\u2030",
+ /* 98 */ ",",
+ /* 99 */ "!",
+ /* 100 */ "!",
+ /* 101 */ "?",
+ /* 102 */ "?",
+ /* 103 */ "\'",
+ /* 104 */ "\"",
+ /* 105 */ "\"",
+ /* 106 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
+ /* 107 */ "!icon/settings_key|!code/key_settings",
+ /* 108 */ "!icon/shortcut_key|!code/key_shortcut",
+ /* 109 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
+ /* 110 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
+ // Label for "switch to more symbol" modifier key. Must be short to fit on key!
+ /* 111 */ "= \\ <",
+ // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key!
+ /* 112 */ "~ \\ {",
+ // Label for "Tab" key. Must be short to fit on key!
+ /* 113 */ "Tab",
+ // Label for "switch to phone numeric" key. Must be short to fit on key!
+ /* 114 */ "123",
+ // Label for "switch to phone symbols" key. Must be short to fit on key!
+ // U+FF0A: "*" FULLWIDTH ASTERISK
+ // U+FF03: "#" FULLWIDTH NUMBER SIGN
+ /* 115 */ "\uFF0A\uFF03",
+ // Key label for "ante meridiem"
+ /* 116 */ "AM",
+ // Key label for "post meridiem"
+ /* 117 */ "PM",
+ // Label for "switch to symbols" key on PC QWERTY layout
+ /* 118 */ "Sym",
+ /* 119 */ ".com",
+ // popular web domains for the locale - most popular, displayed on the keyboard
+ /* 120 */ "!hasLabels!,.net,.org,.gov,.edu",
+ /* 121 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+ };
+
+ /* Language ar: Arabic */
+ private static final String[] LANGUAGE_ar = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~41 */
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
+ /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 44~ */
+ null, null, null, null,
+ /* ~47 */
+ // U+061F: "؟" ARABIC QUESTION MARK
+ // U+060C: "،" ARABIC COMMA
+ // U+061B: "؛" ARABIC SEMICOLON
+ /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ // U+2605: "★" BLACK STAR
+ // U+066D: "٭" ARABIC FIVE POINTED STAR
+ /* 49 */ "\u2605,\u066D",
+ // U+266A: "♪" EIGHTH NOTE
+ /* 50 */ "\u266A",
+ /* 51 */ null,
+ // The all letters need to be mirrored are found at
+ // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
+ // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS
+ // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS
+ /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+ /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ // U+2264: "≤" LESS-THAN OR EQUAL TO
+ // U+2265: "≥" GREATER-THAN EQUAL TO
+ // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ // The following characters don't need BIDI mirroring.
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+ /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ // U+0655: "ٕ" ARABIC HAMZA BELOW
+ // U+0654: "ٔ" ARABIC HAMZA ABOVE
+ // U+0652: "ْ" ARABIC SUKUN
+ // U+064D: "ٍ" ARABIC KASRATAN
+ // U+064C: "ٌ" ARABIC DAMMATAN
+ // U+064B: "ً" ARABIC FATHATAN
+ // U+0651: "ّ" ARABIC SHADDA
+ // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF
+ // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF
+ // U+0653: "ٓ" ARABIC MADDAH ABOVE
+ // U+0650: "ِ" ARABIC KASRA
+ // U+064F: "ُ" ARABIC DAMMA
+ // U+064E: "َ" ARABIC FATHA
+ // U+0640: "ـ" ARABIC TATWEEL
+ // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
+ /* 56 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640",
+ /* 57 */ "\u0651",
+ // U+0661: "١" ARABIC-INDIC DIGIT ONE
+ /* 58 */ "\u0661",
+ // U+0662: "٢" ARABIC-INDIC DIGIT TWO
+ /* 59 */ "\u0662",
+ // U+0663: "٣" ARABIC-INDIC DIGIT THREE
+ /* 60 */ "\u0663",
+ // U+0664: "٤" ARABIC-INDIC DIGIT FOUR
+ /* 61 */ "\u0664",
+ // U+0665: "٥" ARABIC-INDIC DIGIT FIVE
+ /* 62 */ "\u0665",
+ // U+0666: "٦" ARABIC-INDIC DIGIT SIX
+ /* 63 */ "\u0666",
+ // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN
+ /* 64 */ "\u0667",
+ // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT
+ /* 65 */ "\u0668",
+ // U+0669: "٩" ARABIC-INDIC DIGIT NINE
+ /* 66 */ "\u0669",
+ // U+0660: "٠" ARABIC-INDIC DIGIT ZERO
+ /* 67 */ "\u0660",
+ /* 68 */ "1",
+ /* 69 */ "2",
+ /* 70 */ "3",
+ /* 71 */ "4",
+ /* 72 */ "5",
+ /* 73 */ "6",
+ /* 74 */ "7",
+ /* 75 */ "8",
+ /* 76 */ "9",
+ // U+066B: "٫" ARABIC DECIMAL SEPARATOR
+ // U+066C: "٬" ARABIC THOUSANDS SEPARATOR
+ /* 77 */ "0,\u066B,\u066C",
+ /* 78~ */
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~87 */
+ // U+060C: "،" ARABIC COMMA
+ /* 88 */ "\u060C",
+ /* 89 */ "\\,",
+ /* 90 */ null,
+ /* 91 */ "\u061F",
+ /* 92 */ "\u061B",
+ // U+066A: "٪" ARABIC PERCENT SIGN
+ /* 93 */ "\u066A",
+ /* 94 */ null,
+ /* 95 */ "?",
+ /* 96 */ ";",
+ // U+2030: "‰" PER MILLE SIGN
+ /* 97 */ "\\%,\u2030",
+ /* 98~ */
+ null, null, null, null, null,
+ /* ~102 */
+ // U+060C: "،" ARABIC COMMA
+ // U+061B: "؛" ARABIC SEMICOLON
+ // U+061F: "؟" ARABIC QUESTION MARK
+ /* 103 */ "\u060C",
+ /* 104 */ "\u061F",
+ /* 105 */ "\u061F,\u061B,!,:,-,/,\',\"",
+ };
+
+ /* Language be: Belarusian */
+ private static final String[] LANGUAGE_be = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~24 */
+ // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U
+ /* 25 */ "\u045E",
+ // U+044B: "ы" CYRILLIC SMALL LETTER YERU
+ /* 26 */ "\u044B",
+ // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
+ /* 27 */ "\u0456",
+ /* 28~ */
+ null, null, null,
+ /* ~30 */
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 31 */ "\u044A",
+ /* 32 */ null,
+ /* 33 */ null,
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 34 */ "\u044A",
+ };
+
+ /* Language ca: Catalan */
+ private static final String[] LANGUAGE_ca = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E0,\u00E1,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B",
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B",
+ /* 5 */ null,
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u010D",
+ /* 8~ */
+ null, null, null, null, null, null,
+ /* ~13 */
+ // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u0140,\u0142",
+ };
+
+ /* Language cs: Czech */
+ private static final String[] LANGUAGE_cs = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ /* 5 */ "\u0161,\u00DF,\u015B",
+ // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u0148,\u00F1,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u00E7,\u0107",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ /* 9 */ "\u010F",
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ /* 10 */ "\u0159",
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ /* 11 */ "\u0165",
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ /* 12 */ "\u017E,\u017A,\u017C",
+ };
+
+ /* Language da: Danish */
+ private static final String[] LANGUAGE_da = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ /* 1 */ "\u00E9,\u00EB",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ /* 2 */ "\u00ED,\u00EF",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u00DF,\u015B,\u0161",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ /* 7 */ null,
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+00F0: "ð" LATIN SMALL LETTER ETH
+ /* 9 */ "\u00F0",
+ /* 10~ */
+ null, null, null, null,
+ /* ~13 */
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u0142",
+ /* 15~ */
+ null, null, null, null, null,
+ /* ~19 */
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ /* 20 */ "\u00E5",
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 21 */ "\u00E6",
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 22 */ "\u00F8",
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ /* 23 */ "\u00E4",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ /* 24 */ "\u00F6",
+ };
+
+ /* Language de: German */
+ private static final String[] LANGUAGE_de = {
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E4,\u00E2,\u00E0,\u00E1,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ /* 1 */ "\u0117",
+ /* 2 */ null,
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F6,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u00DF,\u015B,\u0161",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ };
+
+ /* Language en: English */
+ private static final String[] LANGUAGE_en = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113",
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC",
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5",
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ /* 5 */ "\u00DF",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ /* 6 */ "\u00F1",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ /* 7 */ "\u00E7",
+ };
+
+ /* Language es: Spanish */
+ private static final String[] LANGUAGE_es = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B",
+ /* 5 */ null,
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u010D",
+ /* 8~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~47 */
+ // U+00A1: "¡" INVERTED EXCLAMATION MARK
+ // U+00BF: "¿" INVERTED QUESTION MARK
+ /* 48 */ "!fixedColumnOrder!9,\",\',#,-,\u00A1,!,\u00BF,\\,,?,@,&,\\%,+,;,:,/,(,)",
+ /* 49~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null,
+ /* ~89 */
+ // U+00A1: "¡" INVERTED EXCLAMATION MARK
+ /* 90 */ "\u00A1",
+ // U+00BF: "¿" INVERTED QUESTION MARK
+ /* 91 */ "\u00BF",
+ /* 92 */ null,
+ /* 93 */ null,
+ /* 94 */ "!",
+ /* 95 */ "?",
+ /* 96~ */
+ null, null, null,
+ /* ~98 */
+ /* 99 */ "\u00A1",
+ /* 100 */ "\u00A1,!",
+ /* 101 */ "\u00BF",
+ /* 102 */ "\u00BF,?",
+ };
+
+ /* Language et: Estonian */
+ private static final String[] LANGUAGE_et = {
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ /* 0 */ "\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6,\u0105",
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ /* 1 */ "\u0113,\u00E8,\u0117,\u00E9,\u00EA,\u00EB,\u0119,\u011B",
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ /* 2 */ "\u012B,\u00EC,\u012F,\u00ED,\u00EE,\u00EF,\u0131",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ /* 4 */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ /* 5 */ "\u0161,\u00DF,\u015B,\u015F",
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u0146,\u00F1,\u0144,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u00E7,\u0107",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ /* 9 */ "\u010F",
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ /* 10 */ "\u0157,\u0159,\u0155",
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ /* 11 */ "\u0163,\u0165",
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ /* 12 */ "\u017E,\u017C,\u017A",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ /* 13 */ "\u0137",
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ /* 14 */ "\u013C,\u0142,\u013A,\u013E",
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ /* 15 */ "\u0123,\u011F",
+ /* 16~ */
+ null, null, null, null,
+ /* ~19 */
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ /* 20 */ "\u00FC",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ /* 21 */ "\u00F6",
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ /* 22 */ "\u00E4",
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ /* 23 */ "\u00F5",
+ };
+
+ /* Language fa: Persian */
+ private static final String[] LANGUAGE_fa = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~41 */
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
+ /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 44~ */
+ null, null, null, null,
+ /* ~47 */
+ // U+061F: "؟" ARABIC QUESTION MARK
+ // U+060C: "،" ARABIC COMMA
+ // U+061B: "؛" ARABIC SEMICOLON
+ /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ // U+2605: "★" BLACK STAR
+ // U+066D: "٭" ARABIC FIVE POINTED STAR
+ /* 49 */ "\u2605,\u066D",
+ // U+266A: "♪" EIGHTH NOTE
+ /* 50 */ "\u266A",
+ /* 51 */ null,
+ // The all letters need to be mirrored are found at
+ // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
+ // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS
+ // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS
+ /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+ /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ // U+2264: "≤" LESS-THAN OR EQUAL TO
+ // U+2265: "≥" GREATER-THAN EQUAL TO
+ // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ // The following characters don't need BIDI mirroring.
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
+ /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
+ // U+0655: "ٕ" ARABIC HAMZA BELOW
+ // U+0652: "ْ" ARABIC SUKUN
+ // U+0651: "ّ" ARABIC SHADDA
+ // U+064C: "ٌ" ARABIC DAMMATAN
+ // U+064D: "ٍ" ARABIC KASRATAN
+ // U+064B: "ً" ARABIC FATHATAN
+ // U+0654: "ٔ" ARABIC HAMZA ABOVE
+ // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF
+ // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF
+ // U+0653: "ٓ" ARABIC MADDAH ABOVE
+ // U+064F: "ُ" ARABIC DAMMA
+ // U+0650: "ِ" ARABIC KASRA
+ // U+064E: "َ" ARABIC FATHA
+ // U+0640: "ـ" ARABIC TATWEEL
+ // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
+ /* 56 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640",
+ /* 57 */ "\u064B",
+ // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE
+ /* 58 */ "\u06F1",
+ // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO
+ /* 59 */ "\u06F2",
+ // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE
+ /* 60 */ "\u06F3",
+ // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR
+ /* 61 */ "\u06F4",
+ // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE
+ /* 62 */ "\u06F5",
+ // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX
+ /* 63 */ "\u06F6",
+ // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN
+ /* 64 */ "\u06F7",
+ // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT
+ /* 65 */ "\u06F8",
+ // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE
+ /* 66 */ "\u06F9",
+ // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO
+ /* 67 */ "\u06F0",
+ /* 68 */ "1",
+ /* 69 */ "2",
+ /* 70 */ "3",
+ /* 71 */ "4",
+ /* 72 */ "5",
+ /* 73 */ "6",
+ /* 74 */ "7",
+ /* 75 */ "8",
+ /* 76 */ "9",
+ // U+066B: "٫" ARABIC DECIMAL SEPARATOR
+ // U+066C: "٬" ARABIC THOUSANDS SEPARATOR
+ /* 77 */ "0,\u066B,\u066C",
+ /* 78~ */
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~87 */
+ // U+060C: "،" ARABIC COMMA
+ /* 88 */ "\u060C",
+ /* 89 */ "\\,",
+ /* 90 */ null,
+ /* 91 */ "\u061F",
+ /* 92 */ "\u061B",
+ // U+066A: "٪" ARABIC PERCENT SIGN
+ /* 93 */ "\u066A",
+ /* 94 */ null,
+ /* 95 */ "?",
+ /* 96 */ ";",
+ // U+2030: "‰" PER MILLE SIGN
+ /* 97 */ "\\%,\u2030",
+ // U+060C: "،" ARABIC COMMA
+ // U+061B: "؛" ARABIC SEMICOLON
+ // U+061F: "؟" ARABIC QUESTION MARK
+ // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ /* 98 */ "\u060C",
+ /* 99 */ "!",
+ /* 100 */ "!,\\,",
+ /* 101 */ "\u061F",
+ /* 102 */ "\u061F,?",
+ /* 103 */ "\u060C",
+ /* 104 */ "\u061F",
+ /* 105 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB",
+ };
+
+ /* Language fi: Finnish */
+ private static final String[] LANGUAGE_fi = {
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101",
+ /* 1 */ null,
+ /* 2 */ null,
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ /* 4 */ "\u00FC",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ /* 5 */ "\u0161,\u00DF,\u015B",
+ /* 6~ */
+ null, null, null, null, null, null,
+ /* ~11 */
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ /* 12 */ "\u017E,\u017A,\u017C",
+ /* 13~ */
+ null, null, null, null, null, null, null,
+ /* ~19 */
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ /* 20 */ "\u00E5",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ /* 21 */ "\u00F6",
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ /* 22 */ "\u00E4",
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 23 */ "\u00F8",
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 24 */ "\u00E6",
+ };
+
+ /* Language fr: French */
+ private static final String[] LANGUAGE_fr = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E0,\u00E2,%,\u00E6,\u00E1,\u00E4,\u00E3,\u00E5,\u0101,\u00AA",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,%,\u0119,\u0117,\u0113",
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B",
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F4,\u0153,%,\u00F6,\u00F2,\u00F3,\u00F5,\u00F8,\u014D,\u00BA",
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B",
+ /* 5 */ null,
+ /* 6 */ null,
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u010D",
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "%,\u00FF",
+ };
+
+ /* Language hi: Hindi */
+ private static final String[] LANGUAGE_hi = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~57 */
+ // U+0967: "१" DEVANAGARI DIGIT ONE
+ /* 58 */ "\u0967",
+ // U+0968: "२" DEVANAGARI DIGIT TWO
+ /* 59 */ "\u0968",
+ // U+0969: "३" DEVANAGARI DIGIT THREE
+ /* 60 */ "\u0969",
+ // U+096A: "४" DEVANAGARI DIGIT FOUR
+ /* 61 */ "\u096A",
+ // U+096B: "५" DEVANAGARI DIGIT FIVE
+ /* 62 */ "\u096B",
+ // U+096C: "६" DEVANAGARI DIGIT SIX
+ /* 63 */ "\u096C",
+ // U+096D: "७" DEVANAGARI DIGIT SEVEN
+ /* 64 */ "\u096D",
+ // U+096E: "८" DEVANAGARI DIGIT EIGHT
+ /* 65 */ "\u096E",
+ // U+096F: "९" DEVANAGARI DIGIT NINE
+ /* 66 */ "\u096F",
+ // U+0966: "०" DEVANAGARI DIGIT ZERO
+ /* 67 */ "\u0966",
+ /* 68 */ "1",
+ /* 69 */ "2",
+ /* 70 */ "3",
+ /* 71 */ "4",
+ /* 72 */ "5",
+ /* 73 */ "6",
+ /* 74 */ "7",
+ /* 75 */ "8",
+ /* 76 */ "9",
+ /* 77 */ "0",
+ };
+
+ /* Language hr: Croatian */
+ private static final String[] LANGUAGE_hr = {
+ /* 0~ */
+ null, null, null, null, null,
+ /* ~4 */
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ /* 5 */ "\u0161,\u015B,\u00DF",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ /* 7 */ "\u010D,\u0107,\u00E7",
+ /* 8 */ null,
+ // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+ /* 9 */ "\u0111",
+ /* 10 */ null,
+ /* 11 */ null,
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ /* 12 */ "\u017E,\u017A,\u017C",
+ };
+
+ /* Language hu: Hungarian */
+ private static final String[] LANGUAGE_hu = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F6,\u0151,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B",
+ };
+
+ /* Language is: Icelandic */
+ private static final String[] LANGUAGE_is = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E4,\u00E6,\u00E5,\u00E0,\u00E2,\u00E3,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EF,\u00EE,\u00EC,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B",
+ /* 5~ */
+ null, null, null,
+ /* ~7 */
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+00F0: "ð" LATIN SMALL LETTER ETH
+ /* 9 */ "\u00F0",
+ /* 10 */ null,
+ // U+00FE: "þ" LATIN SMALL LETTER THORN
+ /* 11 */ "\u00FE",
+ /* 12~ */
+ null, null, null, null, null, null, null, null,
+ /* ~19 */
+ // U+00F0: "ð" LATIN SMALL LETTER ETH
+ /* 20 */ "\u00F0",
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 21 */ "\u00E6",
+ // U+00FE: "þ" LATIN SMALL LETTER THORN
+ /* 22 */ "\u00FE",
+ };
+
+ /* Language it: Italian */
+ private static final String[] LANGUAGE_it = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u00AA",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u012F,\u012B",
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F6,\u00F5,\u0153,\u00F8,\u014D,\u00BA",
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B",
+ };
+
+ /* Language iw: Hebrew */
+ private static final String[] LANGUAGE_iw = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~41 */
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
+ /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 44~ */
+ null, null, null, null, null,
+ /* ~48 */
+ // U+2605: "★" BLACK STAR
+ /* 49 */ "\u2605",
+ /* 50 */ null,
+ // U+00B1: "±" PLUS-MINUS SIGN
+ // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN
+ /* 51 */ "\u00B1,\uFB29",
+ // The all letters need to be mirrored are found at
+ // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
+ /* 52 */ "!fixedColumnOrder!3,<|>,{|},[|]",
+ /* 53 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
+ // U+2264: "≤" LESS-THAN OR EQUAL TO
+ // U+2265: "≥" GREATER-THAN EQUAL TO
+ // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK
+ // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
+ // The following characters don't need BIDI mirroring.
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+ /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ };
+
+ /* Language ky: Kirghiz */
+ private static final String[] LANGUAGE_ky = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~24 */
+ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
+ /* 25 */ "\u0449",
+ // U+044B: "ы" CYRILLIC SMALL LETTER YERU
+ /* 26 */ "\u044B",
+ // U+0438: "и" CYRILLIC SMALL LETTER I
+ /* 27 */ "\u0438",
+ // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U
+ /* 28 */ "\u04AF",
+ /* 29 */ null,
+ // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER
+ /* 30 */ "\u04A3",
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 31 */ "\u044A",
+ /* 32 */ null,
+ // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O
+ /* 33 */ "\u04E9",
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 34 */ "\u044A",
+ };
+
+ /* Language lt: Lithuanian */
+ private static final String[] LANGUAGE_lt = {
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 0 */ "\u0105,\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6",
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ /* 1 */ "\u0117,\u0119,\u0113,\u00E8,\u00E9,\u00EA,\u00EB,\u011B",
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ /* 2 */ "\u012F,\u012B,\u00EC,\u00ED,\u00EE,\u00EF,\u0131",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8",
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ /* 4 */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ /* 5 */ "\u0161,\u00DF,\u015B,\u015F",
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u0146,\u00F1,\u0144,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u00E7,\u0107",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ /* 9 */ "\u010F",
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ /* 10 */ "\u0157,\u0159,\u0155",
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ /* 11 */ "\u0163,\u0165",
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ /* 12 */ "\u017E,\u017C,\u017A",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ /* 13 */ "\u0137",
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ /* 14 */ "\u013C,\u0142,\u013A,\u013E",
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ /* 15 */ "\u0123,\u011F",
+ };
+
+ /* Language lv: Latvian */
+ private static final String[] LANGUAGE_lv = {
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ /* 0 */ "\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0105",
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ /* 1 */ "\u0113,\u0117,\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u011B",
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ /* 2 */ "\u012B,\u012F,\u00EC,\u00ED,\u00EE,\u00EF,\u0131",
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u0153,\u0151,\u00F8",
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ /* 4 */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ /* 5 */ "\u0161,\u00DF,\u015B,\u015F",
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u0146,\u00F1,\u0144,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u00E7,\u0107",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ /* 9 */ "\u010F",
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ /* 10 */ "\u0157,\u0159,\u0155",
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ /* 11 */ "\u0163,\u0165",
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ /* 12 */ "\u017E,\u017C,\u017A",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ /* 13 */ "\u0137",
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ /* 14 */ "\u013C,\u0142,\u013A,\u013E",
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ /* 15 */ "\u0123,\u011F",
+ };
+
+ /* Language mk: Macedonian */
+ private static final String[] LANGUAGE_mk = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null,
+ /* ~34 */
+ // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
+ /* 35 */ "\u0455",
+ // U+045C: "ќ" CYRILLIC SMALL LETTER KJE
+ /* 36 */ "\u045C",
+ // U+0437: "з" CYRILLIC SMALL LETTER ZE
+ /* 37 */ "\u0437",
+ // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE
+ /* 38 */ "\u0453",
+ // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
+ /* 39 */ "\u0450",
+ // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
+ /* 40 */ "\u045D",
+ /* 41 */ null,
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
+ /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ };
+
+ /* Language nb: Norwegian Bokmål */
+ private static final String[] LANGUAGE_nb = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E0,\u00E4,\u00E1,\u00E2,\u00E3,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ /* 2 */ null,
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F4,\u00F2,\u00F3,\u00F6,\u00F5,\u0153,\u014D",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B",
+ /* 5~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~19 */
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ /* 20 */ "\u00E5",
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 21 */ "\u00F8",
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 22 */ "\u00E6",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ /* 23 */ "\u00F6",
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ /* 24 */ "\u00E4",
+ };
+
+ /* Language nl: Dutch */
+ private static final String[] LANGUAGE_nl = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E4,\u00E2,\u00E0,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B,\u0133",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B",
+ /* 5 */ null,
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ /* 7 */ null,
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 8 */ "\u0133",
+ };
+
+ /* Language pl: Polish */
+ private static final String[] LANGUAGE_pl = {
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u0105,\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u0119,\u00E8,\u00E9,\u00EA,\u00EB,\u0117,\u0113",
+ /* 2 */ null,
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ /* 4 */ null,
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u015B,\u00DF,\u0161",
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ /* 6 */ "\u0144,\u00F1",
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u0107,\u00E7,\u010D",
+ /* 8~ */
+ null, null, null, null,
+ /* ~11 */
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ /* 12 */ "\u017C,\u017A,\u017E",
+ /* 13 */ null,
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u0142",
+ };
+
+ /* Language pt: Portuguese */
+ private static final String[] LANGUAGE_pt = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E1,\u00E3,\u00E0,\u00E2,\u00E4,\u00E5,\u00E6,\u00AA",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ /* 1 */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F3,\u00F5,\u00F4,\u00F2,\u00F6,\u0153,\u00F8,\u014D,\u00BA",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B",
+ /* 5 */ null,
+ /* 6 */ null,
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u00E7,\u010D,\u0107",
+ };
+
+ /* Language rm: Raeto-Romance */
+ private static final String[] LANGUAGE_rm = {
+ /* 0~ */
+ null, null, null,
+ /* ~2 */
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 3 */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u0153,\u00F8",
+ };
+
+ /* Language ro: Romanian */
+ private static final String[] LANGUAGE_ro = {
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E2,\u00E3,\u0103,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101",
+ /* 1 */ null,
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B",
+ /* 3 */ null,
+ /* 4 */ null,
+ // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u0219,\u00DF,\u015B,\u0161",
+ /* 6~ */
+ null, null, null, null, null,
+ /* ~10 */
+ // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW
+ /* 11 */ "\u021B",
+ };
+
+ /* Language ru: Russian */
+ private static final String[] LANGUAGE_ru = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~24 */
+ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
+ /* 25 */ "\u0449",
+ // U+044B: "ы" CYRILLIC SMALL LETTER YERU
+ /* 26 */ "\u044B",
+ // U+0438: "и" CYRILLIC SMALL LETTER I
+ /* 27 */ "\u0438",
+ /* 28 */ null,
+ // U+0451: "ё" CYRILLIC SMALL LETTER IO
+ /* 29 */ "\u0451",
+ /* 30 */ null,
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 31 */ "\u044A",
+ /* 32 */ null,
+ /* 33 */ null,
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 34 */ "\u044A",
+ };
+
+ /* Language sk: Slovak */
+ private static final String[] LANGUAGE_sk = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ /* 0 */ "\u00E1,\u00E4,\u0101,\u00E0,\u00E2,\u00E3,\u00E5,\u00E6,\u0105",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ /* 1 */ "\u00E9,\u011B,\u0113,\u0117,\u00E8,\u00EA,\u00EB,\u0119",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ /* 2 */ "\u00ED,\u012B,\u012F,\u00EC,\u00EE,\u00EF,\u0131",
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 3 */ "\u00F4,\u00F3,\u00F6,\u00F2,\u00F5,\u0153,\u0151,\u00F8",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ /* 4 */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171",
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ /* 5 */ "\u0161,\u00DF,\u015B,\u015F",
+ // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u0148,\u0146,\u00F1,\u0144,\u0144",
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u00E7,\u0107",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ /* 8 */ "\u00FD,\u00FF",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ /* 9 */ "\u010F",
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ /* 10 */ "\u0155,\u0159,\u0157",
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ /* 11 */ "\u0165,\u0163",
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ /* 12 */ "\u017E,\u017C,\u017A",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ /* 13 */ "\u0137",
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u013E,\u013A,\u013C,\u0142",
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ /* 15 */ "\u0123,\u011F",
+ };
+
+ /* Language sl: Slovenian */
+ private static final String[] LANGUAGE_sl = {
+ /* 0~ */
+ null, null, null, null, null,
+ /* ~4 */
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u0161",
+ /* 6 */ null,
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ /* 7 */ "\u010D,\u0107",
+ /* 8 */ null,
+ // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+ /* 9 */ "\u0111",
+ /* 10 */ null,
+ /* 11 */ null,
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ /* 12 */ "\u017E",
+ };
+
+ /* Language sr: Serbian */
+ private static final String[] LANGUAGE_sr = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null,
+ /* ~34 */
+ // U+0437: "з" CYRILLIC SMALL LETTER ZE
+ /* 35 */ "\u0437",
+ // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE
+ /* 36 */ "\u045B",
+ // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
+ /* 37 */ "\u0455",
+ // U+0452: "ђ" CYRILLIC SMALL LETTER DJE
+ /* 38 */ "\u0452",
+ // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
+ /* 39 */ "\u0450",
+ // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
+ /* 40 */ "\u045D",
+ /* 41 */ null,
+ // U+2018: "‘" LEFT SINGLE QUOTATION MARK
+ // U+2019: "’" RIGHT SINGLE QUOTATION MARK
+ // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
+ // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK
+ // U+201C: "“" LEFT DOUBLE QUOTATION MARK
+ // U+201D: "”" RIGHT DOUBLE QUOTATION MARK
+ // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
+ // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
+ /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+ // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
+ // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
+ /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ };
+
+ /* Language sv: Swedish */
+ private static final String[] LANGUAGE_sv = {
+ /* 0 */ null,
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119",
+ /* 2 */ null,
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u0153,\u00F4,\u00F2,\u00F3,\u00F5,\u014D",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u00DF,\u015B,\u0161",
+ /* 6~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~19 */
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ /* 20 */ "\u00E5",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ /* 21 */ "\u00F6",
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ /* 22 */ "\u00E4",
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ /* 23 */ "\u00F8",
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ /* 24 */ "\u00E6",
+ };
+
+ /* Language tr: Turkish */
+ private static final String[] LANGUAGE_tr = {
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ /* 0 */ "\u00E2",
+ /* 1 */ null,
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B",
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D",
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B",
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ /* 5 */ "\u015F,\u00DF,\u015B,\u0161",
+ /* 6 */ null,
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u010D",
+ /* 8~ */
+ null, null, null, null, null, null, null,
+ /* ~14 */
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ /* 15 */ "\u011F",
+ };
+
+ /* Language uk: Ukrainian */
+ private static final String[] LANGUAGE_uk = {
+ /* 0~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null,
+ /* ~24 */
+ // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
+ /* 25 */ "\u0449",
+ // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
+ /* 26 */ "\u0456",
+ // U+0438: "и" CYRILLIC SMALL LETTER I
+ /* 27 */ "\u0438",
+ /* 28~ */
+ null, null, null,
+ /* ~30 */
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 31 */ "\u044A",
+ // U+0457: "ї" CYRILLIC SMALL LETTER YI
+ /* 32 */ "\u0457",
+ /* 33 */ null,
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 34 */ "\u044A",
+ };
+
+ /* Language vi: Vietnamese */
+ private static final String[] LANGUAGE_vi = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+1EA3: "ả" LATIN SMALL LETTER A WITH HOOK ABOVE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+1EA1: "ạ" LATIN SMALL LETTER A WITH DOT BELOW
+ // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+ // U+1EB1: "ằ" LATIN SMALL LETTER A WITH BREVE AND GRAVE
+ // U+1EAF: "ắ" LATIN SMALL LETTER A WITH BREVE AND ACUTE
+ // U+1EB3: "ẳ" LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE
+ // U+1EB5: "ẵ" LATIN SMALL LETTER A WITH BREVE AND TILDE
+ // U+1EB7: "ặ" LATIN SMALL LETTER A WITH BREVE AND DOT BELOW
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+1EA7: "ầ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE
+ // U+1EA5: "ấ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE
+ // U+1EA9: "ẩ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE
+ // U+1EAB: "ẫ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE
+ // U+1EAD: "ậ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW
+ /* 0 */ "\u00E0,\u00E1,\u1EA3,\u00E3,\u1EA1,\u0103,\u1EB1,\u1EAF,\u1EB3,\u1EB5,\u1EB7,\u00E2,\u1EA7,\u1EA5,\u1EA9,\u1EAB,\u1EAD",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+1EBB: "ẻ" LATIN SMALL LETTER E WITH HOOK ABOVE
+ // U+1EBD: "ẽ" LATIN SMALL LETTER E WITH TILDE
+ // U+1EB9: "ẹ" LATIN SMALL LETTER E WITH DOT BELOW
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+1EC1: "ề" LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE
+ // U+1EBF: "ế" LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE
+ // U+1EC3: "ể" LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE
+ // U+1EC5: "ễ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE
+ // U+1EC7: "ệ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW
+ /* 1 */ "\u00E8,\u00E9,\u1EBB,\u1EBD,\u1EB9,\u00EA,\u1EC1,\u1EBF,\u1EC3,\u1EC5,\u1EC7",
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+1EC9: "ỉ" LATIN SMALL LETTER I WITH HOOK ABOVE
+ // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
+ // U+1ECB: "ị" LATIN SMALL LETTER I WITH DOT BELOW
+ /* 2 */ "\u00EC,\u00ED,\u1EC9,\u0129,\u1ECB",
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+1ECF: "ỏ" LATIN SMALL LETTER O WITH HOOK ABOVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+1ECD: "ọ" LATIN SMALL LETTER O WITH DOT BELOW
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+1ED3: "ồ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE
+ // U+1ED1: "ố" LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE
+ // U+1ED5: "ổ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE
+ // U+1ED7: "ỗ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE
+ // U+1ED9: "ộ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW
+ // U+01A1: "ơ" LATIN SMALL LETTER O WITH HORN
+ // U+1EDD: "ờ" LATIN SMALL LETTER O WITH HORN AND GRAVE
+ // U+1EDB: "ớ" LATIN SMALL LETTER O WITH HORN AND ACUTE
+ // U+1EDF: "ở" LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE
+ // U+1EE1: "ỡ" LATIN SMALL LETTER O WITH HORN AND TILDE
+ // U+1EE3: "ợ" LATIN SMALL LETTER O WITH HORN AND DOT BELOW
+ /* 3 */ "\u00F2,\u00F3,\u1ECF,\u00F5,\u1ECD,\u00F4,\u1ED3,\u1ED1,\u1ED5,\u1ED7,\u1ED9,\u01A1,\u1EDD,\u1EDB,\u1EDF,\u1EE1,\u1EE3",
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+1EE7: "ủ" LATIN SMALL LETTER U WITH HOOK ABOVE
+ // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
+ // U+1EE5: "ụ" LATIN SMALL LETTER U WITH DOT BELOW
+ // U+01B0: "ư" LATIN SMALL LETTER U WITH HORN
+ // U+1EEB: "ừ" LATIN SMALL LETTER U WITH HORN AND GRAVE
+ // U+1EE9: "ứ" LATIN SMALL LETTER U WITH HORN AND ACUTE
+ // U+1EED: "ử" LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE
+ // U+1EEF: "ữ" LATIN SMALL LETTER U WITH HORN AND TILDE
+ // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW
+ /* 4 */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1",
+ /* 5~ */
+ null, null, null,
+ /* ~7 */
+ // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+1EF7: "ỷ" LATIN SMALL LETTER Y WITH HOOK ABOVE
+ // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE
+ // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW
+ /* 8 */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5",
+ // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+ /* 9 */ "\u0111",
+ };
+
+ /* Language zz: No language */
+ private static final String[] LANGUAGE_zz = {
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ // U+0115: "ĕ" LATIN SMALL LETTER E WITH BREVE
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113,\u0115,\u0117,\u0119,\u011B",
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+012D: "ĭ" LATIN SMALL LETTER I WITH BREVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u0129,\u012B,\u012D,\u012F,\u0131,\u0133",
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+014F: "ŏ" LATIN SMALL LETTER O WITH BREVE
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u00F8,\u014D,\u014F,\u0151,\u0153,\u00BA",
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+017F: "ſ" LATIN SMALL LETTER LONG S
+ /* 5 */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+ // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+ // U+014B: "ŋ" LATIN SMALL LETTER ENG
+ /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX
+ // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u0109,\u010B,\u010D",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 8 */ "\u00FD,\u0177,\u00FF,\u0133",
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+ // U+00F0: "ð" LATIN SMALL LETTER ETH
+ /* 9 */ "\u010F,\u0111,\u00F0",
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ /* 10 */ "\u0155,\u0157,\u0159",
+ // U+00FE: "þ" LATIN SMALL LETTER THORN
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE
+ /* 11 */ "\u00FE,\u0163,\u0165,\u0167",
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ /* 12 */ "\u017A,\u017C,\u017E",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ // U+0138: "ĸ" LATIN SMALL LETTER KRA
+ /* 13 */ "\u0137,\u0138",
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142",
+ // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ /* 15 */ "\u011D,\u011F,\u0121,\u0123",
+ /* 16 */ null,
+ // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX
+ /* 17 */ "\u0125",
+ // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX
+ /* 18 */ "\u0135",
+ // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX
+ /* 19 */ "\u0175",
+ };
+
+ private static final Object[] LANGUAGES_AND_TEXTS = {
+ "DEFAULT", LANGUAGE_DEFAULT, /* default */
+ "ar", LANGUAGE_ar, /* Arabic */
+ "be", LANGUAGE_be, /* Belarusian */
+ "ca", LANGUAGE_ca, /* Catalan */
+ "cs", LANGUAGE_cs, /* Czech */
+ "da", LANGUAGE_da, /* Danish */
+ "de", LANGUAGE_de, /* German */
+ "en", LANGUAGE_en, /* English */
+ "es", LANGUAGE_es, /* Spanish */
+ "et", LANGUAGE_et, /* Estonian */
+ "fa", LANGUAGE_fa, /* Persian */
+ "fi", LANGUAGE_fi, /* Finnish */
+ "fr", LANGUAGE_fr, /* French */
+ "hi", LANGUAGE_hi, /* Hindi */
+ "hr", LANGUAGE_hr, /* Croatian */
+ "hu", LANGUAGE_hu, /* Hungarian */
+ "is", LANGUAGE_is, /* Icelandic */
+ "it", LANGUAGE_it, /* Italian */
+ "iw", LANGUAGE_iw, /* Hebrew */
+ "ky", LANGUAGE_ky, /* Kirghiz */
+ "lt", LANGUAGE_lt, /* Lithuanian */
+ "lv", LANGUAGE_lv, /* Latvian */
+ "mk", LANGUAGE_mk, /* Macedonian */
+ "nb", LANGUAGE_nb, /* Norwegian Bokmål */
+ "nl", LANGUAGE_nl, /* Dutch */
+ "pl", LANGUAGE_pl, /* Polish */
+ "pt", LANGUAGE_pt, /* Portuguese */
+ "rm", LANGUAGE_rm, /* Raeto-Romance */
+ "ro", LANGUAGE_ro, /* Romanian */
+ "ru", LANGUAGE_ru, /* Russian */
+ "sk", LANGUAGE_sk, /* Slovak */
+ "sl", LANGUAGE_sl, /* Slovenian */
+ "sr", LANGUAGE_sr, /* Serbian */
+ "sv", LANGUAGE_sv, /* Swedish */
+ "tr", LANGUAGE_tr, /* Turkish */
+ "uk", LANGUAGE_uk, /* Ukrainian */
+ "vi", LANGUAGE_vi, /* Vietnamese */
+ "zz", LANGUAGE_zz, /* No language */
+ };
+
+ static {
+ int id = 0;
+ for (final String name : NAMES) {
+ sNameToIdsMap.put(name, id++);
+ }
+
+ for (int i = 0; i < LANGUAGES_AND_TEXTS.length; i += 2) {
+ final String language = (String)LANGUAGES_AND_TEXTS[i];
+ final String[] texts = (String[])LANGUAGES_AND_TEXTS[i + 1];
+ sLocaleToTextsMap.put(language, texts);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java
deleted file mode 100644
index 1e67eec70..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Paint;
-import android.graphics.Rect;
-
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.keyboard.MiniKeyboard;
-import com.android.inputmethod.latin.R;
-
-import java.util.List;
-
-public class MiniKeyboardBuilder {
- private final Resources mRes;
- private final MiniKeyboard mKeyboard;
- private final CharSequence[] mPopupCharacters;
- private final MiniKeyboardLayoutParams mParams;
-
- /* package */ static class MiniKeyboardLayoutParams {
- public final int mKeyWidth;
- public final int mRowHeight;
- /* package */ final int mTopRowAdjustment;
- public final int mNumRows;
- public final int mNumColumns;
- public final int mLeftKeys;
- public final int mRightKeys; // includes default key.
-
- /**
- * The object holding mini keyboard layout parameters.
- *
- * @param numKeys number of keys in this mini keyboard.
- * @param maxColumns number of maximum columns of this mini keyboard.
- * @param keyWidth mini keyboard key width in pixel, including horizontal gap.
- * @param rowHeight mini keyboard row height in pixel, including vertical gap.
- * @param coordXInParent coordinate x of the popup key in parent keyboard.
- * @param parentKeyboardWidth parent keyboard width in pixel.
- */
- public MiniKeyboardLayoutParams(int numKeys, int maxColumns, int keyWidth, int rowHeight,
- int coordXInParent, int parentKeyboardWidth) {
- if (parentKeyboardWidth / keyWidth < maxColumns)
- throw new IllegalArgumentException("Keyboard is too small to hold mini keyboard: "
- + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
- mKeyWidth = keyWidth;
- mRowHeight = rowHeight;
-
- final int numRows = (numKeys + maxColumns - 1) / maxColumns;
- mNumRows = numRows;
- final int numColumns = getOptimizedColumns(numKeys, maxColumns);
- mNumColumns = numColumns;
-
- final int numLeftKeys = (numColumns - 1) / 2;
- final int numRightKeys = numColumns - numLeftKeys; // including default key.
- final int maxLeftKeys = coordXInParent / keyWidth;
- final int maxRightKeys = Math.max(1, (parentKeyboardWidth - coordXInParent) / keyWidth);
- int leftKeys, rightKeys;
- if (numLeftKeys > maxLeftKeys) {
- leftKeys = maxLeftKeys;
- rightKeys = numColumns - maxLeftKeys;
- } else if (numRightKeys > maxRightKeys) {
- leftKeys = numColumns - maxRightKeys;
- rightKeys = maxRightKeys;
- } else {
- leftKeys = numLeftKeys;
- rightKeys = numRightKeys;
- }
- // Shift right if the left edge of mini keyboard is on the edge of parent keyboard
- // unless the parent key is on the left edge.
- if (leftKeys * keyWidth >= coordXInParent && leftKeys > 0) {
- leftKeys--;
- rightKeys++;
- }
- // Shift left if the right edge of mini keyboard is on the edge of parent keyboard
- // unless the parent key is on the right edge.
- if (rightKeys * keyWidth + coordXInParent >= parentKeyboardWidth && rightKeys > 1) {
- leftKeys++;
- rightKeys--;
- }
- mLeftKeys = leftKeys;
- mRightKeys = rightKeys;
-
- // Centering of the top row.
- final boolean onEdge = (leftKeys == 0 || rightKeys == 1);
- if (numRows < 2 || onEdge || getTopRowEmptySlots(numKeys, numColumns) % 2 == 0) {
- mTopRowAdjustment = 0;
- } else if (mLeftKeys < mRightKeys - 1) {
- mTopRowAdjustment = 1;
- } else {
- mTopRowAdjustment = -1;
- }
- }
-
- // Return key position according to column count (0 is default).
- /* package */ int getColumnPos(int n) {
- final int col = n % mNumColumns;
- if (col == 0) {
- // default position.
- return 0;
- }
- int pos = 0;
- int right = 1; // include default position key.
- int left = 0;
- int i = 0;
- while (true) {
- // Assign right key if available.
- if (right < mRightKeys) {
- pos = right;
- right++;
- i++;
- }
- if (i >= col)
- break;
- // Assign left key if available.
- if (left < mLeftKeys) {
- left++;
- pos = -left;
- i++;
- }
- if (i >= col)
- break;
- }
- return pos;
- }
-
- private static int getTopRowEmptySlots(int numKeys, int numColumns) {
- final int remainingKeys = numKeys % numColumns;
- if (remainingKeys == 0) {
- return 0;
- } else {
- return numColumns - remainingKeys;
- }
- }
-
- private int getOptimizedColumns(int numKeys, int maxColumns) {
- int numColumns = Math.min(numKeys, maxColumns);
- while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
- numColumns--;
- }
- return numColumns;
- }
-
- public int getDefaultKeyCoordX() {
- return mLeftKeys * mKeyWidth;
- }
-
- public int getX(int n, int row) {
- final int x = getColumnPos(n) * mKeyWidth + getDefaultKeyCoordX();
- if (isTopRow(row)) {
- return x + mTopRowAdjustment * (mKeyWidth / 2);
- }
- return x;
- }
-
- public int getY(int row) {
- return (mNumRows - 1 - row) * mRowHeight;
- }
-
- public int getRowFlags(int row) {
- int rowFlags = 0;
- if (row == 0) rowFlags |= Keyboard.EDGE_TOP;
- if (isTopRow(row)) rowFlags |= Keyboard.EDGE_BOTTOM;
- return rowFlags;
- }
-
- private boolean isTopRow(int rowCount) {
- return rowCount == mNumRows - 1;
- }
- }
-
- public MiniKeyboardBuilder(KeyboardView view, int layoutTemplateResId, Key parentKey,
- Keyboard parentKeyboard) {
- final Context context = view.getContext();
- mRes = context.getResources();
- final MiniKeyboard keyboard = new MiniKeyboard(
- context, layoutTemplateResId, parentKeyboard);
- mKeyboard = keyboard;
- mPopupCharacters = parentKey.mPopupCharacters;
-
- final int keyWidth = getMaxKeyWidth(view, mPopupCharacters, keyboard.getKeyWidth());
- final MiniKeyboardLayoutParams params = new MiniKeyboardLayoutParams(
- mPopupCharacters.length, parentKey.mMaxPopupColumn,
- keyWidth, parentKeyboard.getRowHeight(),
- parentKey.mX + (parentKey.mWidth + parentKey.mGap) / 2 - keyWidth / 2,
- view.getMeasuredWidth());
- mParams = params;
-
- keyboard.setRowHeight(params.mRowHeight);
- keyboard.setHeight(params.mNumRows * params.mRowHeight);
- keyboard.setMinWidth(params.mNumColumns * params.mKeyWidth);
- keyboard.setDefaultCoordX(params.getDefaultKeyCoordX() + params.mKeyWidth / 2);
- }
-
- private static int getMaxKeyWidth(KeyboardView view, CharSequence[] popupCharacters,
- int minKeyWidth) {
- Paint paint = null;
- Rect bounds = null;
- int maxWidth = 0;
- for (CharSequence popupSpec : popupCharacters) {
- final CharSequence label = PopupCharactersParser.getLabel(popupSpec.toString());
- // If the label is single letter, minKeyWidth is enough to hold the label.
- if (label != null && label.length() > 1) {
- if (paint == null) {
- paint = new Paint();
- paint.setAntiAlias(true);
- }
- final int labelSize = view.getDefaultLabelSizeAndSetPaint(paint);
- paint.setTextSize(labelSize);
- if (bounds == null) bounds = new Rect();
- paint.getTextBounds(label.toString(), 0, label.length(), bounds);
- if (maxWidth < bounds.width())
- maxWidth = bounds.width();
- }
- }
- final int horizontalPadding = (int)view.getContext().getResources().getDimension(
- R.dimen.mini_keyboard_key_horizontal_padding);
- return Math.max(minKeyWidth, maxWidth + horizontalPadding);
- }
-
- public MiniKeyboard build() {
- final MiniKeyboard keyboard = mKeyboard;
- final List<Key> keys = keyboard.getKeys();
- final MiniKeyboardLayoutParams params = mParams;
- for (int n = 0; n < mPopupCharacters.length; n++) {
- final CharSequence label = mPopupCharacters[n];
- final int row = n / params.mNumColumns;
- final Key key = new Key(mRes, keyboard, label, params.getX(n, row), params.getY(row),
- params.mKeyWidth, params.mRowHeight, params.getRowFlags(row));
- keys.add(key);
- }
- return keyboard;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java
index dae73c4e4..b39b97720 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/ModifierKeyState.java
@@ -18,15 +18,13 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-
-public class ModifierKeyState {
- protected static final String TAG = "ModifierKeyState";
- protected static final boolean DEBUG = KeyboardSwitcher.DEBUG_STATE;
+/* package */ class ModifierKeyState {
+ protected static final String TAG = ModifierKeyState.class.getSimpleName();
+ protected static final boolean DEBUG = false;
protected static final int RELEASING = 0;
protected static final int PRESSING = 1;
- protected static final int MOMENTARY = 2;
+ protected static final int CHORDING = 2;
protected final String mName;
protected int mState = RELEASING;
@@ -52,7 +50,7 @@ public class ModifierKeyState {
public void onOtherKeyPressed() {
final int oldState = mState;
if (oldState == PRESSING)
- mState = MOMENTARY;
+ mState = CHORDING;
if (DEBUG)
Log.d(TAG, mName + ".onOtherKeyPressed: " + toString(oldState) + " > " + this);
}
@@ -65,8 +63,8 @@ public class ModifierKeyState {
return mState == RELEASING;
}
- public boolean isMomentary() {
- return mState == MOMENTARY;
+ public boolean isChording() {
+ return mState == CHORDING;
}
@Override
@@ -78,7 +76,7 @@ public class ModifierKeyState {
switch (state) {
case RELEASING: return "RELEASING";
case PRESSING: return "PRESSING";
- case MOMENTARY: return "MOMENTARY";
+ case CHORDING: return "CHORDING";
default: return "UNKNOWN";
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java
deleted file mode 100644
index ddadb1338..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerKeyState.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import com.android.inputmethod.keyboard.KeyDetector;
-import com.android.inputmethod.keyboard.PointerTracker;
-
-/**
- * This class keeps track of a key index and a position where {@link PointerTracker} is.
- */
-public class PointerTrackerKeyState {
- private final KeyDetector mKeyDetector;
-
- // The position and time at which first down event occurred.
- private long mDownTime;
- private long mUpTime;
-
- // The current key index where this pointer is.
- private int mKeyIndex = KeyDetector.NOT_A_KEY;
- // The position where mKeyIndex was recognized for the first time.
- private int mKeyX;
- private int mKeyY;
-
- // Last pointer position.
- private int mLastX;
- private int mLastY;
-
- public PointerTrackerKeyState(KeyDetector keyDetecor) {
- mKeyDetector = keyDetecor;
- }
-
- public int getKeyIndex() {
- return mKeyIndex;
- }
-
- public int getKeyX() {
- return mKeyX;
- }
-
- public int getKeyY() {
- return mKeyY;
- }
-
- public long getDownTime() {
- return mDownTime;
- }
-
- public long getUpTime() {
- return mUpTime;
- }
-
- public int getLastX() {
- return mLastX;
- }
-
- public int getLastY() {
- return mLastY;
- }
-
- public int onDownKey(int x, int y, long eventTime) {
- mDownTime = eventTime;
- return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
- }
-
- private int onMoveKeyInternal(int x, int y) {
- mLastX = x;
- mLastY = y;
- return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
- }
-
- public int onMoveKey(int x, int y) {
- return onMoveKeyInternal(x, y);
- }
-
- public int onMoveToNewKey(int keyIndex, int x, int y) {
- mKeyIndex = keyIndex;
- mKeyX = x;
- mKeyY = y;
- return keyIndex;
- }
-
- public int onUpKey(int x, int y, long eventTime) {
- mUpTime = eventTime;
- mKeyIndex = KeyDetector.NOT_A_KEY;
- return onMoveKeyInternal(x, y);
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index f87cd869e..5db65c660 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -16,70 +16,89 @@
package com.android.inputmethod.keyboard.internal;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.PointerTracker;
+import java.util.Iterator;
import java.util.LinkedList;
public class PointerTrackerQueue {
- private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
+ private static final String TAG = PointerTrackerQueue.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
- public void add(PointerTracker tracker) {
+ public synchronized void add(PointerTracker tracker) {
mQueue.add(tracker);
}
- public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) {
- if (mQueue.lastIndexOf(tracker) < 0) {
+ public synchronized void remove(PointerTracker tracker) {
+ mQueue.remove(tracker);
+ }
+
+ public synchronized void releaseAllPointersOlderThan(PointerTracker tracker,
+ long eventTime) {
+ if (DEBUG) {
+ Log.d(TAG, "releaseAllPoniterOlderThan: [" + tracker.mPointerId + "] " + this);
+ }
+ if (!mQueue.contains(tracker)) {
return;
}
- final LinkedList<PointerTracker> queue = mQueue;
- int oldestPos = 0;
- for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) {
- if (t.isModifier()) {
- oldestPos++;
- } else {
- t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime, true);
- queue.remove(oldestPos);
+ final Iterator<PointerTracker> it = mQueue.iterator();
+ while (it.hasNext()) {
+ final PointerTracker t = it.next();
+ if (t == tracker) {
+ break;
+ }
+ if (!t.isModifier()) {
+ t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime);
+ it.remove();
}
}
}
public void releaseAllPointers(long eventTime) {
- releaseAllPointersExcept(null, eventTime, true);
+ releaseAllPointersExcept(null, eventTime);
}
- public void releaseAllPointersExcept(PointerTracker tracker, long eventTime,
- boolean updateReleasedKeyGraphics) {
- for (PointerTracker t : mQueue) {
- if (t == tracker)
- continue;
- t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime, updateReleasedKeyGraphics);
+ public synchronized void releaseAllPointersExcept(PointerTracker tracker, long eventTime) {
+ if (DEBUG) {
+ if (tracker == null) {
+ Log.d(TAG, "releaseAllPoniters: " + this);
+ } else {
+ Log.d(TAG, "releaseAllPoniterExcept: [" + tracker.mPointerId + "] " + this);
+ }
+ }
+ final Iterator<PointerTracker> it = mQueue.iterator();
+ while (it.hasNext()) {
+ final PointerTracker t = it.next();
+ if (t != tracker) {
+ t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime);
+ it.remove();
+ }
}
- mQueue.clear();
- if (tracker != null)
- mQueue.add(tracker);
- }
-
- public void remove(PointerTracker tracker) {
- mQueue.remove(tracker);
}
- public boolean isInSlidingKeyInput() {
+ public synchronized boolean isAnyInSlidingKeyInput() {
for (final PointerTracker tracker : mQueue) {
- if (tracker.isInSlidingKeyInput())
+ if (tracker.isInSlidingKeyInput()) {
return true;
+ }
}
return false;
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("[");
- for (PointerTracker tracker : mQueue) {
- if (sb.length() > 1)
+ final StringBuilder sb = new StringBuilder();
+ for (final PointerTracker tracker : mQueue) {
+ if (sb.length() > 0)
sb.append(" ");
- sb.append(String.format("%d", tracker.mPointerId));
+ sb.append("[" + tracker.mPointerId + " "
+ + Keyboard.printableCode(tracker.getKey().mCode) + "]");
}
- sb.append("]");
return sb.toString();
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java b/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java
deleted file mode 100644
index 8276f5d78..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/PopupCharactersParser.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.R;
-
-/**
- * String parser of popupCharacters attribute of Key.
- * The string is comma separated texts each of which represents one popup key.
- * Each popup key text is one of the following:
- * - A single letter (Letter)
- * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (@icon/icon_number|@integer/key_code)
- * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\'
- * character.
- * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
- * See {@link KeyboardIconsSet} about icon_number.
- */
-public class PopupCharactersParser {
- private static final String TAG = PopupCharactersParser.class.getSimpleName();
-
- private static final char ESCAPE = '\\';
- private static final String LABEL_END = "|";
- private static final String PREFIX_AT = "@";
- private static final String PREFIX_ICON = PREFIX_AT + "icon/";
- private static final String PREFIX_CODE = PREFIX_AT + "integer/";
-
- private PopupCharactersParser() {
- // Intentional empty constructor for utility class.
- }
-
- private static boolean hasIcon(String popupSpec) {
- if (popupSpec.startsWith(PREFIX_ICON)) {
- final int end = indexOfLabelEnd(popupSpec, 0);
- if (end > 0)
- return true;
- throw new PopupCharactersParserError("outputText or code not specified: " + popupSpec);
- }
- return false;
- }
-
- private static boolean hasCode(String popupSpec) {
- final int end = indexOfLabelEnd(popupSpec, 0);
- if (end > 0 && end + 1 < popupSpec.length()
- && popupSpec.substring(end + 1).startsWith(PREFIX_CODE)) {
- return true;
- }
- return false;
- }
-
- private static String parseEscape(String text) {
- if (text.indexOf(ESCAPE) < 0)
- return text;
- final int length = text.length();
- final StringBuilder sb = new StringBuilder();
- for (int pos = 0; pos < length; pos++) {
- final char c = text.charAt(pos);
- if (c == ESCAPE && pos + 1 < length) {
- sb.append(text.charAt(++pos));
- } else {
- sb.append(c);
- }
- }
- return sb.toString();
- }
-
- private static int indexOfLabelEnd(String popupSpec, int start) {
- if (popupSpec.indexOf(ESCAPE, start) < 0) {
- final int end = popupSpec.indexOf(LABEL_END, start);
- if (end == 0)
- throw new PopupCharactersParserError(LABEL_END + " at " + start + ": " + popupSpec);
- return end;
- }
- final int length = popupSpec.length();
- for (int pos = start; pos < length; pos++) {
- final char c = popupSpec.charAt(pos);
- if (c == ESCAPE && pos + 1 < length) {
- pos++;
- } else if (popupSpec.startsWith(LABEL_END, pos)) {
- return pos;
- }
- }
- return -1;
- }
-
- public static String getLabel(String popupSpec) {
- if (hasIcon(popupSpec))
- return null;
- final int end = indexOfLabelEnd(popupSpec, 0);
- final String label = (end > 0) ? parseEscape(popupSpec.substring(0, end))
- : parseEscape(popupSpec);
- if (TextUtils.isEmpty(label))
- throw new PopupCharactersParserError("Empty label: " + popupSpec);
- return label;
- }
-
- public static String getOutputText(String popupSpec) {
- if (hasCode(popupSpec))
- return null;
- final int end = indexOfLabelEnd(popupSpec, 0);
- if (end > 0) {
- if (indexOfLabelEnd(popupSpec, end + 1) >= 0)
- throw new PopupCharactersParserError("Multiple " + LABEL_END + ": "
- + popupSpec);
- final String outputText = parseEscape(popupSpec.substring(end + LABEL_END.length()));
- if (!TextUtils.isEmpty(outputText))
- return outputText;
- throw new PopupCharactersParserError("Empty outputText: " + popupSpec);
- }
- final String label = getLabel(popupSpec);
- if (label == null)
- throw new PopupCharactersParserError("Empty label: " + popupSpec);
- // Code is automatically generated for one letter label. See {@link getCode()}.
- if (label.length() == 1)
- return null;
- return label;
- }
-
- public static int getCode(Resources res, String popupSpec) {
- if (hasCode(popupSpec)) {
- final int end = indexOfLabelEnd(popupSpec, 0);
- if (indexOfLabelEnd(popupSpec, end + 1) >= 0)
- throw new PopupCharactersParserError("Multiple " + LABEL_END + ": " + popupSpec);
- final int resId = getResourceId(res,
- popupSpec.substring(end + LABEL_END.length() + PREFIX_AT.length()));
- final int code = res.getInteger(resId);
- return code;
- }
- if (indexOfLabelEnd(popupSpec, 0) > 0)
- return Keyboard.CODE_DUMMY;
- final String label = getLabel(popupSpec);
- // Code is automatically generated for one letter label.
- if (label != null && label.length() == 1)
- return label.charAt(0);
- return Keyboard.CODE_DUMMY;
- }
-
- public static int getIconId(String popupSpec) {
- if (hasIcon(popupSpec)) {
- int end = popupSpec.indexOf(LABEL_END, PREFIX_ICON.length() + 1);
- final String iconId = popupSpec.substring(PREFIX_ICON.length(), end);
- try {
- return Integer.valueOf(iconId);
- } catch (NumberFormatException e) {
- Log.w(TAG, "illegal icon id specified: " + iconId);
- return KeyboardIconsSet.ICON_UNDEFINED;
- }
- }
- return KeyboardIconsSet.ICON_UNDEFINED;
- }
-
- private static int getResourceId(Resources res, String name) {
- String packageName = res.getResourcePackageName(R.string.english_ime_name);
- int resId = res.getIdentifier(name, null, packageName);
- if (resId == 0)
- throw new PopupCharactersParserError("Unknown resource: " + name);
- return resId;
- }
-
- @SuppressWarnings("serial")
- public static class PopupCharactersParserError extends RuntimeException {
- public PopupCharactersParserError(String message) {
- super(message);
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/Row.java b/java/src/com/android/inputmethod/keyboard/internal/Row.java
deleted file mode 100644
index 06aadcc05..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/Row.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.util.Xml;
-
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.R;
-
-/**
- * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
- * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
- * defines.
- */
-public class Row {
- /** Default width of a key in this row. */
- public final int mDefaultWidth;
- /** Default height of a key in this row. */
- public final int mDefaultHeight;
- /** Default horizontal gap between keys in this row. */
- public final int mDefaultHorizontalGap;
- /** Vertical gap following this row. */
- public final int mVerticalGap;
- /**
- * Edge flags for this row of keys. Possible values that can be assigned are
- * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
- */
- public final int mRowEdgeFlags;
-
- private final Keyboard mKeyboard;
-
- public Row(Resources res, Keyboard keyboard, XmlResourceParser parser) {
- this.mKeyboard = keyboard;
- final int keyboardWidth = keyboard.getDisplayWidth();
- final int keyboardHeight = keyboard.getKeyboardHeight();
- TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- mDefaultWidth = KeyboardParser.getDimensionOrFraction(a,
- R.styleable.Keyboard_keyWidth, keyboardWidth, keyboard.getKeyWidth());
- mDefaultHeight = KeyboardParser.getDimensionOrFraction(a,
- R.styleable.Keyboard_rowHeight, keyboardHeight, keyboard.getRowHeight());
- mDefaultHorizontalGap = KeyboardParser.getDimensionOrFraction(a,
- R.styleable.Keyboard_horizontalGap, keyboardWidth, keyboard.getHorizontalGap());
- mVerticalGap = KeyboardParser.getDimensionOrFraction(a,
- R.styleable.Keyboard_verticalGap, keyboardHeight, keyboard.getVerticalGap());
- a.recycle();
- a = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Row);
- mRowEdgeFlags = a.getInt(R.styleable.Keyboard_Row_rowEdgeFlags, 0);
- a.recycle();
- }
-
- public Keyboard getKeyboard() {
- return mKeyboard;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
index 6617b917f..edb40c8e7 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-public class ShiftKeyState extends ModifierKeyState {
+/* package */ class ShiftKeyState extends ModifierKeyState {
private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked
private static final int IGNORING = 4;
@@ -30,7 +30,7 @@ public class ShiftKeyState extends ModifierKeyState {
public void onOtherKeyPressed() {
int oldState = mState;
if (oldState == PRESSING) {
- mState = MOMENTARY;
+ mState = CHORDING;
} else if (oldState == PRESSING_ON_SHIFTED) {
mState = IGNORING;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java
deleted file mode 100644
index 78a3a7e9d..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/SlidingLocaleDrawable.java
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.PixelFormat;
-import android.graphics.drawable.Drawable;
-import android.text.TextPaint;
-import android.view.ViewConfiguration;
-
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.LatinKeyboard;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
-
-/**
- * Animation to be displayed on the spacebar preview popup when switching languages by swiping the
- * spacebar. It draws the current, previous and next languages and moves them by the delta of touch
- * movement on the spacebar.
- */
-public class SlidingLocaleDrawable extends Drawable {
- private static final int SLIDE_SPEED_MULTIPLIER_RATIO = 150;
- private final int mWidth;
- private final int mHeight;
- private final Drawable mBackground;
- private final int mSpacebarTextColor;
- private final TextPaint mTextPaint;
- private final int mMiddleX;
- private final Drawable mLeftDrawable;
- private final Drawable mRightDrawable;
- private final int mThreshold;
-
- private int mDiff;
- private boolean mHitThreshold;
- private String mCurrentLanguage;
- private String mNextLanguage;
- private String mPrevLanguage;
-
- public SlidingLocaleDrawable(Context context, Drawable background, int width, int height) {
- mBackground = background;
- Keyboard.setDefaultBounds(background);
- mWidth = width;
- mHeight = height;
- final TextPaint textPaint = new TextPaint();
- textPaint.setTextSize(LatinKeyboard.getTextSizeFromTheme(
- context.getTheme(), android.R.style.TextAppearance_Medium, 18));
- textPaint.setColor(Color.TRANSPARENT);
- textPaint.setTextAlign(Align.CENTER);
- textPaint.setAntiAlias(true);
- mTextPaint = textPaint;
- mMiddleX = (background != null) ? (mWidth - mBackground.getIntrinsicWidth()) / 2 : 0;
-
- final TypedArray lka = context.obtainStyledAttributes(
- null, R.styleable.LatinKeyboard, R.attr.latinKeyboardStyle, R.style.LatinKeyboard);
- mLeftDrawable = lka.getDrawable(R.styleable.LatinKeyboard_spacebarArrowPreviewLeftIcon);
- mRightDrawable = lka.getDrawable(R.styleable.LatinKeyboard_spacebarArrowPreviewRightIcon);
- lka.recycle();
- final TypedArray kva = context.obtainStyledAttributes(
- null, R.styleable.KeyboardView, R.attr.keyboardViewStyle, R.style.KeyboardView);
- mSpacebarTextColor = kva.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
- kva.recycle();
-
- mThreshold = ViewConfiguration.get(context).getScaledTouchSlop();
- }
-
- public void setDiff(int diff) {
- if (diff == Integer.MAX_VALUE) {
- mHitThreshold = false;
- mCurrentLanguage = null;
- return;
- }
- mDiff = Math.max(diff, diff * SLIDE_SPEED_MULTIPLIER_RATIO / 100);
- if (mDiff > mWidth) mDiff = mWidth;
- if (mDiff < -mWidth) mDiff = -mWidth;
- if (Math.abs(mDiff) > mThreshold) mHitThreshold = true;
- invalidateSelf();
- }
-
-
- @Override
- public void draw(Canvas canvas) {
- canvas.save();
- if (mHitThreshold) {
- Paint paint = mTextPaint;
- final int width = mWidth;
- final int height = mHeight;
- final int diff = mDiff;
- final Drawable lArrow = mLeftDrawable;
- final Drawable rArrow = mRightDrawable;
- canvas.clipRect(0, 0, width, height);
- if (mCurrentLanguage == null) {
- SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance();
- mCurrentLanguage = subtypeSwitcher.getInputLanguageName();
- mNextLanguage = subtypeSwitcher.getNextInputLanguageName();
- mPrevLanguage = subtypeSwitcher.getPreviousInputLanguageName();
- }
- // Draw language text with shadow
- final float baseline = mHeight * LatinKeyboard.SPACEBAR_LANGUAGE_BASELINE
- - paint.descent();
- paint.setColor(mSpacebarTextColor);
- canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint);
- canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint);
- canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint);
-
- if (lArrow != null && rArrow != null) {
- Keyboard.setDefaultBounds(lArrow);
- rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width,
- rArrow.getIntrinsicHeight());
- lArrow.draw(canvas);
- rArrow.draw(canvas);
- }
- }
- if (mBackground != null) {
- canvas.translate(mMiddleX, 0);
- mBackground.draw(canvas);
- }
- canvas.restore();
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- @Override
- public void setAlpha(int alpha) {
- // Ignore
- }
-
- @Override
- public void setColorFilter(ColorFilter cf) {
- // Ignore
- }
-
- @Override
- public int getIntrinsicWidth() {
- return mWidth;
- }
-
- @Override
- public int getIntrinsicHeight() {
- return mHeight;
- }
-}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java b/java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java
deleted file mode 100644
index 8d192c2f0..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/SwipeTracker.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2010 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.keyboard.internal;
-
-import android.view.MotionEvent;
-
-public class SwipeTracker {
- private static final int NUM_PAST = 4;
- private static final int LONGEST_PAST_TIME = 200;
-
- final EventRingBuffer mBuffer = new EventRingBuffer(NUM_PAST);
-
- private float mYVelocity;
- private float mXVelocity;
-
- public void addMovement(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mBuffer.clear();
- return;
- }
- long time = ev.getEventTime();
- final int count = ev.getHistorySize();
- for (int i = 0; i < count; i++) {
- addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i), ev.getHistoricalEventTime(i));
- }
- addPoint(ev.getX(), ev.getY(), time);
- }
-
- private void addPoint(float x, float y, long time) {
- final EventRingBuffer buffer = mBuffer;
- while (buffer.size() > 0) {
- long lastT = buffer.getTime(0);
- if (lastT >= time - LONGEST_PAST_TIME)
- break;
- buffer.dropOldest();
- }
- buffer.add(x, y, time);
- }
-
- public void computeCurrentVelocity(int units) {
- computeCurrentVelocity(units, Float.MAX_VALUE);
- }
-
- public void computeCurrentVelocity(int units, float maxVelocity) {
- final EventRingBuffer buffer = mBuffer;
- final float oldestX = buffer.getX(0);
- final float oldestY = buffer.getY(0);
- final long oldestTime = buffer.getTime(0);
-
- float accumX = 0;
- float accumY = 0;
- final int count = buffer.size();
- for (int pos = 1; pos < count; pos++) {
- final int dur = (int)(buffer.getTime(pos) - oldestTime);
- if (dur == 0) continue;
- float dist = buffer.getX(pos) - oldestX;
- float vel = (dist / dur) * units; // pixels/frame.
- if (accumX == 0) accumX = vel;
- else accumX = (accumX + vel) * .5f;
-
- dist = buffer.getY(pos) - oldestY;
- vel = (dist / dur) * units; // pixels/frame.
- if (accumY == 0) accumY = vel;
- else accumY = (accumY + vel) * .5f;
- }
- mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
- : Math.min(accumX, maxVelocity);
- mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
- : Math.min(accumY, maxVelocity);
- }
-
- public float getXVelocity() {
- return mXVelocity;
- }
-
- public float getYVelocity() {
- return mYVelocity;
- }
-
- public static class EventRingBuffer {
- private final int bufSize;
- private final float xBuf[];
- private final float yBuf[];
- private final long timeBuf[];
- private int top; // points new event
- private int end; // points oldest event
- private int count; // the number of valid data
-
- public EventRingBuffer(int max) {
- this.bufSize = max;
- xBuf = new float[max];
- yBuf = new float[max];
- timeBuf = new long[max];
- clear();
- }
-
- public void clear() {
- top = end = count = 0;
- }
-
- public int size() {
- return count;
- }
-
- // Position 0 points oldest event
- private int index(int pos) {
- return (end + pos) % bufSize;
- }
-
- private int advance(int index) {
- return (index + 1) % bufSize;
- }
-
- public void add(float x, float y, long time) {
- xBuf[top] = x;
- yBuf[top] = y;
- timeBuf[top] = time;
- top = advance(top);
- if (count < bufSize) {
- count++;
- } else {
- end = advance(end);
- }
- }
-
- public float getX(int pos) {
- return xBuf[index(pos)];
- }
-
- public float getY(int pos) {
- return yBuf[index(pos)];
- }
-
- public long getTime(int pos) {
- return timeBuf[index(pos)];
- }
-
- public void dropOldest() {
- count--;
- end = advance(end);
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
new file mode 100644
index 000000000..f8f1395b3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.util.ArrayList;
+
+public class AdditionalSubtype {
+ private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
+
+ private AdditionalSubtype() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static boolean isAdditionalSubtype(InputMethodSubtype subtype) {
+ return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE);
+ }
+
+ private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
+ public static final String PREF_SUBTYPE_SEPARATOR = ";";
+
+ public static InputMethodSubtype createAdditionalSubtype(
+ String localeString, String keyboardLayoutSetName, String extraValue) {
+ final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
+ final String layoutDisplayNameExtraValue;
+ if (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15
+ && SubtypeLocale.isExceptionalLocale(localeString)) {
+ final String layoutDisplayName = SubtypeLocale.getKeyboardLayoutSetDisplayName(
+ keyboardLayoutSetName);
+ layoutDisplayNameExtraValue = StringUtils.appendToCsvIfNotExists(
+ UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
+ } else {
+ layoutDisplayNameExtraValue = extraValue;
+ }
+ final String additionalSubtypeExtraValue = StringUtils.appendToCsvIfNotExists(
+ IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
+ final int nameId = SubtypeLocale.getSubtypeNameId(localeString, keyboardLayoutSetName);
+ return new InputMethodSubtype(nameId, R.drawable.ic_subtype_keyboard,
+ localeString, KEYBOARD_MODE,
+ layoutExtraValue + "," + additionalSubtypeExtraValue, false, false);
+ }
+
+ public static String getPrefSubtype(InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
+ final String extraValue = StringUtils.removeFromCsvIfExists(layoutExtraValue,
+ StringUtils.removeFromCsvIfExists(IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
+ final String basePrefSubtype = localeString + LOCALE_AND_LAYOUT_SEPARATOR
+ + keyboardLayoutSetName;
+ return extraValue.isEmpty() ? basePrefSubtype
+ : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
+ }
+
+ public static InputMethodSubtype createAdditionalSubtype(String prefSubtype) {
+ final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
+ if (elems.length < 2 || elems.length > 3) {
+ throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype);
+ }
+ final String localeString = elems[0];
+ final String keyboardLayoutSetName = elems[1];
+ final String extraValue = (elems.length == 3) ? elems[2] : null;
+ return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue);
+ }
+
+ public static InputMethodSubtype[] createAdditionalSubtypesArray(String prefSubtypes) {
+ if (TextUtils.isEmpty(prefSubtypes)) {
+ return EMPTY_SUBTYPE_ARRAY;
+ }
+ final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
+ final ArrayList<InputMethodSubtype> subtypesList =
+ new ArrayList<InputMethodSubtype>(prefSubtypeArray.length);
+ for (final String prefSubtype : prefSubtypeArray) {
+ final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
+ if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) {
+ // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
+ // layout has been removed.
+ continue;
+ }
+ subtypesList.add(subtype);
+ }
+ return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
new file mode 100644
index 000000000..779a38823
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -0,0 +1,599 @@
+/**
+ * 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;
+
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.DialogPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceGroup;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.SpinnerAdapter;
+import android.widget.Toast;
+
+import com.android.inputmethod.compat.CompatUtils;
+
+import java.util.ArrayList;
+import java.util.TreeSet;
+
+public class AdditionalSubtypeSettings extends PreferenceFragment {
+ private SharedPreferences mPrefs;
+ private SubtypeLocaleAdapter mSubtypeLocaleAdapter;
+ private KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
+
+ private boolean mIsAddingNewSubtype;
+ private AlertDialog mSubtypeEnablerNotificationDialog;
+ private String mSubtypePreferenceKeyForSubtypeEnabler;
+
+ private static final int MENU_ADD_SUBTYPE = Menu.FIRST;
+ private static final String KEY_IS_ADDING_NEW_SUBTYPE = "is_adding_new_subtype";
+ private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
+ "is_subtype_enabler_notification_dialog_open";
+ private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
+ static class SubtypeLocaleItem extends Pair<String, String>
+ implements Comparable<SubtypeLocaleItem> {
+ public SubtypeLocaleItem(String localeString, String displayName) {
+ super(localeString, displayName);
+ }
+
+ public SubtypeLocaleItem(String localeString) {
+ this(localeString, SubtypeLocale.getSubtypeLocaleDisplayName(localeString));
+ }
+
+ @Override
+ public String toString() {
+ return second;
+ }
+
+ @Override
+ public int compareTo(SubtypeLocaleItem o) {
+ return first.compareTo(o.first);
+ }
+ }
+
+ static class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
+ public SubtypeLocaleAdapter(Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ final TreeSet<SubtypeLocaleItem> items = new TreeSet<SubtypeLocaleItem>();
+ final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context);
+ final int count = imi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+ if (subtype.containsExtraValueKey(ASCII_CAPABLE)) {
+ items.add(createItem(context, subtype.getLocale()));
+ }
+ }
+ // TODO: Should filter out already existing combinations of locale and layout.
+ addAll(items);
+ }
+
+ public static SubtypeLocaleItem createItem(Context context, String localeString) {
+ if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) {
+ final String displayName = context.getString(R.string.subtype_no_language);
+ return new SubtypeLocaleItem(localeString, displayName);
+ } else {
+ return new SubtypeLocaleItem(localeString);
+ }
+ }
+ }
+
+ static class KeyboardLayoutSetItem extends Pair<String, String> {
+ public KeyboardLayoutSetItem(InputMethodSubtype subtype) {
+ super(SubtypeLocale.getKeyboardLayoutSetName(subtype),
+ SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype));
+ }
+
+ @Override
+ public String toString() {
+ return second;
+ }
+ }
+
+ static class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
+ public KeyboardLayoutSetAdapter(Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ // TODO: Should filter out already existing combinations of locale and layout.
+ for (final String layout : SubtypeLocale.getPredefinedKeyboardLayoutSet()) {
+ // This is a dummy subtype with NO_LANGUAGE, only for display.
+ final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype(
+ SubtypeLocale.NO_LANGUAGE, layout, null);
+ add(new KeyboardLayoutSetItem(subtype));
+ }
+ }
+ }
+
+ private interface SubtypeDialogProxy {
+ public void onRemovePressed(SubtypePreference subtypePref);
+ public void onSavePressed(SubtypePreference subtypePref);
+ public void onAddPressed(SubtypePreference subtypePref);
+ public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
+ public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
+ }
+
+ static class SubtypePreference extends DialogPreference
+ implements DialogInterface.OnCancelListener {
+ private static final String KEY_PREFIX = "subtype_pref_";
+ private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
+
+ private InputMethodSubtype mSubtype;
+ private InputMethodSubtype mPreviousSubtype;
+
+ private final SubtypeDialogProxy mProxy;
+ private Spinner mSubtypeLocaleSpinner;
+ private Spinner mKeyboardLayoutSetSpinner;
+
+ public static SubtypePreference newIncompleteSubtypePreference(
+ Context context, SubtypeDialogProxy proxy) {
+ return new SubtypePreference(context, null, proxy);
+ }
+
+ public SubtypePreference(Context context, InputMethodSubtype subtype,
+ SubtypeDialogProxy proxy) {
+ super(context, null);
+ setDialogLayoutResource(R.layout.additional_subtype_dialog);
+ setPersistent(false);
+ mProxy = proxy;
+ setSubtype(subtype);
+ }
+
+ public void show() {
+ showDialog(null);
+ }
+
+ public final boolean isIncomplete() {
+ return mSubtype == null;
+ }
+
+ public InputMethodSubtype getSubtype() {
+ return mSubtype;
+ }
+
+ public void setSubtype(InputMethodSubtype subtype) {
+ mPreviousSubtype = mSubtype;
+ mSubtype = subtype;
+ if (isIncomplete()) {
+ setTitle(null);
+ setDialogTitle(R.string.add_style);
+ setKey(KEY_NEW_SUBTYPE);
+ } else {
+ final String displayName = SubtypeLocale.getSubtypeDisplayName(
+ subtype, getContext().getResources());
+ setTitle(displayName);
+ setDialogTitle(displayName);
+ setKey(KEY_PREFIX + subtype.getLocale() + "_"
+ + SubtypeLocale.getKeyboardLayoutSetName(subtype));
+ }
+ }
+
+ public void revert() {
+ setSubtype(mPreviousSubtype);
+ }
+
+ public boolean hasBeenModified() {
+ return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
+ }
+
+ @Override
+ protected View onCreateDialogView() {
+ final View v = super.onCreateDialogView();
+ mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner);
+ mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter());
+ mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner);
+ mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter());
+ return v;
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ final Context context = builder.getContext();
+ builder.setCancelable(true).setOnCancelListener(this);
+ if (isIncomplete()) {
+ builder.setPositiveButton(R.string.add, this)
+ .setNegativeButton(android.R.string.cancel, this);
+ } else {
+ builder.setPositiveButton(R.string.save, this)
+ .setNeutralButton(android.R.string.cancel, this)
+ .setNegativeButton(R.string.remove, this);
+ final SubtypeLocaleItem localeItem = SubtypeLocaleAdapter.createItem(
+ context, mSubtype.getLocale());
+ final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype);
+ setSpinnerPosition(mSubtypeLocaleSpinner, localeItem);
+ setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem);
+ }
+ }
+
+ private static void setSpinnerPosition(Spinner spinner, Object itemToSelect) {
+ final SpinnerAdapter adapter = spinner.getAdapter();
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ final Object item = spinner.getItemAtPosition(i);
+ if (item.equals(itemToSelect)) {
+ spinner.setSelection(i);
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (isIncomplete()) {
+ mProxy.onRemovePressed(this);
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ super.onClick(dialog, which);
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ final boolean isEditing = !isIncomplete();
+ final SubtypeLocaleItem locale =
+ (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
+ final KeyboardLayoutSetItem layout =
+ (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
+ final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype(
+ locale.first, layout.first, ASCII_CAPABLE);
+ setSubtype(subtype);
+ notifyChanged();
+ if (isEditing) {
+ mProxy.onSavePressed(this);
+ } else {
+ mProxy.onAddPressed(this);
+ }
+ break;
+ case DialogInterface.BUTTON_NEUTRAL:
+ // Nothing to do
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ mProxy.onRemovePressed(this);
+ break;
+ }
+ }
+
+ private static int getSpinnerPosition(Spinner spinner) {
+ if (spinner == null) return -1;
+ return spinner.getSelectedItemPosition();
+ }
+
+ private static void setSpinnerPosition(Spinner spinner, int position) {
+ if (spinner == null || position < 0) return;
+ spinner.setSelection(position);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final Dialog dialog = getDialog();
+ if (dialog == null || !dialog.isShowing()) {
+ return superState;
+ }
+
+ final SavedState myState = new SavedState(superState);
+ myState.mSubtype = mSubtype;
+ myState.mSubtypeLocaleSelectedPos = getSpinnerPosition(mSubtypeLocaleSpinner);
+ myState.mKeyboardLayoutSetSelectedPos = getSpinnerPosition(mKeyboardLayoutSetSpinner);
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ final SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ setSpinnerPosition(mSubtypeLocaleSpinner, myState.mSubtypeLocaleSelectedPos);
+ setSpinnerPosition(mKeyboardLayoutSetSpinner, myState.mKeyboardLayoutSetSelectedPos);
+ setSubtype(myState.mSubtype);
+ }
+
+ static class SavedState extends Preference.BaseSavedState {
+ InputMethodSubtype mSubtype;
+ int mSubtypeLocaleSelectedPos;
+ int mKeyboardLayoutSetSelectedPos;
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(mSubtypeLocaleSelectedPos);
+ dest.writeInt(mKeyboardLayoutSetSelectedPos);
+ dest.writeParcelable(mSubtype, 0);
+ }
+
+ public SavedState(Parcel source) {
+ super(source);
+ mSubtypeLocaleSelectedPos = source.readInt();
+ mKeyboardLayoutSetSelectedPos = source.readInt();
+ mSubtype = (InputMethodSubtype)source.readParcelable(null);
+ }
+
+ @SuppressWarnings("hiding")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel source) {
+ return new SavedState(source);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+ }
+
+ public AdditionalSubtypeSettings() {
+ // Empty constructor for fragment generation.
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.additional_subtype_settings);
+ setHasOptionsMenu(true);
+
+ mPrefs = getPreferenceManager().getSharedPreferences();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ final Context context = getActivity();
+ mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context);
+ mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context);
+
+ final String prefSubtypes =
+ SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources());
+ setPrefSubtypes(prefSubtypes, context);
+
+ mIsAddingNewSubtype = (savedInstanceState != null)
+ && savedInstanceState.containsKey(KEY_IS_ADDING_NEW_SUBTYPE);
+ if (mIsAddingNewSubtype) {
+ getPreferenceScreen().addPreference(
+ SubtypePreference.newIncompleteSubtypePreference(context, mSubtypeProxy));
+ }
+
+ super.onActivityCreated(savedInstanceState);
+
+ if (savedInstanceState != null && savedInstanceState.containsKey(
+ KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) {
+ mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString(
+ KEY_SUBTYPE_FOR_SUBTYPE_ENABLER);
+ final SubtypePreference subtypePref = (SubtypePreference)findPreference(
+ mSubtypePreferenceKeyForSubtypeEnabler);
+ mSubtypeEnablerNotificationDialog = createDialog(subtypePref);
+ mSubtypeEnablerNotificationDialog.show();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mIsAddingNewSubtype) {
+ outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true);
+ }
+ if (mSubtypeEnablerNotificationDialog != null
+ && mSubtypeEnablerNotificationDialog.isShowing()) {
+ outState.putBoolean(KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN, true);
+ outState.putString(
+ KEY_SUBTYPE_FOR_SUBTYPE_ENABLER, mSubtypePreferenceKeyForSubtypeEnabler);
+ }
+ }
+
+ private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() {
+ @Override
+ public void onRemovePressed(SubtypePreference subtypePref) {
+ mIsAddingNewSubtype = false;
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(subtypePref);
+ ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes());
+ }
+
+ @Override
+ public void onSavePressed(SubtypePreference subtypePref) {
+ final InputMethodSubtype subtype = subtypePref.getSubtype();
+ if (!subtypePref.hasBeenModified()) {
+ return;
+ }
+ if (findDuplicatedSubtype(subtype) == null) {
+ ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes());
+ return;
+ }
+
+ // Saved subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(subtypePref);
+ subtypePref.revert();
+ group.addPreference(subtypePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
+
+ @Override
+ public void onAddPressed(SubtypePreference subtypePref) {
+ mIsAddingNewSubtype = false;
+ final InputMethodSubtype subtype = subtypePref.getSubtype();
+ if (findDuplicatedSubtype(subtype) == null) {
+ ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), getSubtypes());
+ mSubtypePreferenceKeyForSubtypeEnabler = subtypePref.getKey();
+ mSubtypeEnablerNotificationDialog = createDialog(subtypePref);
+ mSubtypeEnablerNotificationDialog.show();
+ return;
+ }
+
+ // Newly added subtype is duplicated.
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removePreference(subtypePref);
+ showSubtypeAlreadyExistsToast(subtype);
+ }
+
+ @Override
+ public SubtypeLocaleAdapter getSubtypeLocaleAdapter() {
+ return mSubtypeLocaleAdapter;
+ }
+
+ @Override
+ public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() {
+ return mKeyboardLayoutSetAdapter;
+ }
+ };
+
+ private void showSubtypeAlreadyExistsToast(InputMethodSubtype subtype) {
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ final String message = res.getString(R.string.custom_input_style_already_exists,
+ SubtypeLocale.getSubtypeDisplayName(subtype, res));
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+ }
+
+ private InputMethodSubtype findDuplicatedSubtype(InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ return ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
+ getActivity(), localeString, keyboardLayoutSetName);
+ }
+
+ private AlertDialog createDialog(SubtypePreference subtypePref) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.custom_input_styles_title)
+ .setMessage(R.string.custom_input_style_note_message)
+ .setNegativeButton(R.string.not_now, null)
+ .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Intent intent = CompatUtils.getInputLanguageSelectionIntent(
+ ImfUtils.getInputMethodIdOfThisIme(getActivity()),
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ // TODO: Add newly adding subtype to extra value of the intent as a hint
+ // for the input language selection activity.
+ // intent.putExtra("newlyAddedSubtype", subtypePref.getSubtype());
+ startActivity(intent);
+ }
+ });
+
+ return builder.create();
+ }
+
+ private void setPrefSubtypes(String prefSubtypes, Context context) {
+ final PreferenceGroup group = getPreferenceScreen();
+ group.removeAll();
+ final InputMethodSubtype[] subtypesArray =
+ AdditionalSubtype.createAdditionalSubtypesArray(prefSubtypes);
+ for (final InputMethodSubtype subtype : subtypesArray) {
+ final SubtypePreference pref = new SubtypePreference(
+ context, subtype, mSubtypeProxy);
+ group.addPreference(pref);
+ }
+ }
+
+ private InputMethodSubtype[] getSubtypes() {
+ final PreferenceGroup group = getPreferenceScreen();
+ final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ final int count = group.getPreferenceCount();
+ for (int i = 0; i < count; i++) {
+ final Preference pref = group.getPreference(i);
+ if (pref instanceof SubtypePreference) {
+ final SubtypePreference subtypePref = (SubtypePreference)pref;
+ // We should not save newly adding subtype to preference because it is incomplete.
+ if (subtypePref.isIncomplete()) continue;
+ subtypes.add(subtypePref.getSubtype());
+ }
+ }
+ return subtypes.toArray(new InputMethodSubtype[subtypes.size()]);
+ }
+
+ private String getPrefSubtypes(InputMethodSubtype[] subtypes) {
+ final StringBuilder sb = new StringBuilder();
+ for (final InputMethodSubtype subtype : subtypes) {
+ if (sb.length() > 0) {
+ sb.append(AdditionalSubtype.PREF_SUBTYPE_SEPARATOR);
+ }
+ sb.append(AdditionalSubtype.getPrefSubtype(subtype));
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ final String oldSubtypes = SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources());
+ final InputMethodSubtype[] subtypes = getSubtypes();
+ final String prefSubtypes = getPrefSubtypes(subtypes);
+ if (prefSubtypes.equals(oldSubtypes)) {
+ return;
+ }
+
+ final SharedPreferences.Editor editor = mPrefs.edit();
+ try {
+ editor.putString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes);
+ } finally {
+ editor.apply();
+ }
+ ImfUtils.setAdditionalInputMethodSubtypes(getActivity(), subtypes);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style);
+ addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == MENU_ADD_SUBTYPE) {
+ final SubtypePreference newSubtype =
+ SubtypePreference.newIncompleteSubtypePreference(getActivity(), mSubtypeProxy);
+ getPreferenceScreen().addPreference(newSubtype);
+ newSubtype.show();
+ mIsAddingNewSubtype = true;
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 074ecacc5..3549a1561 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -37,16 +37,16 @@ class AssetFileAddress {
public static AssetFileAddress makeFromFileName(final String filename) {
if (null == filename) return null;
- File f = new File(filename);
- if (null == f || !f.isFile()) return null;
+ final File f = new File(filename);
+ if (!f.isFile()) return null;
return new AssetFileAddress(filename, 0l, f.length());
}
public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
final long offset, final long length) {
if (null == filename) return null;
- File f = new File(filename);
- if (null == f || !f.isFile()) return null;
+ final File f = new File(filename);
+ if (!f.isFile()) return null;
return new AssetFileAddress(filename, offset, length);
}
}
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
new file mode 100644
index 000000000..55664d411
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.view.HapticFeedbackConstants;
+import android.view.View;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.VibratorUtils;
+
+/**
+ * This class gathers audio feedback and haptic feedback functions.
+ *
+ * It offers a consistent and simple interface that allows LatinIME to forget about the
+ * complexity of settings and the like.
+ */
+public class AudioAndHapticFeedbackManager {
+ final private SettingsValues mSettingsValues;
+ final private AudioManager mAudioManager;
+ final private VibratorUtils mVibratorUtils;
+ private boolean mSoundOn;
+
+ public AudioAndHapticFeedbackManager(final LatinIME latinIme,
+ final SettingsValues settingsValues) {
+ mSettingsValues = settingsValues;
+ mVibratorUtils = VibratorUtils.getInstance(latinIme);
+ mAudioManager = (AudioManager) latinIme.getSystemService(Context.AUDIO_SERVICE);
+ mSoundOn = reevaluateIfSoundIsOn();
+ }
+
+ public void hapticAndAudioFeedback(final int primaryCode,
+ final View viewToPerformHapticFeedbackOn) {
+ vibrate(viewToPerformHapticFeedbackOn);
+ playKeyClick(primaryCode);
+ }
+
+ private boolean reevaluateIfSoundIsOn() {
+ if (!mSettingsValues.mSoundOn || mAudioManager == null) {
+ return false;
+ } else {
+ return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
+ }
+ }
+
+ private void playKeyClick(int primaryCode) {
+ // if mAudioManager is null, we can't play a sound anyway, so return
+ if (mAudioManager == null) return;
+ if (mSoundOn) {
+ final int sound;
+ switch (primaryCode) {
+ case Keyboard.CODE_DELETE:
+ sound = AudioManager.FX_KEYPRESS_DELETE;
+ break;
+ case Keyboard.CODE_ENTER:
+ sound = AudioManager.FX_KEYPRESS_RETURN;
+ break;
+ case Keyboard.CODE_SPACE:
+ sound = AudioManager.FX_KEYPRESS_SPACEBAR;
+ break;
+ default:
+ sound = AudioManager.FX_KEYPRESS_STANDARD;
+ break;
+ }
+ mAudioManager.playSoundEffect(sound, mSettingsValues.mFxVolume);
+ }
+ }
+
+ // TODO: make this private when LatinIME does not call it any more
+ public void vibrate(final View viewToPerformHapticFeedbackOn) {
+ if (!mSettingsValues.mVibrateOn) {
+ return;
+ }
+ if (mSettingsValues.mKeypressVibrationDuration < 0) {
+ // Go ahead with the system default
+ if (viewToPerformHapticFeedbackOn != null) {
+ viewToPerformHapticFeedbackOn.performHapticFeedback(
+ HapticFeedbackConstants.KEYBOARD_TAP,
+ HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
+ }
+ } else if (mVibratorUtils != null) {
+ mVibratorUtils.vibrate(mSettingsValues.mKeypressVibrationDuration);
+ }
+ }
+
+ public void onRingerModeChanged() {
+ mSoundOn = reevaluateIfSoundIsOn();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index d3119792c..e0452483c 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -16,60 +16,41 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
-import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
public class AutoCorrection {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = AutoCorrection.class.getSimpleName();
- private boolean mHasAutoCorrection;
- private CharSequence mAutoCorrectionWord;
- private double mNormalizedScore;
-
- public void init() {
- mHasAutoCorrection = false;
- mAutoCorrectionWord = null;
- mNormalizedScore = Integer.MIN_VALUE;
- }
-
- public boolean hasAutoCorrection() {
- return mHasAutoCorrection;
- }
- public CharSequence getAutoCorrectionWord() {
- return mAutoCorrectionWord;
+ private AutoCorrection() {
+ // Purely static class: can't instantiate.
}
- public double getNormalizedScore() {
- return mNormalizedScore;
- }
-
- public void updateAutoCorrectionStatus(Map<String, Dictionary> dictionaries,
- WordComposer wordComposer, ArrayList<CharSequence> suggestions, int[] sortedScores,
- CharSequence typedWord, double autoCorrectionThreshold, int correctionMode,
- CharSequence quickFixedWord, CharSequence whitelistedWord) {
+ public static CharSequence computeAutoCorrectionWord(
+ final ConcurrentHashMap<String, Dictionary> dictionaries,
+ final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
+ final CharSequence consideredWord, final float autoCorrectionThreshold,
+ final CharSequence whitelistedWord) {
if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
- mHasAutoCorrection = true;
- mAutoCorrectionWord = whitelistedWord;
- } else if (hasAutoCorrectionForTypedWord(
- dictionaries, wordComposer, suggestions, typedWord, correctionMode)) {
- mHasAutoCorrection = true;
- mAutoCorrectionWord = typedWord;
- } else if (hasAutoCorrectionForQuickFix(quickFixedWord)) {
- mHasAutoCorrection = true;
- mAutoCorrectionWord = quickFixedWord;
- } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions, correctionMode,
- sortedScores, typedWord, autoCorrectionThreshold)) {
- mHasAutoCorrection = true;
- mAutoCorrectionWord = suggestions.get(0);
+ return whitelistedWord;
+ } else if (hasAutoCorrectionForConsideredWord(
+ dictionaries, wordComposer, suggestions, consideredWord)) {
+ return consideredWord;
+ } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions,
+ consideredWord, autoCorrectionThreshold)) {
+ return suggestions.get(0).mWord;
}
+ return null;
}
- public static boolean isValidWord(
- Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) {
+ public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
+ CharSequence word, boolean ignoreCase) {
if (TextUtils.isEmpty(word)) {
return false;
}
@@ -77,6 +58,14 @@ public class AutoCorrection {
for (final String key : dictionaries.keySet()) {
if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
final Dictionary dictionary = dictionaries.get(key);
+ // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
+ // managing to get null in here. Presumably the language is changing to a language with
+ // no main dictionary and the monkey manages to type a whole word before the thread
+ // that reads the dictionary is started or something?
+ // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
+ // would be immutable once it's finished initializing, but concretely a null test is
+ // probably good enough for the time being.
+ if (null == dictionary) continue;
if (dictionary.isValidWord(word)
|| (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
return true;
@@ -85,52 +74,68 @@ public class AutoCorrection {
return false;
}
- public static boolean isValidWordForAutoCorrection(
- Map<String, Dictionary> dictionaries, CharSequence word, boolean ignoreCase) {
- final Dictionary whiteList = dictionaries.get(Suggest.DICT_KEY_WHITELIST);
+ public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries,
+ CharSequence word) {
+ if (TextUtils.isEmpty(word)) {
+ return Dictionary.NOT_A_PROBABILITY;
+ }
+ int maxFreq = -1;
+ for (final String key : dictionaries.keySet()) {
+ if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
+ final Dictionary dictionary = dictionaries.get(key);
+ if (null == dictionary) continue;
+ final int tempFreq = dictionary.getFrequency(word);
+ if (tempFreq >= maxFreq) {
+ maxFreq = tempFreq;
+ }
+ }
+ return maxFreq;
+ }
+
+ public static boolean allowsToBeAutoCorrected(
+ final ConcurrentHashMap<String, Dictionary> dictionaries,
+ final CharSequence word, final boolean ignoreCase) {
+ final WhitelistDictionary whitelistDictionary =
+ (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_WHITELIST);
// If "word" is in the whitelist dictionary, it should not be auto corrected.
- if (whiteList != null && whiteList.isValidWord(word)) {
- return false;
+ if (whitelistDictionary != null
+ && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
+ return true;
}
- return isValidWord(dictionaries, word, ignoreCase);
+ return !isValidWord(dictionaries, word, ignoreCase);
}
private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
return whiteListedWord != null;
}
- private boolean hasAutoCorrectionForTypedWord(Map<String, Dictionary> dictionaries,
- WordComposer wordComposer, ArrayList<CharSequence> suggestions, CharSequence typedWord,
- int correctionMode) {
- if (TextUtils.isEmpty(typedWord)) return false;
- boolean isValidWord = isValidWordForAutoCorrection(dictionaries, typedWord, false);
- return wordComposer.size() > 1 && suggestions.size() > 0 && isValidWord
- && (correctionMode == Suggest.CORRECTION_FULL
- || correctionMode == Suggest.CORRECTION_FULL_BIGRAM);
- }
-
- private static boolean hasAutoCorrectionForQuickFix(CharSequence quickFixedWord) {
- return quickFixedWord != null;
+ private static boolean hasAutoCorrectionForConsideredWord(
+ final ConcurrentHashMap<String, Dictionary> dictionaries,
+ final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
+ final CharSequence consideredWord) {
+ if (TextUtils.isEmpty(consideredWord)) return false;
+ return wordComposer.size() > 1 && suggestions.size() > 0
+ && !allowsToBeAutoCorrected(dictionaries, consideredWord, false);
}
- private boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
- ArrayList<CharSequence> suggestions, int correctionMode, int[] sortedScores,
- CharSequence typedWord, double autoCorrectionThreshold) {
- if (wordComposer.size() > 1 && (correctionMode == Suggest.CORRECTION_FULL
- || correctionMode == Suggest.CORRECTION_FULL_BIGRAM)
- && typedWord != null && suggestions.size() > 0 && sortedScores.length > 0) {
- final CharSequence autoCorrectionCandidate = suggestions.get(0);
- final int autoCorrectionCandidateScore = sortedScores[0];
+ private static boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
+ ArrayList<SuggestedWordInfo> suggestions,
+ CharSequence consideredWord, float autoCorrectionThreshold) {
+ if (wordComposer.size() > 1 && suggestions.size() > 0) {
+ final SuggestedWordInfo autoCorrectionSuggestion = suggestions.get(0);
+ //final int autoCorrectionSuggestionScore = sortedScores[0];
+ final int autoCorrectionSuggestionScore = autoCorrectionSuggestion.mScore;
// TODO: when the normalized score of the first suggestion is nearly equals to
// the normalized score of the second suggestion, behave less aggressive.
- mNormalizedScore = Utils.calcNormalizedScore(
- typedWord,autoCorrectionCandidate, autoCorrectionCandidateScore);
+ final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+ consideredWord.toString(), autoCorrectionSuggestion.mWord.toString(),
+ autoCorrectionSuggestionScore);
if (DBG) {
- Log.d(TAG, "Normalized " + typedWord + "," + autoCorrectionCandidate + ","
- + autoCorrectionCandidateScore + ", " + mNormalizedScore
+ Log.d(TAG, "Normalized " + consideredWord + "," + autoCorrectionSuggestion + ","
+ + autoCorrectionSuggestionScore + ", " + normalizedScore
+ "(" + autoCorrectionThreshold + ")");
}
- if (mNormalizedScore >= autoCorrectionThreshold) {
+ if (normalizedScore >= autoCorrectionThreshold) {
if (DBG) {
Log.d(TAG, "Auto corrected by S-threshold.");
}
diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java
deleted file mode 100644
index 460930f16..000000000
--- a/java/src/com/android/inputmethod/latin/AutoDictionary.java
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.os.AsyncTask;
-import android.provider.BaseColumns;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.Map.Entry;
-import java.util.Set;
-
-/**
- * Stores new words temporarily until they are promoted to the user dictionary
- * for longevity. Words in the auto dictionary are used to determine if it's ok
- * to accept a word that's not in the main or user dictionary. Using a new word
- * repeatedly will promote it to the user dictionary.
- */
-public class AutoDictionary extends ExpandableDictionary {
- // Weight added to a user picking a new word from the suggestion strip
- static final int FREQUENCY_FOR_PICKED = 3;
- // Weight added to a user typing a new word that doesn't get corrected (or is reverted)
- static final int FREQUENCY_FOR_TYPED = 1;
- // If the user touches a typed word 2 times or more, it will become valid.
- private static final int VALIDITY_THRESHOLD = 2 * FREQUENCY_FOR_PICKED;
-
- private LatinIME mIme;
- // Locale for which this auto dictionary is storing words
- private String mLocale;
-
- private HashMap<String,Integer> mPendingWrites = new HashMap<String,Integer>();
- private final Object mPendingWritesLock = new Object();
-
- private static final String DATABASE_NAME = "auto_dict.db";
- private static final int DATABASE_VERSION = 1;
-
- // These are the columns in the dictionary
- // TODO: Consume less space by using a unique id for locale instead of the whole
- // 2-5 character string.
- private static final String COLUMN_ID = BaseColumns._ID;
- private static final String COLUMN_WORD = "word";
- private static final String COLUMN_FREQUENCY = "freq";
- private static final String COLUMN_LOCALE = "locale";
-
- /** Sort by descending order of frequency. */
- public static final String DEFAULT_SORT_ORDER = COLUMN_FREQUENCY + " DESC";
-
- /** Name of the words table in the auto_dict.db */
- private static final String AUTODICT_TABLE_NAME = "words";
-
- private static HashMap<String, String> sDictProjectionMap;
-
- static {
- sDictProjectionMap = new HashMap<String, String>();
- sDictProjectionMap.put(COLUMN_ID, COLUMN_ID);
- sDictProjectionMap.put(COLUMN_WORD, COLUMN_WORD);
- sDictProjectionMap.put(COLUMN_FREQUENCY, COLUMN_FREQUENCY);
- sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE);
- }
-
- private static DatabaseHelper sOpenHelper = null;
-
- public AutoDictionary(Context context, LatinIME ime, String locale, int dicTypeId) {
- super(context, dicTypeId);
- mIme = ime;
- mLocale = locale;
- if (sOpenHelper == null) {
- sOpenHelper = new DatabaseHelper(getContext());
- }
- if (mLocale != null && mLocale.length() > 1) {
- loadDictionary();
- }
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- final int frequency = getWordFrequency(word);
- return frequency >= VALIDITY_THRESHOLD;
- }
-
- @Override
- public void close() {
- flushPendingWrites();
- // Don't close the database as locale changes will require it to be reopened anyway
- // Also, the database is written to somewhat frequently, so it needs to be kept alive
- // throughout the life of the process.
- // mOpenHelper.close();
- super.close();
- }
-
- @Override
- public void loadDictionaryAsync() {
- // Load the words that correspond to the current input locale
- Cursor cursor = query(COLUMN_LOCALE + "=?", new String[] { mLocale });
- try {
- if (cursor.moveToFirst()) {
- int wordIndex = cursor.getColumnIndex(COLUMN_WORD);
- int frequencyIndex = cursor.getColumnIndex(COLUMN_FREQUENCY);
- while (!cursor.isAfterLast()) {
- String word = cursor.getString(wordIndex);
- int frequency = cursor.getInt(frequencyIndex);
- // Safeguard against adding really long words. Stack may overflow due
- // to recursive lookup
- if (word.length() < getMaxWordLength()) {
- super.addWord(word, frequency);
- }
- cursor.moveToNext();
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- @Override
- public void addWord(String newWord, int addFrequency) {
- String word = newWord;
- final int length = word.length();
- // Don't add very short or very long words.
- if (length < 2 || length > getMaxWordLength()) return;
- if (mIme.getCurrentWord().isAutoCapitalized()) {
- // Remove caps before adding
- word = Character.toLowerCase(word.charAt(0)) + word.substring(1);
- }
- int freq = getWordFrequency(word);
- freq = freq < 0 ? addFrequency : freq + addFrequency;
- super.addWord(word, freq);
-
- synchronized (mPendingWritesLock) {
- // Write a null frequency if it is to be deleted from the db
- mPendingWrites.put(word, freq == 0 ? null : new Integer(freq));
- }
- }
-
- /**
- * Schedules a background thread to write any pending words to the database.
- */
- public void flushPendingWrites() {
- synchronized (mPendingWritesLock) {
- // Nothing pending? Return
- if (mPendingWrites.isEmpty()) return;
- // Create a background thread to write the pending entries
- new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute();
- // Create a new map for writing new entries into while the old one is written to db
- mPendingWrites = new HashMap<String, Integer>();
- }
- }
-
- /**
- * This class helps open, create, and upgrade the database file.
- */
- private static class DatabaseHelper extends SQLiteOpenHelper {
-
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + AUTODICT_TABLE_NAME + " ("
- + COLUMN_ID + " INTEGER PRIMARY KEY,"
- + COLUMN_WORD + " TEXT,"
- + COLUMN_FREQUENCY + " INTEGER,"
- + COLUMN_LOCALE + " TEXT"
- + ");");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.w("AutoDictionary", "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS " + AUTODICT_TABLE_NAME);
- onCreate(db);
- }
- }
-
- private Cursor query(String selection, String[] selectionArgs) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables(AUTODICT_TABLE_NAME);
- qb.setProjectionMap(sDictProjectionMap);
-
- // Get the database and run the query
- SQLiteDatabase db = sOpenHelper.getReadableDatabase();
- Cursor c = qb.query(db, null, selection, selectionArgs, null, null,
- DEFAULT_SORT_ORDER);
- return c;
- }
-
- /**
- * Async task to write pending words to the database so that it stays in sync with
- * the in-memory trie.
- */
- private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
- private final HashMap<String, Integer> mMap;
- private final DatabaseHelper mDbHelper;
- private final String mLocale;
-
- public UpdateDbTask(@SuppressWarnings("unused") Context context, DatabaseHelper openHelper,
- HashMap<String, Integer> pendingWrites, String locale) {
- mMap = pendingWrites;
- mLocale = locale;
- mDbHelper = openHelper;
- }
-
- @Override
- protected Void doInBackground(Void... v) {
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
- // Write all the entries to the db
- Set<Entry<String,Integer>> mEntries = mMap.entrySet();
- for (Entry<String,Integer> entry : mEntries) {
- Integer freq = entry.getValue();
- db.delete(AUTODICT_TABLE_NAME, COLUMN_WORD + "=? AND " + COLUMN_LOCALE + "=?",
- new String[] { entry.getKey(), mLocale });
- if (freq != null) {
- db.insert(AUTODICT_TABLE_NAME, null,
- getContentValues(entry.getKey(), freq, mLocale));
- }
- }
- return null;
- }
-
- private ContentValues getContentValues(String word, int frequency, String locale) {
- ContentValues values = new ContentValues(4);
- values.put(COLUMN_WORD, word);
- values.put(COLUMN_FREQUENCY, frequency);
- values.put(COLUMN_LOCALE, locale);
- return values;
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 9748d6006..d0613bd72 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -16,13 +16,13 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.keyboard.ProximityInfo;
-
import android.content.Context;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
import java.util.Arrays;
+import java.util.Locale;
/**
* Implements a static, compacted, binary dictionary of standard words.
@@ -41,38 +41,20 @@ public class BinaryDictionary extends Dictionary {
public static final int MAX_WORD_LENGTH = 48;
public static final int MAX_WORDS = 18;
- @SuppressWarnings("unused")
private static final String TAG = "BinaryDictionary";
- private static final int MAX_PROXIMITY_CHARS_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
private static final int MAX_BIGRAMS = 60;
private static final int TYPED_LETTER_MULTIPLIER = 2;
private int mDicTypeId;
- private int mNativeDict;
- private final int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_PROXIMITY_CHARS_SIZE];
+ private long mNativeDict;
+ private final int[] mInputCodes = new int[MAX_WORD_LENGTH];
private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
private final int[] mScores = new int[MAX_WORDS];
private final int[] mBigramScores = new int[MAX_BIGRAMS];
- private final KeyboardSwitcher mKeyboardSwitcher = KeyboardSwitcher.getInstance();
-
- public static final Flag FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING =
- new Flag(R.bool.config_require_umlaut_processing, 0x1);
-
- // Can create a new flag from extravalue :
- // public static final Flag FLAG_MYFLAG =
- // new Flag("my_flag", 0x02);
-
- private static final Flag[] ALL_FLAGS = {
- // Here should reside all flags that trigger some special processing
- // These *must* match the definition in UnigramDictionary enum in
- // unigram_dictionary.h so please update both at the same time.
- FLAG_REQUIRES_GERMAN_UMLAUT_PROCESSING,
- };
-
- private int mFlags = 0;
+ private final boolean mUseFullEditDistance;
/**
* Constructor for the binary dictionary. This is supposed to be called from the
@@ -82,40 +64,42 @@ public class BinaryDictionary extends Dictionary {
* @param filename the name of the file to read through native code.
* @param offset the offset of the dictionary data within the file.
* @param length the length of the binary data.
- * @param flagArray the flags to limit the dictionary to, or null for default.
+ * @param useFullEditDistance whether to use the full edit distance in suggestions
*/
public BinaryDictionary(final Context context,
- final String filename, final long offset, final long length, Flag[] flagArray) {
+ final String filename, final long offset, final long length,
+ final boolean useFullEditDistance, final Locale locale) {
// Note: at the moment a binary dictionary is always of the "main" type.
// Initializing this here will help transitioning out of the scheme where
// the Suggest class knows everything about every single dictionary.
mDicTypeId = Suggest.DIC_MAIN;
- // TODO: Stop relying on the state of SubtypeSwitcher, get it as a parameter
- mFlags = Flag.initFlags(null == flagArray ? ALL_FLAGS : flagArray, context,
- SubtypeSwitcher.getInstance());
+ mUseFullEditDistance = useFullEditDistance;
loadDictionary(filename, offset, length);
}
static {
- Utils.loadNativeLibrary();
+ JniUtils.loadNativeLibrary();
}
- private native int openNative(String sourceDir, long dictOffset, long dictSize,
- int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength,
- int maxWords, int maxAlternatives);
- private native void closeNative(int dict);
- private native boolean isValidWordNative(int nativeData, char[] word, int wordLength);
- private native int getSuggestionsNative(int dict, int proximityInfo, int[] xCoordinates,
- int[] yCoordinates, int[] inputCodes, int codesSize, int flags, char[] outputChars,
- int[] scores);
- private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength,
+ private native long openNative(String sourceDir, long dictOffset, long dictSize,
+ int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords);
+ private native void closeNative(long dict);
+ private native int getFrequencyNative(long dict, int[] word, int wordLength);
+ private native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
+ private native int getSuggestionsNative(long dict, long proximityInfo, int[] xCoordinates,
+ int[] yCoordinates, int[] inputCodes, int codesSize, int[] prevWordForBigrams,
+ boolean useFullEditDistance, char[] outputChars, int[] scores);
+ private native int getBigramsNative(long dict, int[] prevWord, int prevWordLength,
int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores,
- int maxWordLength, int maxBigrams, int maxAlternatives);
+ int maxWordLength, int maxBigrams);
+ private static native float calcNormalizedScoreNative(
+ char[] before, int beforeLength, char[] after, int afterLength, int score);
+ private static native int editDistanceNative(
+ char[] before, int beforeLength, char[] after, int afterLength);
private final void loadDictionary(String path, long startOffset, long length) {
mNativeDict = openNative(path, startOffset, length,
- TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER,
- MAX_WORD_LENGTH, MAX_WORDS, MAX_PROXIMITY_CHARS_SIZE);
+ TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS);
}
@Override
@@ -123,27 +107,24 @@ public class BinaryDictionary extends Dictionary {
final WordCallback callback) {
if (mNativeDict == 0) return;
- char[] chars = previousWord.toString().toCharArray();
+ int[] codePoints = StringUtils.toCodePointArray(previousWord.toString());
Arrays.fill(mOutputChars_bigrams, (char) 0);
Arrays.fill(mBigramScores, 0);
int codesSize = codes.size();
- if (codesSize <= 0) {
- // Do not return bigrams from BinaryDictionary when nothing was typed.
- // Only use user-history bigrams (or whatever other bigram dictionaries decide).
- return;
- }
Arrays.fill(mInputCodes, -1);
- int[] alternatives = codes.getCodesAt(0);
- System.arraycopy(alternatives, 0, mInputCodes, 0,
- Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE));
+ if (codesSize > 0) {
+ mInputCodes[0] = codes.getCodeAt(0);
+ }
- int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize,
- mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS,
- MAX_PROXIMITY_CHARS_SIZE);
+ int count = getBigramsNative(mNativeDict, codePoints, codePoints.length, mInputCodes,
+ codesSize, mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS);
+ if (count > MAX_BIGRAMS) {
+ count = MAX_BIGRAMS;
+ }
for (int j = 0; j < count; ++j) {
- if (mBigramScores[j] < 1) break;
+ if (codesSize > 0 && mBigramScores[j] < 1) break;
final int start = j * MAX_WORD_LENGTH;
int len = 0;
while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) {
@@ -151,15 +132,17 @@ public class BinaryDictionary extends Dictionary {
}
if (len > 0) {
callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j],
- mDicTypeId, DataType.BIGRAM);
+ mDicTypeId, Dictionary.BIGRAM);
}
}
}
+ // proximityInfo and/or prevWordForBigrams may not be null.
@Override
- public void getWords(final WordComposer codes, final WordCallback callback) {
- final int count = getSuggestions(codes, mKeyboardSwitcher.getLatinKeyboard(),
- mOutputChars, mScores);
+ public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
+ final WordCallback callback, final ProximityInfo proximityInfo) {
+ final int count = getSuggestions(codes, prevWordForBigrams, proximityInfo, mOutputChars,
+ mScores);
for (int j = 0; j < count; ++j) {
if (mScores[j] < 1) break;
@@ -170,7 +153,7 @@ public class BinaryDictionary extends Dictionary {
}
if (len > 0) {
callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId,
- DataType.UNIGRAM);
+ Dictionary.UNIGRAM);
}
}
}
@@ -179,7 +162,9 @@ public class BinaryDictionary extends Dictionary {
return mNativeDict != 0;
}
- /* package for test */ int getSuggestions(final WordComposer codes, final Keyboard keyboard,
+ // proximityInfo may not be null.
+ /* package for test */ int getSuggestions(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo,
char[] outputChars, int[] scores) {
if (!isValidDictionary()) return -1;
@@ -189,25 +174,50 @@ public class BinaryDictionary extends Dictionary {
Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
for (int i = 0; i < codesSize; i++) {
- int[] alternatives = codes.getCodesAt(i);
- System.arraycopy(alternatives, 0, mInputCodes, i * MAX_PROXIMITY_CHARS_SIZE,
- Math.min(alternatives.length, MAX_PROXIMITY_CHARS_SIZE));
+ mInputCodes[i] = codes.getCodeAt(i);
}
Arrays.fill(outputChars, (char) 0);
Arrays.fill(scores, 0);
- final int proximityInfo = keyboard == null ? 0 : keyboard.getProximityInfo();
+ final int[] prevWordCodePointArray = null == prevWordForBigrams
+ ? null : StringUtils.toCodePointArray(prevWordForBigrams.toString());
+
+ // TODO: pass the previous word to native code
return getSuggestionsNative(
- mNativeDict, proximityInfo,
+ mNativeDict, proximityInfo.getNativeProximityInfo(),
codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
- mFlags, outputChars, scores);
+ prevWordCodePointArray, mUseFullEditDistance, outputChars, scores);
+ }
+
+ public static float calcNormalizedScore(String before, String after, int score) {
+ return calcNormalizedScoreNative(before.toCharArray(), before.length(),
+ after.toCharArray(), after.length(), score);
+ }
+
+ public static int editDistance(String before, String after) {
+ return editDistanceNative(
+ before.toCharArray(), before.length(), after.toCharArray(), after.length());
}
@Override
public boolean isValidWord(CharSequence word) {
- if (word == null) return false;
- char[] chars = word.toString().toCharArray();
- return isValidWordNative(mNativeDict, chars, chars.length);
+ return getFrequency(word) >= 0;
+ }
+
+ @Override
+ public int getFrequency(CharSequence word) {
+ if (word == null) return -1;
+ int[] chars = StringUtils.toCodePointArray(word.toString());
+ return getFrequencyNative(mNativeDict, chars, chars.length);
+ }
+
+ // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
+ // calls when checking for changes in an entire dictionary.
+ public boolean isValidBigram(CharSequence word1, CharSequence word2) {
+ if (TextUtils.isEmpty(word1) || TextUtils.isEmpty(word2)) return false;
+ int[] chars1 = StringUtils.toCodePointArray(word1.toString());
+ int[] chars2 = StringUtils.toCodePointArray(word2.toString());
+ return isValidBigramNative(mNativeDict, chars1, chars2);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 76a230f82..37eced5d6 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -19,16 +19,20 @@ package com.android.inputmethod.latin;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
+import android.util.Log;
+import java.io.BufferedInputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
@@ -37,115 +41,262 @@ import java.util.Locale;
* file from the dictionary provider
*/
public class BinaryDictionaryFileDumper {
+ private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
/**
* The size of the temporary buffer to copy files.
*/
- static final int FILE_READ_BUFFER_SIZE = 1024;
+ private static final int FILE_READ_BUFFER_SIZE = 1024;
+ // TODO: make the following data common with the native code
+ private static final byte[] MAGIC_NUMBER_VERSION_1 =
+ new byte[] { (byte)0x78, (byte)0xB1, (byte)0x00, (byte)0x00 };
+ private static final byte[] MAGIC_NUMBER_VERSION_2 =
+ new byte[] { (byte)0x9B, (byte)0xC1, (byte)0x3A, (byte)0xFE };
+
+ private static final String DICTIONARY_PROJECTION[] = { "id" };
+
+ public static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
+ public static final String QUERY_PARAMETER_TRUE = "true";
+ public static final String QUERY_PARAMETER_DELETE_RESULT = "result";
+ public static final String QUERY_PARAMETER_SUCCESS = "success";
+ public static final String QUERY_PARAMETER_FAILURE = "failure";
// Prevents this class to be accidentally instantiated.
private BinaryDictionaryFileDumper() {
}
/**
- * Generates a file name that matches the locale passed as an argument.
- * The file name is basically the result of the .toString() method, except we replace
- * any @File.separator with an underscore to avoid generating a file name that may not
- * be created.
- * @param locale the locale for which to get the file name
- * @param context the context to use for getting the directory
- * @return the name of the file to be created
+ * Returns a URI builder pointing to the dictionary pack.
+ *
+ * This creates a URI builder able to build a URI pointing to the dictionary
+ * pack content provider for a specific dictionary id.
*/
- private static String getCacheFileNameForLocale(Locale locale, Context context) {
- // The following assumes two things :
- // 1. That File.separator is not the same character as "_"
- // I don't think any android system will ever use "_" as a path separator
- // 2. That no two locales differ by only a File.separator versus a "_"
- // Since "_" can't be part of locale components this should be safe.
- // Examples:
- // en -> en
- // en_US_POSIX -> en_US_POSIX
- // en__foo/bar -> en__foo_bar
- final String[] separator = { File.separator };
- final String[] empty = { "_" };
- final CharSequence basename = TextUtils.replace(locale.toString(), separator, empty);
- return context.getFilesDir() + File.separator + basename;
+ private static Uri.Builder getProviderUriBuilder(final String path) {
+ return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
+ path);
}
/**
- * Return for a given locale the provider URI to query to get the dictionary.
+ * Queries a content provider for the list of word lists for a specific locale
+ * available to copy into Latin IME.
*/
- public static Uri getProviderUri(Locale locale) {
- return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(BinaryDictionary.DICTIONARY_PACK_AUTHORITY).appendPath(
- locale.toString()).build();
+ private static List<WordListInfo> getWordListWordListInfos(final Locale locale,
+ final Context context, final boolean hasDefaultWordList) {
+ final ContentResolver resolver = context.getContentResolver();
+ final Uri.Builder builder = getProviderUriBuilder(locale.toString());
+ if (!hasDefaultWordList) {
+ builder.appendQueryParameter(QUERY_PARAMETER_MAY_PROMPT_USER, QUERY_PARAMETER_TRUE);
+ }
+ final Uri dictionaryPackUri = builder.build();
+
+ final Cursor c = resolver.query(dictionaryPackUri, DICTIONARY_PROJECTION, null, null, null);
+ if (null == c) return Collections.<WordListInfo>emptyList();
+ if (c.getCount() <= 0 || !c.moveToFirst()) {
+ c.close();
+ return Collections.<WordListInfo>emptyList();
+ }
+
+ try {
+ final List<WordListInfo> list = new ArrayList<WordListInfo>();
+ do {
+ final String wordListId = c.getString(0);
+ final String wordListLocale = c.getString(1);
+ if (TextUtils.isEmpty(wordListId)) continue;
+ list.add(new WordListInfo(wordListId, wordListLocale));
+ } while (c.moveToNext());
+ c.close();
+ return list;
+ } catch (Exception e) {
+ // Just in case we hit a problem in communication with the dictionary pack.
+ // We don't want to die.
+ Log.e(TAG, "Exception communicating with the dictionary pack : " + e);
+ return Collections.<WordListInfo>emptyList();
+ }
}
+
/**
- * Queries a content provider for dictionary data for some locale and returns the file addresses
- *
- * This will query a content provider for dictionary data for a given locale, and return
- * the addresses of a file set the members of which are suitable to be mmap'ed. It will copy
- * them to local storage if needed.
- * It should also check the dictionary versions to avoid unnecessary copies but this is
- * still in TODO state.
- * This will make the data from the content provider the cached dictionary for this locale,
- * overwriting any previous cached data.
- * @returns the addresses of the files, or null if no data could be obtained.
- * @throw FileNotFoundException if the provider returns non-existent data.
- * @throw IOException if the provider-returned data could not be read.
+ * Helper method to encapsulate exception handling.
*/
- public static List<AssetFileAddress> getDictSetFromContentProvider(Locale locale,
- Context context) throws FileNotFoundException, IOException {
- // TODO: check whether the dictionary is the same or not and if it is, return the cached
- // file.
- // TODO: This should be able to read a number of files from the dictionary pack, copy
- // them all and return them.
- final ContentResolver resolver = context.getContentResolver();
- final Uri dictionaryPackUri = getProviderUri(locale);
- final AssetFileDescriptor afd = resolver.openAssetFileDescriptor(dictionaryPackUri, "r");
- if (null == afd) return null;
- final String fileName =
- copyFileTo(afd.createInputStream(), getCacheFileNameForLocale(locale, context));
- return Arrays.asList(AssetFileAddress.makeFromFileName(fileName));
+ private static AssetFileDescriptor openAssetFileDescriptor(final ContentResolver resolver,
+ final Uri uri) {
+ try {
+ return resolver.openAssetFileDescriptor(uri, "r");
+ } catch (FileNotFoundException e) {
+ // I don't want to log the word list URI here for security concerns
+ Log.e(TAG, "Could not find a word list from the dictionary provider.");
+ return null;
+ }
}
/**
- * Accepts a file as dictionary data for some locale and returns the name of a file.
- *
- * This will make the data in the input file the cached dictionary for this locale, overwriting
- * any previous cached data.
+ * Caches a word list the id of which is passed as an argument. This will write the file
+ * to the cache file name designated by its id and locale, overwriting it if already present
+ * and creating it (and its containing directory) if necessary.
*/
- public static String getDictionaryFileFromFile(String fileName, Locale locale,
- Context context) throws FileNotFoundException, IOException {
- return copyFileTo(new FileInputStream(fileName), getCacheFileNameForLocale(locale,
- context));
+ private static AssetFileAddress cacheWordList(final String id, final String locale,
+ final ContentResolver resolver, final Context context) {
+
+ final int COMPRESSED_CRYPTED_COMPRESSED = 0;
+ final int CRYPTED_COMPRESSED = 1;
+ final int COMPRESSED_CRYPTED = 2;
+ final int COMPRESSED_ONLY = 3;
+ final int CRYPTED_ONLY = 4;
+ final int NONE = 5;
+ final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED;
+ final int MODE_MAX = NONE;
+
+ final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id);
+ final String outputFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
+
+ for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
+ InputStream originalSourceStream = null;
+ InputStream inputStream = null;
+ File outputFile = null;
+ FileOutputStream outputStream = null;
+ AssetFileDescriptor afd = null;
+ final Uri wordListUri = wordListUriBuilder.build();
+ try {
+ // Open input.
+ afd = openAssetFileDescriptor(resolver, wordListUri);
+ // If we can't open it at all, don't even try a number of times.
+ if (null == afd) return null;
+ originalSourceStream = afd.createInputStream();
+ // Open output.
+ outputFile = new File(outputFileName);
+ outputStream = new FileOutputStream(outputFile);
+ // Get the appropriate decryption method for this try
+ switch (mode) {
+ case COMPRESSED_CRYPTED_COMPRESSED:
+ inputStream = FileTransforms.getUncompressedStream(
+ FileTransforms.getDecryptedStream(
+ FileTransforms.getUncompressedStream(
+ originalSourceStream)));
+ break;
+ case CRYPTED_COMPRESSED:
+ inputStream = FileTransforms.getUncompressedStream(
+ FileTransforms.getDecryptedStream(originalSourceStream));
+ break;
+ case COMPRESSED_CRYPTED:
+ inputStream = FileTransforms.getDecryptedStream(
+ FileTransforms.getUncompressedStream(originalSourceStream));
+ break;
+ case COMPRESSED_ONLY:
+ inputStream = FileTransforms.getUncompressedStream(originalSourceStream);
+ break;
+ case CRYPTED_ONLY:
+ inputStream = FileTransforms.getDecryptedStream(originalSourceStream);
+ break;
+ case NONE:
+ inputStream = originalSourceStream;
+ break;
+ }
+ checkMagicAndCopyFileTo(new BufferedInputStream(inputStream), outputStream);
+ wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
+ QUERY_PARAMETER_SUCCESS);
+ if (0 >= resolver.delete(wordListUriBuilder.build(), null, null)) {
+ Log.e(TAG, "Could not have the dictionary pack delete a word list");
+ }
+ BinaryDictionaryGetter.removeFilesWithIdExcept(context, id, outputFile);
+ // Success! Close files (through the finally{} clause) and return.
+ return AssetFileAddress.makeFromFileName(outputFileName);
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.i(TAG, "Can't open word list in mode " + mode + " : " + e);
+ }
+ if (null != outputFile) {
+ // This may or may not fail. The file may not have been created if the
+ // exception was thrown before it could be. Hence, both failure and
+ // success are expected outcomes, so we don't check the return value.
+ outputFile.delete();
+ }
+ // Try the next method.
+ } finally {
+ // Ignore exceptions while closing files.
+ try {
+ // inputStream.close() will close afd, we should not call afd.close().
+ if (null != inputStream) inputStream.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while closing a cross-process file descriptor : " + e);
+ }
+ try {
+ if (null != outputStream) outputStream.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while closing a file : " + e);
+ }
+ }
+ }
+
+ // We could not copy the file at all. This is very unexpected.
+ // I'd rather not print the word list ID to the log out of security concerns
+ Log.e(TAG, "Could not copy a word list. Will not be able to use it.");
+ // If we can't copy it we should warn the dictionary provider so that it can mark it
+ // as invalid.
+ wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
+ QUERY_PARAMETER_FAILURE);
+ if (0 >= resolver.delete(wordListUriBuilder.build(), null, null)) {
+ Log.e(TAG, "In addition, we were unable to delete it.");
+ }
+ return null;
}
/**
- * Accepts a resource number as dictionary data for some locale and returns the name of a file.
+ * Queries a content provider for word list data for some locale and cache the returned files
*
- * This will make the resource the cached dictionary for this locale, overwriting any previous
- * cached data.
+ * This will query a content provider for word list data for a given locale, and copy the
+ * files locally so that they can be mmap'ed. This may overwrite previously cached word lists
+ * with newer versions if a newer version is made available by the content provider.
+ * @returns the addresses of the word list files, or null if no data could be obtained.
+ * @throw FileNotFoundException if the provider returns non-existent data.
+ * @throw IOException if the provider-returned data could not be read.
*/
- public static String getDictionaryFileFromResource(int resource, Locale locale,
- Context context) throws FileNotFoundException, IOException {
- return copyFileTo(context.getResources().openRawResource(resource),
- getCacheFileNameForLocale(locale, context));
+ public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale,
+ final Context context, final boolean hasDefaultWordList) {
+ final ContentResolver resolver = context.getContentResolver();
+ final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
+ hasDefaultWordList);
+ final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
+ for (WordListInfo id : idList) {
+ final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context);
+ if (null != afd) {
+ fileAddressList.add(afd);
+ }
+ }
+ return fileAddressList;
}
/**
- * Copies the data in an input stream to a target file, creating the file if necessary and
- * overwriting it if it already exists.
+ * Copies the data in an input stream to a target file if the magic number matches.
+ *
+ * If the magic number does not match the expected value, this method throws an
+ * IOException. Other usual conditions for IOException or FileNotFoundException
+ * also apply.
+ *
* @param input the stream to be copied.
- * @param outputFileName the name of a file to copy the data to. It is created if necessary.
+ * @param output an output stream to copy the data to.
*/
- private static String copyFileTo(final InputStream input, final String outputFileName)
- throws FileNotFoundException, IOException {
+ private static void checkMagicAndCopyFileTo(final BufferedInputStream input,
+ final FileOutputStream output) throws FileNotFoundException, IOException {
+ // Check the magic number
+ final int length = MAGIC_NUMBER_VERSION_2.length;
+ final byte[] magicNumberBuffer = new byte[length];
+ final int readMagicNumberSize = input.read(magicNumberBuffer, 0, length);
+ if (readMagicNumberSize < length) {
+ throw new IOException("Less bytes to read than the magic number length");
+ }
+ if (!Arrays.equals(MAGIC_NUMBER_VERSION_2, magicNumberBuffer)) {
+ if (!Arrays.equals(MAGIC_NUMBER_VERSION_1, magicNumberBuffer)) {
+ throw new IOException("Wrong magic number for downloaded file");
+ }
+ }
+ output.write(magicNumberBuffer);
+
+ // Actually copy the file
final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE];
- final FileOutputStream output = new FileOutputStream(outputFileName);
for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer))
output.write(buffer, 0, readBytes);
input.close();
- return outputFileName;
}
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 7ce92920d..063243e1b 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -17,13 +17,14 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.util.Log;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Locale;
/**
@@ -36,13 +37,127 @@ class BinaryDictionaryGetter {
*/
private static final String TAG = BinaryDictionaryGetter.class.getSimpleName();
+ /**
+ * Used to return empty lists
+ */
+ private static final File[] EMPTY_FILE_ARRAY = new File[0];
+
+ /**
+ * Name of the common preferences name to know which word list are on and which are off.
+ */
+ private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
+
+ // Name of the category for the main dictionary
+ private static final String MAIN_DICTIONARY_CATEGORY = "main";
+ public static final String ID_CATEGORY_SEPARATOR = ":";
+
// Prevents this from being instantiated
private BinaryDictionaryGetter() {}
/**
+ * Returns whether we may want to use this character as part of a file name.
+ *
+ * This basically only accepts ascii letters and numbers, and rejects everything else.
+ */
+ private static boolean isFileNameCharacter(int codePoint) {
+ if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
+ if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
+ if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
+ return codePoint == '_'; // Underscore
+ }
+
+ /**
+ * Escapes a string for any characters that may be suspicious for a file or directory name.
+ *
+ * Concretely this does a sort of URL-encoding except it will encode everything that's not
+ * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+ * we cannot allow here)
+ */
+ // TODO: create a unit test for this method
+ private static String replaceFileNameDangerousCharacters(final String name) {
+ // This assumes '%' is fully available as a non-separator, normal
+ // character in a file name. This is probably true for all file systems.
+ final StringBuilder sb = new StringBuilder();
+ final int nameLength = name.length();
+ for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
+ final int codePoint = name.codePointAt(i);
+ if (isFileNameCharacter(codePoint)) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ // 6 digits - unicode is limited to 21 bits
+ sb.append(String.format((Locale)null, "%%%1$06x", codePoint));
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Reverse escaping done by replaceFileNameDangerousCharacters.
+ */
+ private static String getWordListIdFromFileName(final String fname) {
+ final StringBuilder sb = new StringBuilder();
+ final int fnameLength = fname.length();
+ for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
+ final int codePoint = fname.codePointAt(i);
+ if ('%' != codePoint) {
+ sb.appendCodePoint(codePoint);
+ } else {
+ final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16);
+ i += 6;
+ sb.appendCodePoint(encodedCodePoint);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method to get the top level cache directory.
+ */
+ private static String getWordListCacheDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "dicts";
+ }
+
+ /**
+ * Find out the cache directory associated with a specific locale.
+ */
+ private static String getCacheDirectoryForLocale(final String locale, final Context context) {
+ final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
+ final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ + relativeDirectoryName;
+ final File directory = new File(absoluteDirectoryName);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the directory for locale" + locale);
+ }
+ }
+ return absoluteDirectoryName;
+ }
+
+ /**
+ * Generates a file name for the id and locale passed as an argument.
+ *
+ * In the current implementation the file name returned will always be unique for
+ * any id/locale pair, but please do not expect that the id can be the same for
+ * different dictionaries with different locales. An id should be unique for any
+ * dictionary.
+ * The file name is pretty much an URL-encoded version of the id inside a directory
+ * named like the locale, except it will also escape characters that look dangerous
+ * to some file systems.
+ * @param id the id of the dictionary for which to get a file name
+ * @param locale the locale for which to get the file name as a string
+ * @param context the context to use for getting the directory
+ * @return the name of the file to be created
+ */
+ public static String getCacheFileName(String id, String locale, Context context) {
+ final String fileName = replaceFileNameDangerousCharacters(id);
+ return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
+ }
+
+ /**
* Returns a file address from a resource, or null if it cannot be opened.
*/
- private static AssetFileAddress loadFallbackResource(Context context, int fallbackResId) {
+ private static AssetFileAddress loadFallbackResource(final Context context,
+ final int fallbackResId) {
final AssetFileDescriptor afd = context.getResources().openRawResourceFd(fallbackResId);
if (afd == null) {
Log.e(TAG, "Found the resource but cannot read it. Is it compressed? resId="
@@ -53,45 +168,224 @@ class BinaryDictionaryGetter {
context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
}
+ static private class DictPackSettings {
+ final SharedPreferences mDictPreferences;
+ public DictPackSettings(final Context context) {
+ Context dictPackContext = null;
+ try {
+ final String dictPackName =
+ context.getString(R.string.dictionary_pack_package_name);
+ dictPackContext = context.createPackageContext(dictPackName, 0);
+ } catch (NameNotFoundException e) {
+ // The dictionary pack is not installed...
+ // TODO: fallback on the built-in dict, see the TODO above
+ Log.e(TAG, "Could not find a dictionary pack");
+ }
+ mDictPreferences = null == dictPackContext ? null
+ : dictPackContext.getSharedPreferences(COMMON_PREFERENCES_NAME,
+ Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS);
+ }
+ public boolean isWordListActive(final String dictId) {
+ if (null == mDictPreferences) {
+ // If we don't have preferences it basically means we can't find the dictionary
+ // pack - either it's not installed, or it's disabled, or there is some strange
+ // bug. Either way, a word list with no settings should be on by default: default
+ // dictionaries in LatinIME are on if there is no settings at all, and if for some
+ // reason some dictionaries have been installed BUT the dictionary pack can't be
+ // found anymore it's safer to actually supply installed dictionaries.
+ return true;
+ } else {
+ // The default is true here for the same reasons as above. We got the dictionary
+ // pack but if we don't have any settings for it it means the user has never been
+ // to the settings yet. So by default, the main dictionaries should be on.
+ return mDictPreferences.getBoolean(dictId, true);
+ }
+ }
+ }
+
+ /**
+ * Helper method to the list of cache directories, one for each distinct locale.
+ */
+ private static File[] getCachedDirectoryList(final Context context) {
+ return new File(getWordListCacheDirectory(context)).listFiles();
+ }
+
+ /**
+ * Returns the category for a given file name.
+ *
+ * This parses the file name, extracts the category, and returns it. See
+ * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
+ * @return The category as a string or null if it can't be found in the file name.
+ */
+ private static String getCategoryFromFileName(final String fileName) {
+ final String id = getWordListIdFromFileName(fileName);
+ final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
+ if (2 != idArray.length) return null;
+ return idArray[0];
+ }
+
+ /**
+ * Utility class for the {@link #getCachedWordLists} method
+ */
+ private static class FileAndMatchLevel {
+ final File mFile;
+ final int mMatchLevel;
+ public FileAndMatchLevel(final File file, final int matchLevel) {
+ mFile = file;
+ mMatchLevel = matchLevel;
+ }
+ }
+
+ /**
+ * Returns the list of cached files for a specific locale, one for each category.
+ *
+ * This will return exactly one file for each word list category that matches
+ * the passed locale. If several files match the locale for any given category,
+ * this returns the file with the closest match to the locale. For example, if
+ * the passed word list is en_US, and for a category we have an en and an en_US
+ * word list available, we'll return only the en_US one.
+ * Thus, the list will contain as many files as there are categories.
+ *
+ * @param locale the locale to find the dictionary files for, as a string.
+ * @param context the context on which to open the files upon.
+ * @return an array of binary dictionary files, which may be empty but may not be null.
+ */
+ private static File[] getCachedWordLists(final String locale,
+ final Context context) {
+ final File[] directoryList = getCachedDirectoryList(context);
+ if (null == directoryList) return EMPTY_FILE_ARRAY;
+ final HashMap<String, FileAndMatchLevel> cacheFiles =
+ new HashMap<String, FileAndMatchLevel>();
+ for (File directory : directoryList) {
+ if (!directory.isDirectory()) continue;
+ final String dirLocale = getWordListIdFromFileName(directory.getName());
+ final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale);
+ if (LocaleUtils.isMatch(matchLevel)) {
+ final File[] wordLists = directory.listFiles();
+ if (null != wordLists) {
+ for (File wordList : wordLists) {
+ final String category = getCategoryFromFileName(wordList.getName());
+ final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
+ if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) {
+ cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel));
+ }
+ }
+ }
+ }
+ }
+ if (cacheFiles.isEmpty()) return EMPTY_FILE_ARRAY;
+ final File[] result = new File[cacheFiles.size()];
+ int index = 0;
+ for (final FileAndMatchLevel entry : cacheFiles.values()) {
+ result[index++] = entry.mFile;
+ }
+ return result;
+ }
+
+ /**
+ * Remove all files with the passed id, except the passed file.
+ *
+ * If a dictionary with a given ID has a metadata change that causes it to change
+ * path, we need to remove the old version. The only way to do this is to check all
+ * installed files for a matching ID in a different directory.
+ */
+ public static void removeFilesWithIdExcept(final Context context, final String id,
+ final File fileToKeep) {
+ try {
+ final File canonicalFileToKeep = fileToKeep.getCanonicalFile();
+ final File[] directoryList = getCachedDirectoryList(context);
+ if (null == directoryList) return;
+ for (File directory : directoryList) {
+ // There is one directory per locale. See #getCachedDirectoryList
+ if (!directory.isDirectory()) continue;
+ final File[] wordLists = directory.listFiles();
+ if (null == wordLists) continue;
+ for (File wordList : wordLists) {
+ final String fileId = getWordListIdFromFileName(wordList.getName());
+ if (fileId.equals(id)) {
+ if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) {
+ wordList.delete();
+ }
+ }
+ }
+ }
+ } catch (java.io.IOException e) {
+ Log.e(TAG, "IOException trying to cleanup files : " + e);
+ }
+ }
+
+
+ /**
+ * Returns the id associated with the main word list for a specified locale.
+ *
+ * Word lists stored in Android Keyboard's resources are referred to as the "main"
+ * word lists. Since they can be updated like any other list, we need to assign a
+ * unique ID to them. This ID is just the name of the language (locale-wise) they
+ * are for, and this method returns this ID.
+ */
+ private static String getMainDictId(final Locale locale) {
+ // This works because we don't include by default different dictionaries for
+ // different countries. This actually needs to return the id that we would
+ // like to use for word lists included in resources, and the following is okay.
+ return MAIN_DICTIONARY_CATEGORY + ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
+ }
+
+ private static boolean isMainWordListId(final String id) {
+ final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
+ if (2 != idArray.length) return false;
+ return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
+ }
+
/**
* Returns a list of file addresses for a given locale, trying relevant methods in order.
*
* Tries to get binary dictionaries from various sources, in order:
- * - Uses a private method of getting a private dictionaries, as implemented by the
- * PrivateBinaryDictionaryGetter class.
- * If that fails:
* - Uses a content provider to get a public dictionary set, as per the protocol described
* in BinaryDictionaryFileDumper.
* If that fails:
- * - Gets a file name from the fallback resource passed as an argument.
+ * - Gets a file name from the built-in dictionary for this locale, if any.
* If that fails:
* - Returns null.
- * @return The address of a valid file, or null.
- */
- public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context,
- int fallbackResId) {
- // Try first to query a private package signed the same way for private files.
- final List<AssetFileAddress> privateFiles =
- PrivateBinaryDictionaryGetter.getDictionaryFiles(locale, context);
- if (null != privateFiles) {
- return privateFiles;
- } else {
- try {
- // If that was no-go, try to find a publicly exported dictionary.
- List<AssetFileAddress> listFromContentProvider =
- BinaryDictionaryFileDumper.getDictSetFromContentProvider(locale, context);
- if (null != listFromContentProvider) {
- return listFromContentProvider;
- }
- // If the list is null, fall through and return the fallback
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Unable to create dictionary file from provider for locale "
- + locale.toString() + ": falling back to internal dictionary");
- } catch (IOException e) {
- Log.e(TAG, "Unable to read source data for locale "
- + locale.toString() + ": falling back to internal dictionary");
+ * @return The list of addresses of valid dictionary files, or null.
+ */
+ public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
+ final Context context) {
+
+ final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale);
+ // cacheWordListsFromContentProvider returns the list of files it copied to local
+ // storage, but we don't really care about what was copied NOW: what we want is the
+ // list of everything we ever cached, so we ignore the return value.
+ BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context,
+ hasDefaultWordList);
+ final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
+ final String mainDictId = getMainDictId(locale);
+ final DictPackSettings dictPackSettings = new DictPackSettings(context);
+
+ boolean foundMainDict = false;
+ final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+ // cachedWordLists may not be null, see doc for getCachedDictionaryList
+ for (final File f : cachedWordLists) {
+ final String wordListId = getWordListIdFromFileName(f.getName());
+ if (isMainWordListId(wordListId)) {
+ foundMainDict = true;
+ }
+ if (!dictPackSettings.isWordListActive(wordListId)) continue;
+ if (f.canRead()) {
+ fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
+ } else {
+ Log.e(TAG, "Found a cached dictionary file but cannot read it");
+ }
+ }
+
+ if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
+ final int fallbackResId =
+ DictionaryFactory.getMainDictionaryResourceId(context.getResources(), locale);
+ final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
+ if (null != fallbackAsset) {
+ fileList.add(fallbackAsset);
}
- return Arrays.asList(loadFallbackResource(context, fallbackResId));
}
+
+ return fileList;
}
}
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
deleted file mode 100644
index a5bfea0f8..000000000
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ /dev/null
@@ -1,633 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.Typeface;
-import android.os.Message;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.style.BackgroundColorSpan;
-import android.text.style.CharacterStyle;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.UnderlineSpan;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.PopupWindow;
-import android.widget.TextView;
-
-import com.android.inputmethod.compat.FrameLayoutCompatUtils;
-import com.android.inputmethod.compat.LinearLayoutCompatUtils;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener {
-
- public interface Listener {
- public boolean addWordToDictionary(String word);
- public void pickSuggestionManually(int index, CharSequence word);
- }
-
- private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
- // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
- private static final int MAX_SUGGESTIONS = 18;
- private static final int MATCH_PARENT = MeasureSpec.makeMeasureSpec(
- -1, MeasureSpec.UNSPECIFIED);
-
- private static final boolean DBG = LatinImeLogger.sDBG;
-
- private final View mCandidatesStrip;
- private static final int NUM_CANDIDATES_IN_STRIP = 3;
- private final ImageView mExpandCandidatesPane;
- private final ImageView mCloseCandidatesPane;
- private ViewGroup mCandidatesPane;
- private ViewGroup mCandidatesPaneContainer;
- private View mKeyboardView;
- private final ArrayList<TextView> mWords = new ArrayList<TextView>();
- private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
- private final ArrayList<View> mDividers = new ArrayList<View>();
- private final int mCandidateStripHeight;
- private final CharacterStyle mInvertedForegroundColorSpan;
- private final CharacterStyle mInvertedBackgroundColorSpan;
- private final int mAutoCorrectHighlight;
- private static final int AUTO_CORRECT_BOLD = 0x01;
- private static final int AUTO_CORRECT_UNDERLINE = 0x02;
- private static final int AUTO_CORRECT_INVERT = 0x04;
- private final int mColorTypedWord;
- private final int mColorAutoCorrect;
- private final int mColorSuggestedCandidate;
- private final PopupWindow mPreviewPopup;
- private final TextView mPreviewText;
-
- private final View mTouchToSave;
- private final TextView mWordToSave;
-
- private Listener mListener;
- private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
- private boolean mShowingAutoCorrectionInverted;
- private boolean mShowingAddToDictionary;
-
- private static final float MIN_TEXT_XSCALE = 0.4f;
- private static final String ELLIPSIS = "\u2026";
-
- private final UiHandler mHandler = new UiHandler(this);
-
- private static class UiHandler extends StaticInnerHandlerWrapper<CandidateView> {
- private static final int MSG_HIDE_PREVIEW = 0;
- private static final int MSG_UPDATE_SUGGESTION = 1;
-
- private static final long DELAY_HIDE_PREVIEW = 1000;
- private static final long DELAY_UPDATE_SUGGESTION = 300;
-
- public UiHandler(CandidateView outerInstance) {
- super(outerInstance);
- }
-
- @Override
- public void dispatchMessage(Message msg) {
- final CandidateView candidateView = getOuterInstance();
- switch (msg.what) {
- case MSG_HIDE_PREVIEW:
- candidateView.hidePreview();
- break;
- case MSG_UPDATE_SUGGESTION:
- candidateView.updateSuggestions();
- break;
- }
- }
-
- public void postHidePreview() {
- cancelHidePreview();
- sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW);
- }
-
- public void cancelHidePreview() {
- removeMessages(MSG_HIDE_PREVIEW);
- }
-
- public void postUpdateSuggestions() {
- cancelUpdateSuggestions();
- sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION),
- DELAY_UPDATE_SUGGESTION);
- }
-
- public void cancelUpdateSuggestions() {
- removeMessages(MSG_UPDATE_SUGGESTION);
- }
-
- public void cancelAllMessages() {
- cancelHidePreview();
- cancelUpdateSuggestions();
- }
- }
-
- /**
- * Construct a CandidateView for showing suggested words for completion.
- * @param context
- * @param attrs
- */
- public CandidateView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.candidateViewStyle);
- }
-
- public CandidateView(Context context, AttributeSet attrs, int defStyle) {
- // Note: Up to version 10 (Gingerbread) of the API, LinearLayout doesn't have 3-argument
- // constructor.
- // TODO: Call 3-argument constructor, super(context, attrs, defStyle), when we abandon
- // backward compatibility with the version 10 or earlier of the API.
- super(context, attrs);
- if (defStyle != R.attr.candidateViewStyle) {
- throw new IllegalArgumentException(
- "can't accept defStyle other than R.attr.candidayeViewStyle: defStyle="
- + defStyle);
- }
- setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable(
- context, attrs, defStyle, R.style.CandidateViewStyle));
-
- Resources res = context.getResources();
- LayoutInflater inflater = LayoutInflater.from(context);
- inflater.inflate(R.layout.candidates_strip, this);
-
- mPreviewPopup = new PopupWindow(context);
- mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null);
- mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- mPreviewPopup.setContentView(mPreviewText);
- mPreviewPopup.setBackgroundDrawable(null);
-
- mCandidatesStrip = findViewById(R.id.candidates_strip);
- mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height);
- for (int i = 0; i < MAX_SUGGESTIONS; i++) {
- final TextView word, info;
- switch (i) {
- case 0:
- word = (TextView)findViewById(R.id.word_left);
- info = (TextView)findViewById(R.id.info_left);
- break;
- case 1:
- word = (TextView)findViewById(R.id.word_center);
- info = (TextView)findViewById(R.id.info_center);
- break;
- case 2:
- word = (TextView)findViewById(R.id.word_right);
- info = (TextView)findViewById(R.id.info_right);
- break;
- default:
- word = (TextView)inflater.inflate(R.layout.candidate_word, null);
- info = (TextView)inflater.inflate(R.layout.candidate_info, null);
- break;
- }
- word.setTag(i);
- word.setOnClickListener(this);
- if (i == 0)
- word.setOnLongClickListener(this);
- mWords.add(word);
- mInfos.add(info);
- if (i > 0) {
- final View divider = inflater.inflate(R.layout.candidate_divider, null);
- divider.measure(MATCH_PARENT, MATCH_PARENT);
- mDividers.add(divider);
- }
- }
-
- mTouchToSave = findViewById(R.id.touch_to_save);
- mWordToSave = (TextView)findViewById(R.id.word_to_save);
- mWordToSave.setOnClickListener(this);
-
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle);
- mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0);
- mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0);
- mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0);
- mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0);
- mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff);
- mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord);
-
- mExpandCandidatesPane = (ImageView)findViewById(R.id.expand_candidates_pane);
- mExpandCandidatesPane.setImageDrawable(
- a.getDrawable(R.styleable.CandidateView_iconExpandPane));
- mExpandCandidatesPane.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- expandCandidatesPane();
- }
- });
- mCloseCandidatesPane = (ImageView)findViewById(R.id.close_candidates_pane);
- mCloseCandidatesPane.setImageDrawable(
- a.getDrawable(R.styleable.CandidateView_iconClosePane));
- mCloseCandidatesPane.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- closeCandidatesPane();
- }
- });
-
- a.recycle();
- }
-
- /**
- * A connection back to the input method.
- * @param listener
- */
- public void setListener(Listener listener, View inputView) {
- mListener = listener;
- mKeyboardView = inputView.findViewById(R.id.keyboard_view);
- mCandidatesPane = FrameLayoutCompatUtils.getPlacer(
- (ViewGroup)inputView.findViewById(R.id.candidates_pane));
- mCandidatesPane.setOnClickListener(this);
- mCandidatesPaneContainer = (ViewGroup)inputView.findViewById(
- R.id.candidates_pane_container);
- }
-
- public void setSuggestions(SuggestedWords suggestions) {
- if (suggestions == null)
- return;
- mSuggestions = suggestions;
- if (mShowingAutoCorrectionInverted) {
- mHandler.postUpdateSuggestions();
- } else {
- updateSuggestions();
- }
- }
-
- private CharSequence getStyledCandidateWord(CharSequence word, TextView v,
- boolean isAutoCorrect) {
- v.setTypeface(Typeface.DEFAULT);
- if (!isAutoCorrect)
- return word;
- final Spannable spannedWord = new SpannableString(word);
- if ((mAutoCorrectHighlight & AUTO_CORRECT_BOLD) != 0)
- v.setTypeface(Typeface.DEFAULT_BOLD);
- if ((mAutoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0)
- spannedWord.setSpan(UNDERLINE_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
- return spannedWord;
- }
-
- private int getCandidateTextColor(boolean isAutoCorrect, boolean isSuggestedCandidate,
- SuggestedWordInfo info) {
- final int color;
- if (isAutoCorrect) {
- color = mColorAutoCorrect;
- } else if (isSuggestedCandidate) {
- color = mColorSuggestedCandidate;
- } else {
- color = mColorTypedWord;
- }
- if (info != null && info.isPreviousSuggestedWord()) {
- final int newAlpha = (int)(Color.alpha(color) * 0.5f);
- return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
- } else {
- return color;
- }
- }
-
- private void updateSuggestions() {
- final SuggestedWords suggestions = mSuggestions;
- final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList;
-
- clear();
- final int paneWidth = getWidth();
- final int dividerWidth = mDividers.get(0).getMeasuredWidth();
- final int dividerHeight = mDividers.get(0).getMeasuredHeight();
- int x = 0;
- int y = 0;
- int fromIndex = NUM_CANDIDATES_IN_STRIP;
- final int count = Math.min(mWords.size(), suggestions.size());
- closeCandidatesPane();
- mExpandCandidatesPane.setVisibility(count > NUM_CANDIDATES_IN_STRIP ? VISIBLE : GONE);
- for (int i = 0; i < count; i++) {
- final CharSequence suggestion = suggestions.getWord(i);
- if (suggestion == null) continue;
-
- final SuggestedWordInfo suggestionInfo = (suggestedWordInfoList != null)
- ? suggestedWordInfoList.get(i) : null;
- final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
- && ((i == 1 && !suggestions.mTypedWordValid)
- || (i == 0 && suggestions.mTypedWordValid));
- // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1
- // and there are multiple suggestions, such as the default punctuation list.
- // TODO: Need to revisit this logic with bigram suggestions
- final boolean isSuggestedCandidate = (i != 0);
- final boolean isPunctuationSuggestions = (suggestion.length() == 1 && count > 1);
-
- final TextView word = mWords.get(i);
- // TODO: Reorder candidates in strip as appropriate. The center candidate should hold
- // the word when space is typed (valid typed word or auto corrected word).
- word.setTextColor(getCandidateTextColor(isAutoCorrect,
- isSuggestedCandidate || isPunctuationSuggestions, suggestionInfo));
- final CharSequence text = getStyledCandidateWord(suggestion, word, isAutoCorrect);
- if (i < NUM_CANDIDATES_IN_STRIP) {
- final View parent = (View)word.getParent();
- final int width = parent.getWidth() - word.getPaddingLeft()
- - word.getPaddingRight();
- setTextWithAutoScaleAndEllipsis(text, width, word);
- } else {
- setTextWithAutoScaleAndEllipsis(text, paneWidth, word);
- }
-
- final TextView info;
- if (DBG && suggestionInfo != null
- && !TextUtils.isEmpty(suggestionInfo.getDebugString())) {
- info = mInfos.get(i);
- info.setText(suggestionInfo.getDebugString());
- info.setVisibility(View.VISIBLE);
- } else {
- info = null;
- }
-
- if (i < NUM_CANDIDATES_IN_STRIP) {
- if (info != null) {
- word.measure(MATCH_PARENT, MATCH_PARENT);
- info.measure(MATCH_PARENT, MATCH_PARENT);
- final int width = word.getMeasuredWidth();
- final int infoWidth = info.getMeasuredWidth();
- FrameLayoutCompatUtils.placeViewAt(
- info, width - infoWidth, 0, infoWidth, info.getMeasuredHeight());
- }
- } else {
- word.measure(MATCH_PARENT, MATCH_PARENT);
- final int width = word.getMeasuredWidth();
- final int height = word.getMeasuredHeight();
- // TODO: Handle overflow case.
- if (dividerWidth + x + width >= paneWidth) {
- centeringCandidates(fromIndex, i - 1, x, paneWidth);
- x = 0;
- y += mCandidateStripHeight;
- fromIndex = i;
- }
- if (x != 0) {
- final View divider = mDividers.get(i - NUM_CANDIDATES_IN_STRIP);
- mCandidatesPane.addView(divider);
- FrameLayoutCompatUtils.placeViewAt(
- divider, x, y + (mCandidateStripHeight - dividerHeight) / 2,
- dividerWidth, dividerHeight);
- x += dividerWidth;
- }
- mCandidatesPane.addView(word);
- FrameLayoutCompatUtils.placeViewAt(
- word, x, y + (mCandidateStripHeight - height) / 2, width, height);
- if (info != null) {
- mCandidatesPane.addView(info);
- info.measure(MATCH_PARENT, MATCH_PARENT);
- final int infoWidth = info.getMeasuredWidth();
- FrameLayoutCompatUtils.placeViewAt(
- info, x + width - infoWidth, y, infoWidth, info.getMeasuredHeight());
- }
- x += width;
- }
- }
- if (x != 0) {
- // Centering last candidates row.
- centeringCandidates(fromIndex, count - 1, x, paneWidth);
- }
- }
-
- private void centeringCandidates(int from, int to, int width, int paneWidth) {
- final ViewGroup pane = mCandidatesPane;
- final int fromIndex = pane.indexOfChild(mWords.get(from));
- final int toIndex;
- if (mInfos.get(to).getParent() != null) {
- toIndex = pane.indexOfChild(mInfos.get(to));
- } else {
- toIndex = pane.indexOfChild(mWords.get(to));
- }
- final int offset = (paneWidth - width) / 2;
- for (int index = fromIndex; index <= toIndex; index++) {
- offsetMargin(pane.getChildAt(index), offset, 0);
- }
- }
-
- private static void offsetMargin(View v, int dx, int dy) {
- if (v == null)
- return;
- final ViewGroup.LayoutParams lp = v.getLayoutParams();
- if (lp instanceof ViewGroup.MarginLayoutParams) {
- final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
- mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0);
- }
- }
-
- private static void setTextWithAutoScaleAndEllipsis(CharSequence text, int w, TextView v) {
- // To prevent partially rendered character at the end of text, subtract few extra pixels
- // from the width.
- final int width = w - 4;
-
- final TextPaint paint = v.getPaint();
- final int textWidth = getTextWidth(text, paint, 1.0f);
- if (textWidth < width || textWidth == 0 || width <= 0) {
- v.setTextScaleX(1.0f);
- v.setText(text);
- return;
- }
-
- final float scaleX = Math.min((float)width / textWidth, 1.0f);
- if (scaleX >= MIN_TEXT_XSCALE) {
- v.setTextScaleX(scaleX);
- v.setText(text);
- return;
- }
-
- final int truncatedWidth = width - getTextWidth(ELLIPSIS, paint, MIN_TEXT_XSCALE);
- final CharSequence ellipsized = getTextEllipsizedAtStart(text, paint, truncatedWidth);
- v.setTextScaleX(MIN_TEXT_XSCALE);
- v.setText(ELLIPSIS);
- v.append(ellipsized);
- }
-
- private static int getTextWidth(CharSequence text, TextPaint paint, float scaleX) {
- if (TextUtils.isEmpty(text)) return 0;
- final int len = text.length();
- final float[] widths = new float[len];
- paint.setTextScaleX(scaleX);
- final int count = paint.getTextWidths(text, 0, len, widths);
- float width = 0;
- for (int i = 0; i < count; i++) {
- width += widths[i];
- }
- return (int)Math.round(width + 0.5);
- }
-
- private static CharSequence getTextEllipsizedAtStart(CharSequence text, TextPaint paint,
- int maxWidth) {
- final int len = text.length();
- final float[] widths = new float[len];
- final int count = paint.getTextWidths(text, 0, len, widths);
- float width = 0;
- for (int i = count - 1; i >= 0; i--) {
- width += widths[i];
- if (width > maxWidth)
- return text.subSequence(i + 1, len);
- }
- return text;
- }
-
- private void expandCandidatesPane() {
- mExpandCandidatesPane.setVisibility(View.GONE);
- mCloseCandidatesPane.setVisibility(View.VISIBLE);
- mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight());
- mCandidatesPaneContainer.setVisibility(View.VISIBLE);
- mKeyboardView.setVisibility(View.GONE);
- }
-
- private void closeCandidatesPane() {
- mExpandCandidatesPane.setVisibility(View.VISIBLE);
- mCloseCandidatesPane.setVisibility(View.GONE);
- mCandidatesPaneContainer.setVisibility(View.GONE);
- mKeyboardView.setVisibility(View.VISIBLE);
- }
-
- public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
- if ((mAutoCorrectHighlight & AUTO_CORRECT_INVERT) == 0)
- return;
- final TextView tv = mWords.get(1);
- final Spannable word = new SpannableString(autoCorrectedWord);
- final int wordLength = word.length();
- word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength,
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
- word.setSpan(mInvertedForegroundColorSpan, 0, wordLength,
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
- tv.setText(word);
- mShowingAutoCorrectionInverted = true;
- }
-
- public boolean isShowingAddToDictionaryHint() {
- return mShowingAddToDictionary;
- }
-
- public void showAddToDictionaryHint(CharSequence word) {
- mWordToSave.setText(word);
- mShowingAddToDictionary = true;
- mCandidatesStrip.setVisibility(View.GONE);
- mTouchToSave.setVisibility(View.VISIBLE);
- }
-
- public boolean dismissAddToDictionaryHint() {
- if (!mShowingAddToDictionary) return false;
- clear();
- return true;
- }
-
- public SuggestedWords getSuggestions() {
- return mSuggestions;
- }
-
- public void clear() {
- mShowingAddToDictionary = false;
- mShowingAutoCorrectionInverted = false;
- for (int i = 0; i < NUM_CANDIDATES_IN_STRIP; i++) {
- mWords.get(i).setText(null);
- mInfos.get(i).setVisibility(View.GONE);
- }
- mTouchToSave.setVisibility(View.GONE);
- mCandidatesStrip.setVisibility(View.VISIBLE);
- mCandidatesPane.removeAllViews();
- }
-
- private void hidePreview() {
- mPreviewPopup.dismiss();
- }
-
- private void showPreview(int index, CharSequence word) {
- if (TextUtils.isEmpty(word))
- return;
-
- final TextView previewText = mPreviewText;
- previewText.setTextColor(mColorTypedWord);
- previewText.setText(word);
- previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- View v = mWords.get(index);
- final int[] offsetInWindow = new int[2];
- v.getLocationInWindow(offsetInWindow);
- final int posX = offsetInWindow[0];
- final int posY = offsetInWindow[1] - previewText.getMeasuredHeight();
- final PopupWindow previewPopup = mPreviewPopup;
- if (previewPopup.isShowing()) {
- previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight());
- } else {
- previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY);
- }
- previewText.setVisibility(VISIBLE);
- mHandler.postHidePreview();
- }
-
- private void addToDictionary(CharSequence word) {
- if (mListener.addWordToDictionary(word.toString())) {
- showPreview(0, getContext().getString(R.string.added_word, word));
- }
- }
-
- @Override
- public boolean onLongClick(View view) {
- final Object tag = view.getTag();
- if (!(tag instanceof Integer))
- return true;
- final int index = (Integer) tag;
- if (index >= mSuggestions.size())
- return true;
-
- final CharSequence word = mSuggestions.getWord(index);
- if (word.length() < 2)
- return false;
- addToDictionary(word);
- return true;
- }
-
- @Override
- public void onClick(View view) {
- if (view == mWordToSave) {
- addToDictionary(((TextView)view).getText());
- clear();
- return;
- }
-
- final Object tag = view.getTag();
- if (!(tag instanceof Integer))
- return;
- final int index = (Integer) tag;
- if (index >= mSuggestions.size())
- return;
-
- final CharSequence word = mSuggestions.getWord(index);
- mListener.pickSuggestionManually(index, word);
- // Because some punctuation letters are not treated as word separator depending on locale,
- // {@link #setSuggestions} might not be called and candidates pane left opened.
- closeCandidatesPane();
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mHandler.cancelAllMessages();
- hidePreview();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
new file mode 100644
index 000000000..e79db367c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -0,0 +1,127 @@
+/*
+ * 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;
+
+import android.view.inputmethod.EditorInfo;
+
+public final class Constants {
+ public static final class ImeOption {
+ /**
+ * The private IME option used to indicate that no microphone should be shown for a given
+ * text field. For instance, this is specified by the search dialog when the dialog is
+ * already showing a voice search button.
+ *
+ * @deprecated Use {@link ImeOption#NO_MICROPHONE} with package name prefixed.
+ */
+ @SuppressWarnings("dep-ann")
+ public static final String NO_MICROPHONE_COMPAT = "nm";
+
+ /**
+ * The private IME option used to indicate that no microphone should be shown for a given
+ * text field. For instance, this is specified by the search dialog when the dialog is
+ * already showing a voice search button.
+ */
+ public static final String NO_MICROPHONE = "noMicrophoneKey";
+
+ /**
+ * The private IME option used to indicate that no settings key should be shown for a given
+ * text field.
+ */
+ public static final String NO_SETTINGS_KEY = "noSettingsKey";
+
+ /**
+ * The private IME option used to indicate that the given text field needs ASCII code points
+ * input.
+ *
+ * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}.
+ */
+ @SuppressWarnings("dep-ann")
+ public static final String FORCE_ASCII = "forceAscii";
+
+ private ImeOption() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ public static final class Subtype {
+ /**
+ * The subtype mode used to indicate that the subtype is a keyboard.
+ */
+ public static final String KEYBOARD_MODE = "keyboard";
+
+ public static final class ExtraValue {
+ /**
+ * The subtype extra value used to indicate that the subtype keyboard layout is capable
+ * for typing ASCII characters.
+ */
+ public static final String ASCII_CAPABLE = "AsciiCapable";
+
+ /**
+ * The subtype extra value used to indicate that the subtype require network connection
+ * to work.
+ */
+ public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity";
+
+ /**
+ * The subtype extra value used to indicate that the subtype display name contains "%s"
+ * for replacement mark and it should be replaced by this extra value.
+ * This extra value is supported on JellyBean and later.
+ */
+ public static final String UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME =
+ "UntranslatableReplacementStringInSubtypeName";
+
+ /**
+ * The subtype extra value used to indicate that the subtype keyboard layout set name.
+ * This extra value is private to LatinIME.
+ */
+ public static final String KEYBOARD_LAYOUT_SET = "KeyboardLayoutSet";
+
+ /**
+ * The subtype extra value used to indicate that the subtype is additional subtype
+ * that the user defined. This extra value is private to LatinIME.
+ */
+ public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype";
+
+ private ExtraValue() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ private Subtype() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ public static class TextUtils {
+ /**
+ * Capitalization mode for {@link android.text.TextUtils#getCapsMode}: don't capitalize
+ * characters. This value may be used with
+ * {@link android.text.TextUtils#CAP_MODE_CHARACTERS},
+ * {@link android.text.TextUtils#CAP_MODE_WORDS}, and
+ * {@link android.text.TextUtils#CAP_MODE_SENTENCES}.
+ */
+ public static final int CAP_MODE_OFF = 0;
+
+ private TextUtils() {
+ // This utility class is not publicly instantiable.
+ }
+ }
+
+ private Constants() {
+ // This utility class is not publicly instantiable.
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
new file mode 100644
index 000000000..10e511eaf
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -0,0 +1,293 @@
+/*
+ * 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;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.SystemClock;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Contacts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.Keyboard;
+
+import java.util.Locale;
+
+public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
+
+ private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME,};
+ private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID};
+
+ private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
+ private static final String NAME = "contacts";
+
+ private static boolean DEBUG = false;
+
+ /**
+ * Frequency for contacts information into the dictionary
+ */
+ private static final int FREQUENCY_FOR_CONTACTS = 40;
+ private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
+
+ /** The maximum number of contacts that this dictionary supports. */
+ private static final int MAX_CONTACT_COUNT = 10000;
+
+ private static final int INDEX_NAME = 1;
+
+ /** The number of contacts in the most recent dictionary rebuild. */
+ static private int sContactCountAtLastRebuild = 0;
+
+ /** The locale for this contacts dictionary. Controls name bigram predictions. */
+ public final Locale mLocale;
+
+ private ContentObserver mObserver;
+
+ /**
+ * Whether to use "firstname lastname" in bigram predictions.
+ */
+ private final boolean mUseFirstLastBigrams;
+
+ public ContactsBinaryDictionary(final Context context, final int dicTypeId, Locale locale) {
+ super(context, getFilenameWithLocale(NAME, locale.toString()), dicTypeId);
+ mLocale = locale;
+ mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
+ registerObserver(context);
+
+ // Load the current binary dictionary from internal storage. If no binary dictionary exists,
+ // loadDictionary will start a new thread to generate one asynchronously.
+ loadDictionary();
+ }
+
+ private synchronized void registerObserver(final Context context) {
+ // Perform a managed query. The Activity will handle closing and requerying the cursor
+ // when needed.
+ if (mObserver != null) return;
+ ContentResolver cres = context.getContentResolver();
+ cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver =
+ new ContentObserver(null) {
+ @Override
+ public void onChange(boolean self) {
+ setRequiresReload(true);
+ }
+ });
+ }
+
+ public void reopen(final Context context) {
+ registerObserver(context);
+ }
+
+ @Override
+ public synchronized void close() {
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ super.close();
+ }
+
+ @Override
+ public void loadDictionaryAsync() {
+ try {
+ Cursor cursor = mContext.getContentResolver()
+ .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ sContactCountAtLastRebuild = getContactCount();
+ addWords(cursor);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Contacts DB is having problems");
+ }
+ }
+
+ @Override
+ public void getBigrams(final WordComposer codes, final CharSequence previousWord,
+ final WordCallback callback) {
+ super.getBigrams(codes, previousWord, callback);
+ }
+
+ private boolean useFirstLastBigramsForLocale(Locale locale) {
+ // TODO: Add firstname/lastname bigram rules for other languages.
+ if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
+ return true;
+ }
+ return false;
+ }
+
+ private void addWords(Cursor cursor) {
+ clearFusionDictionary();
+ int count = 0;
+ while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
+ String name = cursor.getString(INDEX_NAME);
+ if (isValidName(name)) {
+ addName(name);
+ ++count;
+ }
+ cursor.moveToNext();
+ }
+ }
+
+ private int getContactCount() {
+ // TODO: consider switching to a rawQuery("select count(*)...") on the database if
+ // performance is a bottleneck.
+ final Cursor cursor = mContext.getContentResolver().query(
+ Contacts.CONTENT_URI, PROJECTION_ID_ONLY, null, null, null);
+ if (cursor != null) {
+ try {
+ return cursor.getCount();
+ } finally {
+ cursor.close();
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their
+ * bigrams depending on locale.
+ */
+ private void addName(String name) {
+ int len = name.codePointCount(0, name.length());
+ String prevWord = null;
+ // TODO: Better tokenization for non-Latin writing systems
+ for (int i = 0; i < len; i++) {
+ if (Character.isLetter(name.codePointAt(i))) {
+ int end = getWordEndPosition(name, len, i);
+ String word = name.substring(i, end);
+ i = end - 1;
+ // Don't add single letter words, possibly confuses
+ // capitalization of i.
+ final int wordLen = word.codePointCount(0, word.length());
+ if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
+ super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
+ if (!TextUtils.isEmpty(prevWord)) {
+ if (mUseFirstLastBigrams) {
+ super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
+ }
+ }
+ prevWord = word;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the index of the last letter in the word, starting from position startIndex.
+ */
+ private static int getWordEndPosition(String string, int len, int startIndex) {
+ int end;
+ int cp = 0;
+ for (end = startIndex + 1; end < len; end += Character.charCount(cp)) {
+ cp = string.codePointAt(end);
+ if (!(cp == Keyboard.CODE_DASH || cp == Keyboard.CODE_SINGLE_QUOTE
+ || Character.isLetter(cp))) {
+ break;
+ }
+ }
+ return end;
+ }
+
+ @Override
+ protected boolean hasContentChanged() {
+ final long startTime = SystemClock.uptimeMillis();
+ final int contactCount = getContactCount();
+ if (contactCount > MAX_CONTACT_COUNT) {
+ // If there are too many contacts then return false. In this rare case it is impossible
+ // to include all of them anyways and the cost of rebuilding the dictionary is too high.
+ // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts?
+ return false;
+ }
+ if (contactCount != sContactCountAtLastRebuild) {
+ if (DEBUG) {
+ Log.d(TAG, "Contact count changed: " + sContactCountAtLastRebuild + " to "
+ + contactCount);
+ }
+ return true;
+ }
+ // Check all contacts since it's not possible to find out which names have changed.
+ // This is needed because it's possible to receive extraneous onChange events even when no
+ // name has changed.
+ Cursor cursor = mContext.getContentResolver().query(
+ Contacts.CONTENT_URI, PROJECTION, null, null, null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ while (!cursor.isAfterLast()) {
+ String name = cursor.getString(INDEX_NAME);
+ if (isValidName(name) && !isNameInDictionary(name)) {
+ if (DEBUG) {
+ Log.d(TAG, "Contact name missing: " + name + " (runtime = "
+ + (SystemClock.uptimeMillis() - startTime) + " ms)");
+ }
+ return true;
+ }
+ cursor.moveToNext();
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
+ + " ms)");
+ }
+ return false;
+ }
+
+ private static boolean isValidName(String name) {
+ if (name != null && -1 == name.indexOf('@')) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the words in a name are in the current binary dictionary.
+ */
+ private boolean isNameInDictionary(String name) {
+ int len = name.codePointCount(0, name.length());
+ String prevWord = null;
+ for (int i = 0; i < len; i++) {
+ if (Character.isLetter(name.codePointAt(i))) {
+ int end = getWordEndPosition(name, len, i);
+ String word = name.substring(i, end);
+ i = end - 1;
+ final int wordLen = word.codePointCount(0, word.length());
+ if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
+ if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
+ if (!super.isValidBigramLocked(prevWord, word)) {
+ return false;
+ }
+ } else {
+ if (!super.isValidWordLocked(word)) {
+ return false;
+ }
+ }
+ prevWord = word;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
deleted file mode 100644
index 66a041508..000000000
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-
-public class ContactsDictionary extends ExpandableDictionary {
-
- private static final String[] PROJECTION = {
- BaseColumns._ID,
- Contacts.DISPLAY_NAME,
- };
-
- private static final String TAG = "ContactsDictionary";
-
- /**
- * Frequency for contacts information into the dictionary
- */
- private static final int FREQUENCY_FOR_CONTACTS = 40;
- private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
- private static final int INDEX_NAME = 1;
-
- private ContentObserver mObserver;
-
- private long mLastLoadedContacts;
-
- public ContactsDictionary(Context context, int dicTypeId) {
- super(context, dicTypeId);
- // Perform a managed query. The Activity will handle closing and requerying the cursor
- // when needed.
- ContentResolver cres = context.getContentResolver();
-
- cres.registerContentObserver(
- Contacts.CONTENT_URI, true,mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- });
- loadDictionary();
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void startDictionaryLoadingTaskLocked() {
- long now = SystemClock.uptimeMillis();
- if (mLastLoadedContacts == 0
- || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
- super.startDictionaryLoadingTaskLocked();
- }
- }
-
- @Override
- public void loadDictionaryAsync() {
- try {
- Cursor cursor = getContext().getContentResolver()
- .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
- if (cursor != null) {
- addWords(cursor);
- }
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- mLastLoadedContacts = SystemClock.uptimeMillis();
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- // Do not return bigrams from Contacts when nothing was typed.
- if (codes.size() <= 0) return;
- super.getBigrams(codes, previousWord, callback);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
-
- final int maxWordLength = getMaxWordLength();
- try {
- if (cursor.moveToFirst()) {
- while (!cursor.isAfterLast()) {
- String name = cursor.getString(INDEX_NAME);
-
- if (name != null && -1 == name.indexOf('@')) {
- int len = name.length();
- String prevWord = null;
-
- // TODO: Better tokenization for non-Latin writing systems
- for (int i = 0; i < len; i++) {
- if (Character.isLetter(name.charAt(i))) {
- int j;
- for (j = i + 1; j < len; j++) {
- char c = name.charAt(j);
-
- if (!(c == Keyboard.CODE_DASH
- || c == Keyboard.CODE_SINGLE_QUOTE
- || Character.isLetter(c))) {
- break;
- }
- }
-
- String word = name.substring(i, j);
- i = j - 1;
-
- // Safeguard against adding really long words. Stack
- // may overflow due to recursion
- // Also don't add single letter words, possibly confuses
- // capitalization of i.
- final int wordLen = word.length();
- if (wordLen < maxWordLength && wordLen > 1) {
- super.addWord(word, FREQUENCY_FOR_CONTACTS);
- if (!TextUtils.isEmpty(prevWord)) {
- super.setBigram(prevWord, word,
- FREQUENCY_FOR_CONTACTS_BIGRAM);
- }
- prevWord = word;
- }
- }
- }
- }
- cursor.moveToNext();
- }
- }
- cursor.close();
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index fd62d61c3..af7649863 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -16,27 +16,30 @@
package com.android.inputmethod.latin;
+import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Process;
import android.preference.CheckBoxPreference;
-import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
import android.util.Log;
-public class DebugSettings extends PreferenceActivity
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+
+public class DebugSettings extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = "DebugSettings";
+ private static final String TAG = DebugSettings.class.getSimpleName();
private static final String DEBUG_MODE_KEY = "debug_mode";
+ public static final String FORCE_NON_DISTINCT_MULTITOUCH_KEY = "force_non_distinct_multitouch";
private boolean mServiceNeedsRestart = false;
private CheckBoxPreference mDebugMode;
- private CheckBoxPreference mUseSpacebarLanguageSwitch;
@Override
- protected void onCreate(Bundle icicle) {
+ public void onCreate(Bundle icicle) {
super.onCreate(icicle);
addPreferencesFromResource(R.xml.prefs_for_debug);
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
@@ -48,7 +51,7 @@ public class DebugSettings extends PreferenceActivity
}
@Override
- protected void onStop() {
+ public void onStop() {
super.onStop();
if (mServiceNeedsRestart) Process.killProcess(Process.myPid());
}
@@ -61,13 +64,9 @@ public class DebugSettings extends PreferenceActivity
updateDebugMode();
mServiceNeedsRestart = true;
}
- } else if (key.equals(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY)) {
- if (mUseSpacebarLanguageSwitch != null) {
- mUseSpacebarLanguageSwitch.setChecked(
- prefs.getBoolean(SubtypeSwitcher.USE_SPACEBAR_LANGUAGE_SWITCH_KEY,
- getResources().getBoolean(
- R.bool.config_use_spacebar_language_switcher)));
- }
+ } else if (key.equals(FORCE_NON_DISTINCT_MULTITOUCH_KEY)
+ || key.equals(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT)) {
+ mServiceNeedsRestart = true;
}
}
@@ -78,7 +77,9 @@ public class DebugSettings extends PreferenceActivity
boolean isDebugMode = mDebugMode.isChecked();
String version = "";
try {
- PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0);
+ final Context context = getActivity();
+ final String packageName = context.getPackageName();
+ PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
version = "Version " + info.versionName;
} catch (NameNotFoundException e) {
Log.e(TAG, "Could not find version info.");
diff --git a/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java b/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
new file mode 100644
index 000000000..cde20606a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+public class DebugSettingsActivity extends PreferenceActivity {
+ @Override
+ public Intent getIntent() {
+ final Intent modIntent = new Intent(super.getIntent());
+ modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DebugSettings.class.getName());
+ return modIntent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.english_ime_debug_settings);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index c7737b9a2..9c3d46e70 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -1,12 +1,12 @@
/*
* Copyright (C) 2008 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
@@ -16,28 +16,25 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
* strokes.
*/
public abstract class Dictionary {
/**
- * Whether or not to replicate the typed word in the suggested list, even if it's valid.
- */
- protected static final boolean INCLUDE_TYPED_WORD_IF_VALID = false;
-
- /**
* The weight to give to a word if it's length is the same as the number of typed characters.
*/
protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
- public static enum DataType {
- UNIGRAM, BIGRAM
- }
+ public static final int UNIGRAM = 0;
+ public static final int BIGRAM = 1;
+ public static final int NOT_A_PROBABILITY = -1;
/**
* Interface to be implemented by classes requesting words to be fetched from the dictionary.
- * @see #getWords(WordComposer, WordCallback)
+ * @see #getWords(WordComposer, CharSequence, WordCallback, ProximityInfo)
*/
public interface WordCallback {
/**
@@ -49,21 +46,25 @@ public abstract class Dictionary {
* @param score the score of occurrence. This is normalized between 1 and 255, but
* can exceed those limits
* @param dicTypeId of the dictionary where word was from
- * @param dataType tells type of this data
+ * @param dataType tells type of this data, either UNIGRAM or BIGRAM
* @return true if the word was added, false if no more words are required
*/
boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId,
- DataType dataType);
+ int dataType);
}
/**
- * Searches for words in the dictionary that match the characters in the composer. Matched
+ * Searches for words in the dictionary that match the characters in the composer. Matched
* words are added through the callback object.
* @param composer the key sequence to match
+ * @param prevWordForBigrams the previous word, or null if none
* @param callback the callback object to send matched words to as possible candidates
- * @see WordCallback#addWord(char[], int, int, int, int, DataType)
+ * @param proximityInfo the object for key proximity. May be ignored by some implementations.
+ * @see WordCallback#addWord(char[], int, int, int, int, int)
*/
- abstract public void getWords(final WordComposer composer, final WordCallback callback);
+ abstract public void getWords(final WordComposer composer,
+ final CharSequence prevWordForBigrams, final WordCallback callback,
+ final ProximityInfo proximityInfo);
/**
* Searches for pairs in the bigram dictionary that matches the previous word and all the
@@ -83,7 +84,11 @@ public abstract class Dictionary {
* @return true if the word exists, false otherwise
*/
abstract public boolean isValidWord(CharSequence word);
-
+
+ public int getFrequency(CharSequence word) {
+ return NOT_A_PROBABILITY;
+ }
+
/**
* Compares the contents of the character array with the typed word and returns true if they
* are the same.
@@ -110,4 +115,12 @@ public abstract class Dictionary {
public void close() {
// empty base implementation
}
+
+ /**
+ * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+ */
+
+ public boolean isInitialized() {
+ return true;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 5e7de3e6b..26c2e637e 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -16,33 +16,44 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+import android.util.Log;
+
import java.util.Collection;
-import java.util.List;
+import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Class for a collection of dictionaries that behave like one dictionary.
*/
public class DictionaryCollection extends Dictionary {
-
- protected final List<Dictionary> mDictionaries;
+ private final String TAG = DictionaryCollection.class.getSimpleName();
+ protected final CopyOnWriteArrayList<Dictionary> mDictionaries;
public DictionaryCollection() {
mDictionaries = new CopyOnWriteArrayList<Dictionary>();
}
public DictionaryCollection(Dictionary... dictionaries) {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ if (null == dictionaries) {
+ mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ } else {
+ mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ mDictionaries.removeAll(Collections.singleton(null));
+ }
}
public DictionaryCollection(Collection<Dictionary> dictionaries) {
mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ mDictionaries.removeAll(Collections.singleton(null));
}
@Override
- public void getWords(final WordComposer composer, final WordCallback callback) {
+ public void getWords(final WordComposer composer, final CharSequence prevWordForBigrams,
+ final WordCallback callback, final ProximityInfo proximityInfo) {
for (final Dictionary dict : mDictionaries)
- dict.getWords(composer, callback);
+ dict.getWords(composer, prevWordForBigrams, callback, proximityInfo);
}
@Override
@@ -60,12 +71,43 @@ public class DictionaryCollection extends Dictionary {
}
@Override
+ public int getFrequency(CharSequence word) {
+ int maxFreq = -1;
+ for (int i = mDictionaries.size() - 1; i >= 0; --i) {
+ final int tempFreq = mDictionaries.get(i).getFrequency(word);
+ if (tempFreq >= maxFreq) {
+ maxFreq = tempFreq;
+ }
+ }
+ return maxFreq;
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return !mDictionaries.isEmpty();
+ }
+
+ @Override
public void close() {
for (final Dictionary dict : mDictionaries)
dict.close();
}
- public void addDictionary(Dictionary newDict) {
+ // Warning: this is not thread-safe. Take necessary precaution when calling.
+ public void addDictionary(final Dictionary newDict) {
+ if (null == newDict) return;
+ if (mDictionaries.contains(newDict)) {
+ Log.w(TAG, "This collection already contains this dictionary: " + newDict);
+ }
mDictionaries.add(newDict);
}
+
+ // Warning: this is not thread-safe. Take necessary precaution when calling.
+ public void removeDictionary(final Dictionary dict) {
+ if (mDictionaries.contains(dict)) {
+ mDictionaries.remove(dict);
+ } else {
+ Log.w(TAG, "This collection does not contain this dictionary: " + dict);
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index bba331868..a22d73af7 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -22,60 +22,88 @@ import android.content.res.Resources;
import android.util.Log;
import java.io.File;
+import java.util.ArrayList;
import java.util.LinkedList;
-import java.util.List;
import java.util.Locale;
/**
* Factory for dictionary instances.
*/
public class DictionaryFactory {
-
- private static String TAG = DictionaryFactory.class.getSimpleName();
+ private static final String TAG = DictionaryFactory.class.getSimpleName();
+ // This class must be located in the same package as LatinIME.java.
+ private static final String RESOURCE_PACKAGE_NAME =
+ DictionaryFactory.class.getPackage().getName();
/**
- * Initializes a dictionary from a dictionary pack.
+ * Initializes a main dictionary collection from a dictionary pack, with explicit flags.
*
* This searches for a content provider providing a dictionary pack for the specified
- * locale. If none is found, it falls back to using the resource passed as fallBackResId
- * as a dictionary.
+ * locale. If none is found, it falls back to the built-in dictionary - if any.
* @param context application context for reading resources
* @param locale the locale for which to create the dictionary
- * @param fallbackResId the id of the resource to use as a fallback if no pack is found
- * @return an initialized instance of Dictionary
+ * @param useFullEditDistance whether to use the full edit distance in suggestions
+ * @return an initialized instance of DictionaryCollection
*/
- public static Dictionary createDictionaryFromManager(Context context, Locale locale,
- int fallbackResId) {
+ public static DictionaryCollection createMainDictionaryFromManager(final Context context,
+ final Locale locale, final boolean useFullEditDistance) {
if (null == locale) {
Log.e(TAG, "No locale defined for dictionary");
- return new DictionaryCollection(createBinaryDictionary(context, fallbackResId));
+ return new DictionaryCollection(createBinaryDictionary(context, locale));
}
- final List<Dictionary> dictList = new LinkedList<Dictionary>();
- for (final AssetFileAddress f : BinaryDictionaryGetter.getDictionaryFiles(locale,
- context, fallbackResId)) {
- dictList.add(new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength, null));
+ final LinkedList<Dictionary> dictList = new LinkedList<Dictionary>();
+ final ArrayList<AssetFileAddress> assetFileList =
+ BinaryDictionaryGetter.getDictionaryFiles(locale, context);
+ if (null != assetFileList) {
+ for (final AssetFileAddress f : assetFileList) {
+ final BinaryDictionary binaryDictionary =
+ new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength,
+ useFullEditDistance, locale);
+ if (binaryDictionary.isValidDictionary()) {
+ dictList.add(binaryDictionary);
+ }
+ }
}
- if (null == dictList) return null;
+ // If the list is empty, that means we should not use any dictionary (for example, the user
+ // explicitly disabled the main dictionary), so the following is okay. dictList is never
+ // null, but if for some reason it is, DictionaryCollection handles it gracefully.
return new DictionaryCollection(dictList);
}
/**
+ * Initializes a main dictionary collection from a dictionary pack, with default flags.
+ *
+ * This searches for a content provider providing a dictionary pack for the specified
+ * locale. If none is found, it falls back to the built-in dictionary, if any.
+ * @param context application context for reading resources
+ * @param locale the locale for which to create the dictionary
+ * @return an initialized instance of DictionaryCollection
+ */
+ public static DictionaryCollection createMainDictionaryFromManager(final Context context,
+ final Locale locale) {
+ return createMainDictionaryFromManager(context, locale, false /* useFullEditDistance */);
+ }
+
+ /**
* Initializes a dictionary from a raw resource file
* @param context application context for reading resources
- * @param resId the resource containing the raw binary dictionary
+ * @param locale the locale to use for the resource
* @return an initialized instance of BinaryDictionary
*/
- protected static BinaryDictionary createBinaryDictionary(Context context, int resId) {
+ protected static BinaryDictionary createBinaryDictionary(final Context context,
+ final Locale locale) {
AssetFileDescriptor afd = null;
try {
+ final int resId =
+ getMainDictionaryResourceIdIfAvailableForLocale(context.getResources(), locale);
+ if (0 == resId) return null;
afd = context.getResources().openRawResourceFd(resId);
if (afd == null) {
Log.e(TAG, "Found the resource but it is compressed. resId=" + resId);
return null;
}
- if (!isFullDictionary(afd)) return null;
final String sourceDir = context.getApplicationInfo().sourceDir;
final File packagePath = new File(sourceDir);
// TODO: Come up with a way to handle a directory.
@@ -83,10 +111,10 @@ public class DictionaryFactory {
Log.e(TAG, "sourceDir is not a file: " + sourceDir);
return null;
}
- return new BinaryDictionary(context,
- sourceDir, afd.getStartOffset(), afd.getLength(), null);
+ return new BinaryDictionary(context, sourceDir, afd.getStartOffset(), afd.getLength(),
+ false /* useFullEditDistance */, locale);
} catch (android.content.res.Resources.NotFoundException e) {
- Log.e(TAG, "Could not find the resource. resId=" + resId);
+ Log.e(TAG, "Could not find the resource");
return null;
} finally {
if (null != afd) {
@@ -105,14 +133,14 @@ public class DictionaryFactory {
* @param dictionary the file to read
* @param startOffset the offset in the file where the data starts
* @param length the length of the data
- * @param flagArray the flags to use with this data for testing
+ * @param useFullEditDistance whether to use the full edit distance in suggestions
* @return the created dictionary, or null.
*/
public static Dictionary createDictionaryForTest(Context context, File dictionary,
- long startOffset, long length, Flag[] flagArray) {
+ long startOffset, long length, final boolean useFullEditDistance, Locale locale) {
if (dictionary.isFile()) {
return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
- flagArray);
+ useFullEditDistance, locale);
} else {
Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
return null;
@@ -127,51 +155,47 @@ public class DictionaryFactory {
*/
public static boolean isDictionaryAvailable(Context context, Locale locale) {
final Resources res = context.getResources();
- final Locale saveLocale = Utils.setSystemLocale(res, locale);
-
- final int resourceId = Utils.getMainDictionaryResourceId(res);
- final AssetFileDescriptor afd = res.openRawResourceFd(resourceId);
- final boolean hasDictionary = isFullDictionary(afd);
- try {
- if (null != afd) afd.close();
- } catch (java.io.IOException e) {
- /* Um, what can we do here exactly? */
- }
-
- Utils.setSystemLocale(res, saveLocale);
- return hasDictionary;
+ return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
}
- // TODO: Do not use the size of the dictionary as an unique dictionary ID.
- public static Long getDictionaryId(Context context, Locale locale) {
- final Resources res = context.getResources();
- final Locale saveLocale = Utils.setSystemLocale(res, locale);
+ private static final String DEFAULT_MAIN_DICT = "main";
+ private static final String MAIN_DICT_PREFIX = "main_";
- final int resourceId = Utils.getMainDictionaryResourceId(res);
- final AssetFileDescriptor afd = res.openRawResourceFd(resourceId);
- final Long size = (afd != null && afd.getLength() > PLACEHOLDER_LENGTH)
- ? afd.getLength()
- : null;
- try {
- if (null != afd) afd.close();
- } catch (java.io.IOException e) {
+ /**
+ * Helper method to return a dictionary res id for a locale, or 0 if none.
+ * @param locale dictionary locale
+ * @return main dictionary resource id
+ */
+ private static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
+ final Locale locale) {
+ int resId;
+ // Try to find main_language_country dictionary.
+ if (!locale.getCountry().isEmpty()) {
+ final String dictLanguageCountry = MAIN_DICT_PREFIX + locale.toString().toLowerCase();
+ if ((resId = res.getIdentifier(
+ dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
+ }
+ }
+
+ // Try to find main_language dictionary.
+ final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
+ if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+ return resId;
}
- Utils.setSystemLocale(res, saveLocale);
- return size;
+ // Not found, return 0
+ return 0;
}
- // TODO: Find the Right Way to find out whether the resource is a placeholder or not.
- // Suggestion : strip the locale, open the placeholder file and store its offset.
- // Upon opening the file, if it's the same offset, then it's the placeholder.
- private static final long PLACEHOLDER_LENGTH = 34;
/**
- * Finds out whether the data pointed out by an AssetFileDescriptor is a full
- * dictionary (as opposed to null, or to a place holder).
- * @param afd the file descriptor to test, or null
- * @return true if the dictionary is a real full dictionary, false if it's null or a placeholder
+ * Returns a main dictionary resource id
+ * @param locale dictionary locale
+ * @return main dictionary resource id
*/
- protected static boolean isFullDictionary(final AssetFileDescriptor afd) {
- return (afd != null && afd.getLength() > PLACEHOLDER_LENGTH);
+ public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
+ int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
+ if (0 != resourceId) return resourceId;
+ return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
}
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
index 9d30af84b..9c37d7673 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
@@ -51,6 +51,8 @@ public class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
if (null == packageUri) return; // No package name : we can't do anything
final String packageName = packageUri.getSchemeSpecificPart();
if (null == packageName) return;
+ // TODO: do this in a more appropriate place
+ TargetApplicationGetter.removeApplicationInfoCache(packageName);
final PackageInfo packageInfo;
try {
packageInfo = manager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS);
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
deleted file mode 100644
index e56aa695d..000000000
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ /dev/null
@@ -1,293 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import com.android.inputmethod.compat.InputConnectionCompatUtils;
-
-import android.text.TextUtils;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.regex.Pattern;
-
-/**
- * Utility methods to deal with editing text through an InputConnection.
- */
-public class EditingUtils {
- /**
- * Number of characters we want to look back in order to identify the previous word
- */
- private static final int LOOKBACK_CHARACTER_NUM = 15;
-
- private EditingUtils() {
- // Unintentional empty constructor for singleton.
- }
-
- /**
- * Append newText to the text field represented by connection.
- * The new text becomes selected.
- */
- public static void appendText(InputConnection connection, String newText) {
- if (connection == null) {
- return;
- }
-
- // Commit the composing text
- connection.finishComposingText();
-
- // Add a space if the field already has text.
- String text = newText;
- CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
- if (charBeforeCursor != null
- && !charBeforeCursor.equals(" ")
- && (charBeforeCursor.length() > 0)) {
- text = " " + text;
- }
-
- connection.setComposingText(text, 1);
- }
-
- private static int getCursorPosition(InputConnection connection) {
- ExtractedText extracted = connection.getExtractedText(
- new ExtractedTextRequest(), 0);
- if (extracted == null) {
- return -1;
- }
- return extracted.startOffset + extracted.selectionStart;
- }
-
- /**
- * @param connection connection to the current text field.
- * @param separators characters which may separate words
- * @return the word that surrounds the cursor, including up to one trailing
- * separator. For example, if the field contains "he|llo world", where |
- * represents the cursor, then "hello " will be returned.
- */
- public static String getWordAtCursor(InputConnection connection, String separators) {
- Range r = getWordRangeAtCursor(connection, separators);
- return (r == null) ? null : r.mWord;
- }
-
- /**
- * Removes the word surrounding the cursor. Parameters are identical to
- * getWordAtCursor.
- */
- public static void deleteWordAtCursor(InputConnection connection, String separators) {
- Range range = getWordRangeAtCursor(connection, separators);
- if (range == null) return;
-
- connection.finishComposingText();
- // Move cursor to beginning of word, to avoid crash when cursor is outside
- // of valid range after deleting text.
- int newCursor = getCursorPosition(connection) - range.mCharsBefore;
- connection.setSelection(newCursor, newCursor);
- connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter);
- }
-
- /**
- * Represents a range of text, relative to the current cursor position.
- */
- public static class Range {
- /** Characters before selection start */
- public final int mCharsBefore;
-
- /**
- * Characters after selection start, including one trailing word
- * separator.
- */
- public final int mCharsAfter;
-
- /** The actual characters that make up a word */
- public final String mWord;
-
- public Range(int charsBefore, int charsAfter, String word) {
- if (charsBefore < 0 || charsAfter < 0) {
- throw new IndexOutOfBoundsException();
- }
- this.mCharsBefore = charsBefore;
- this.mCharsAfter = charsAfter;
- this.mWord = word;
- }
- }
-
- private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
- if (connection == null || sep == null) {
- return null;
- }
- CharSequence before = connection.getTextBeforeCursor(1000, 0);
- CharSequence after = connection.getTextAfterCursor(1000, 0);
- if (before == null || after == null) {
- return null;
- }
-
- // Find first word separator before the cursor
- int start = before.length();
- while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
-
- // Find last word separator after the cursor
- int end = -1;
- while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
- // Nothing to do here.
- }
-
- int cursor = getCursorPosition(connection);
- if (start >= 0 && cursor + end <= after.length() + before.length()) {
- String word = before.toString().substring(start, before.length())
- + after.toString().substring(0, end);
- return new Range(before.length() - start, end, word);
- }
-
- return null;
- }
-
- private static boolean isWhitespace(int code, String whitespace) {
- return whitespace.contains(String.valueOf((char) code));
- }
-
- private static final Pattern spaceRegex = Pattern.compile("\\s+");
-
-
- public static CharSequence getPreviousWord(InputConnection connection,
- String sentenceSeperators) {
- //TODO: Should fix this. This could be slow!
- CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getPreviousWord(prev, sentenceSeperators);
- }
-
- // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
- // Also, it won't return words that end in a separator.
- // Example :
- // "abc def|" -> abc
- // "abc def |" -> abc
- // "abc def. |" -> abc
- // "abc def . |" -> def
- // "abc|" -> null
- // "abc |" -> null
- // "abc. def|" -> null
- public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // If we can't find two words, or we found an empty word, return null.
- if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 2];
- }
-
- public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
- final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getThisWord(prev, sentenceSeperators);
- }
-
- // Get the word immediately before the cursor, even if there is whitespace between it and
- // the cursor - but not if there is punctuation.
- // Example :
- // "abc def|" -> def
- // "abc def |" -> def
- // "abc def. |" -> null
- // "abc def . |" -> null
- public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // No word : return null
- if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 1];
- }
-
- public static class SelectedWord {
- public final int mStart;
- public final int mEnd;
- public final CharSequence mWord;
-
- public SelectedWord(int start, int end, CharSequence word) {
- mStart = start;
- mEnd = end;
- mWord = word;
- }
- }
-
- /**
- * Takes a character sequence with a single character and checks if the character occurs
- * in a list of word separators or is empty.
- * @param singleChar A CharSequence with null, zero or one character
- * @param wordSeparators A String containing the word separators
- * @return true if the character is at a word boundary, false otherwise
- */
- private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
- return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
- }
-
- /**
- * Checks if the cursor is inside a word or the current selection is a whole word.
- * @param ic the InputConnection for accessing the text field
- * @param selStart the start position of the selection within the text field
- * @param selEnd the end position of the selection within the text field. This could be
- * the same as selStart, if there's no selection.
- * @param wordSeparators the word separator characters for the current language
- * @return an object containing the text and coordinates of the selected/touching word,
- * null if the selection/cursor is not marking a whole word.
- */
- public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
- int selStart, int selEnd, String wordSeparators) {
- if (selStart == selEnd) {
- // There is just a cursor, so get the word at the cursor
- EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators);
- if (range != null && !TextUtils.isEmpty(range.mWord)) {
- return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter,
- range.mWord);
- }
- } else {
- // Is the previous character empty or a word separator? If not, return null.
- CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
- if (!isWordBoundary(charsBefore, wordSeparators)) {
- return null;
- }
-
- // Is the next character empty or a word separator? If not, return null.
- CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
- if (!isWordBoundary(charsAfter, wordSeparators)) {
- return null;
- }
-
- // Extract the selection alone
- CharSequence touching = InputConnectionCompatUtils.getSelectedText(
- ic, selStart, selEnd);
- if (TextUtils.isEmpty(touching)) return null;
- // Is any part of the selection a separator? If so, return null.
- final int length = touching.length();
- for (int i = 0; i < length; i++) {
- if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
- return null;
- }
- }
- // Prepare the selected word
- return new SelectedWord(selStart, selEnd, touching);
- }
- return null;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
new file mode 100644
index 000000000..c65404cbc
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -0,0 +1,473 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Abstract base class for an expandable dictionary that can be created and updated dynamically
+ * during runtime. When updated it automatically generates a new binary dictionary to handle future
+ * queries in native code. This binary dictionary is written to internal storage, and potentially
+ * shared across multiple ExpandableBinaryDictionary instances. Updates to each dictionary filename
+ * are controlled across multiple instances to ensure that only one instance can update the same
+ * dictionary at the same time.
+ */
+abstract public class ExpandableBinaryDictionary extends Dictionary {
+
+ /** Used for Log actions from this class */
+ private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
+
+ /** Whether to print debug output to log */
+ private static boolean DEBUG = false;
+
+ /**
+ * The maximum length of a word in this dictionary. This is the same value as the binary
+ * dictionary.
+ */
+ protected static final int MAX_WORD_LENGTH = BinaryDictionary.MAX_WORD_LENGTH;
+
+ /**
+ * A static map of locks, each of which controls access to a single binary dictionary file. They
+ * ensure that only one instance can update the same dictionary at the same time. The key for
+ * this map is the filename and the value is the shared dictionary controller associated with
+ * that filename.
+ */
+ private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
+ new HashMap<String, DictionaryController>();
+
+ /** The application context. */
+ protected final Context mContext;
+
+ /**
+ * The binary dictionary generated dynamically from the fusion dictionary. This is used to
+ * answer unigram and bigram queries.
+ */
+ private BinaryDictionary mBinaryDictionary;
+
+ /** The expandable fusion dictionary used to generate the binary dictionary. */
+ private FusionDictionary mFusionDictionary;
+
+ /** The dictionary type id. */
+ public final int mDicTypeId;
+
+ /**
+ * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple
+ * dictionary instances with the same filename is supported, with access controlled by
+ * DictionaryController.
+ */
+ private final String mFilename;
+
+ /** Controls access to the shared binary dictionary file across multiple instances. */
+ private final DictionaryController mSharedDictionaryController;
+
+ /** Controls access to the local binary dictionary for this instance. */
+ private final DictionaryController mLocalDictionaryController = new DictionaryController();
+
+ /**
+ * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
+ * thread.
+ */
+ protected abstract void loadDictionaryAsync();
+
+ /**
+ * Indicates that the source dictionary content has changed and a rebuild of the binary file is
+ * required. If it returns false, the next reload will only read the current binary dictionary
+ * from file. Note that the shared binary dictionary is locked when this is called.
+ */
+ protected abstract boolean hasContentChanged();
+
+ /**
+ * Gets the shared dictionary controller for the given filename.
+ */
+ private static synchronized DictionaryController getSharedDictionaryController(
+ String filename) {
+ DictionaryController controller = sSharedDictionaryControllers.get(filename);
+ if (controller == null) {
+ controller = new DictionaryController();
+ sSharedDictionaryControllers.put(filename, controller);
+ }
+ return controller;
+ }
+
+ /**
+ * Creates a new expandable binary dictionary.
+ *
+ * @param context The application context of the parent.
+ * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
+ * filename is supported.
+ * @param dictType The type of this dictionary.
+ */
+ public ExpandableBinaryDictionary(
+ final Context context, final String filename, final int dictType) {
+ mDicTypeId = dictType;
+ mFilename = filename;
+ mContext = context;
+ mBinaryDictionary = null;
+ mSharedDictionaryController = getSharedDictionaryController(filename);
+ clearFusionDictionary();
+ }
+
+ protected static String getFilenameWithLocale(final String name, final String localeStr) {
+ return name + "." + localeStr + ".dict";
+ }
+
+ /**
+ * Closes and cleans up the binary dictionary.
+ */
+ @Override
+ public void close() {
+ // Ensure that no other threads are accessing the local binary dictionary.
+ mLocalDictionaryController.lock();
+ try {
+ if (mBinaryDictionary != null) {
+ mBinaryDictionary.close();
+ mBinaryDictionary = null;
+ }
+ } finally {
+ mLocalDictionaryController.unlock();
+ }
+ }
+
+ /**
+ * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on
+ * the native side.
+ */
+ public void clearFusionDictionary() {
+ mFusionDictionary = new FusionDictionary(new Node(),
+ new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
+ false));
+ }
+
+ /**
+ * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
+ * are done to update the binary dictionary.
+ */
+ // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
+ // considering performance regression.
+ protected void addWord(final String word, final String shortcutTarget, final int frequency) {
+ if (shortcutTarget == null) {
+ mFusionDictionary.add(word, frequency, null);
+ } else {
+ // TODO: Do this in the subclass, with this class taking an arraylist.
+ final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>();
+ shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
+ mFusionDictionary.add(word, frequency, shortcutTargets);
+ }
+ }
+
+ /**
+ * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are
+ * done to update the binary dictionary.
+ */
+ // TODO: Create "cache dictionary" to cache fresh bigrams for frequently updated dictionaries,
+ // considering performance regression.
+ protected void setBigram(final String prevWord, final String word, final int frequency) {
+ mFusionDictionary.setBigram(prevWord, word, frequency);
+ }
+
+ @Override
+ public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
+ final WordCallback callback, final ProximityInfo proximityInfo) {
+ asyncReloadDictionaryIfRequired();
+ getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ }
+
+ protected final void getWordsInner(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final WordCallback callback,
+ final ProximityInfo proximityInfo) {
+ // Ensure that there are no concurrent calls to getWords. If there are, do nothing and
+ // return.
+ if (mLocalDictionaryController.tryLock()) {
+ try {
+ if (mBinaryDictionary != null) {
+ mBinaryDictionary.getWords(codes, prevWordForBigrams, callback, proximityInfo);
+ }
+ } finally {
+ mLocalDictionaryController.unlock();
+ }
+ }
+ }
+
+ @Override
+ public void getBigrams(final WordComposer codes, final CharSequence previousWord,
+ final WordCallback callback) {
+ asyncReloadDictionaryIfRequired();
+ getBigramsInner(codes, previousWord, callback);
+ }
+
+ protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord,
+ final WordCallback callback) {
+ if (mLocalDictionaryController.tryLock()) {
+ try {
+ if (mBinaryDictionary != null) {
+ mBinaryDictionary.getBigrams(codes, previousWord, callback);
+ }
+ } finally {
+ mLocalDictionaryController.unlock();
+ }
+ }
+ }
+
+ @Override
+ public boolean isValidWord(final CharSequence word) {
+ asyncReloadDictionaryIfRequired();
+ return isValidWordInner(word);
+ }
+
+ protected boolean isValidWordInner(final CharSequence word) {
+ if (mLocalDictionaryController.tryLock()) {
+ try {
+ return isValidWordLocked(word);
+ } finally {
+ mLocalDictionaryController.unlock();
+ }
+ }
+ return false;
+ }
+
+ protected boolean isValidWordLocked(final CharSequence word) {
+ if (mBinaryDictionary == null) return false;
+ return mBinaryDictionary.isValidWord(word);
+ }
+
+ protected boolean isValidBigram(final CharSequence word1, final CharSequence word2) {
+ if (mBinaryDictionary == null) return false;
+ return mBinaryDictionary.isValidBigram(word1, word2);
+ }
+
+ protected boolean isValidBigramInner(final CharSequence word1, final CharSequence word2) {
+ if (mLocalDictionaryController.tryLock()) {
+ try {
+ return isValidBigramLocked(word1, word2);
+ } finally {
+ mLocalDictionaryController.unlock();
+ }
+ }
+ return false;
+ }
+
+ protected boolean isValidBigramLocked(final CharSequence word1, final CharSequence word2) {
+ if (mBinaryDictionary == null) return false;
+ return mBinaryDictionary.isValidBigram(word1, word2);
+ }
+
+ /**
+ * Load the current binary dictionary from internal storage in a background thread. If no binary
+ * dictionary exists, this method will generate one.
+ */
+ protected void loadDictionary() {
+ mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis();
+ asyncReloadDictionaryIfRequired();
+ }
+
+ /**
+ * Loads the current binary dictionary from internal storage. Assumes the dictionary file
+ * exists.
+ */
+ protected void loadBinaryDictionary() {
+ if (DEBUG) {
+ Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
+ + mSharedDictionaryController.mLastUpdateRequestTime + " update="
+ + mSharedDictionaryController.mLastUpdateTime);
+ }
+
+ final File file = new File(mContext.getFilesDir(), mFilename);
+ final String filename = file.getAbsolutePath();
+ final long length = file.length();
+
+ // Build the new binary dictionary
+ final BinaryDictionary newBinaryDictionary =
+ new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */,
+ null);
+
+ if (mBinaryDictionary != null) {
+ // Ensure all threads accessing the current dictionary have finished before swapping in
+ // the new one.
+ final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
+ mLocalDictionaryController.lock();
+ mBinaryDictionary = newBinaryDictionary;
+ mLocalDictionaryController.unlock();
+ oldBinaryDictionary.close();
+ } else {
+ mBinaryDictionary = newBinaryDictionary;
+ }
+ }
+
+ /**
+ * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
+ */
+ private void generateBinaryDictionary() {
+ if (DEBUG) {
+ Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
+ + mSharedDictionaryController.mLastUpdateRequestTime + " update="
+ + mSharedDictionaryController.mLastUpdateTime);
+ }
+
+ loadDictionaryAsync();
+
+ final String tempFileName = mFilename + ".temp";
+ final File file = new File(mContext.getFilesDir(), mFilename);
+ final File tempFile = new File(mContext.getFilesDir(), tempFileName);
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(tempFile);
+ BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1);
+ out.flush();
+ out.close();
+ tempFile.renameTo(file);
+ clearFusionDictionary();
+ } catch (IOException e) {
+ Log.e(TAG, "IO exception while writing file: " + e);
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "Unsupported format: " + e);
+ } finally {
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ }
+
+ /**
+ * Marks that the dictionary is out of date and requires a reload.
+ *
+ * @param requiresRebuild Indicates that the source dictionary content has changed and a rebuild
+ * of the binary file is required. If not true, the next reload process will only read
+ * the current binary dictionary from file.
+ */
+ protected void setRequiresReload(final boolean requiresRebuild) {
+ final long time = SystemClock.uptimeMillis();
+ mLocalDictionaryController.mLastUpdateRequestTime = time;
+ mSharedDictionaryController.mLastUpdateRequestTime = time;
+ if (DEBUG) {
+ Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update="
+ + mSharedDictionaryController.mLastUpdateTime);
+ }
+ }
+
+ /**
+ * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread.
+ */
+ void asyncReloadDictionaryIfRequired() {
+ if (!isReloadRequired()) return;
+ if (DEBUG) {
+ Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename);
+ }
+ new AsyncReloadDictionaryTask().start();
+ }
+
+ /**
+ * Reloads the dictionary if required.
+ */
+ protected final void syncReloadDictionaryIfRequired() {
+ if (!isReloadRequired()) return;
+ syncReloadDictionaryInternal();
+ }
+
+ /**
+ * Returns whether a dictionary reload is required.
+ */
+ private boolean isReloadRequired() {
+ return mBinaryDictionary == null || mLocalDictionaryController.isOutOfDate();
+ }
+
+ /**
+ * Reloads the dictionary. Access is controlled on a per dictionary file basis and supports
+ * concurrent calls from multiple instances that share the same dictionary file.
+ */
+ private final void syncReloadDictionaryInternal() {
+ // Ensure that only one thread attempts to read or write to the shared binary dictionary
+ // file at the same time.
+ mSharedDictionaryController.lock();
+ try {
+ final long time = SystemClock.uptimeMillis();
+ final boolean dictionaryFileExists = dictionaryFileExists();
+ if (mSharedDictionaryController.isOutOfDate() || !dictionaryFileExists) {
+ // If the shared dictionary file does not exist or is out of date, the first
+ // instance that acquires the lock will generate a new one.
+ if (hasContentChanged() || !dictionaryFileExists) {
+ // If the source content has changed or the dictionary does not exist, rebuild
+ // the binary dictionary. Empty dictionaries are supported (in the case where
+ // loadDictionaryAsync() adds nothing) in order to provide a uniform framework.
+ mSharedDictionaryController.mLastUpdateTime = time;
+ generateBinaryDictionary();
+ loadBinaryDictionary();
+ } else {
+ // If not, the reload request was unnecessary so revert LastUpdateRequestTime
+ // to LastUpdateTime.
+ mSharedDictionaryController.mLastUpdateRequestTime =
+ mSharedDictionaryController.mLastUpdateTime;
+ }
+ } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
+ < mSharedDictionaryController.mLastUpdateTime) {
+ // Otherwise, if the local dictionary is older than the shared dictionary, load the
+ // shared dictionary.
+ loadBinaryDictionary();
+ }
+ mLocalDictionaryController.mLastUpdateTime = time;
+ } finally {
+ mSharedDictionaryController.unlock();
+ }
+ }
+
+ // TODO: cache the file's existence so that we avoid doing a disk access each time.
+ private boolean dictionaryFileExists() {
+ final File file = new File(mContext.getFilesDir(), mFilename);
+ return file.exists();
+ }
+
+ /**
+ * Thread class for asynchronously reloading and rewriting the binary dictionary.
+ */
+ private class AsyncReloadDictionaryTask extends Thread {
+ @Override
+ public void run() {
+ syncReloadDictionaryInternal();
+ }
+ }
+
+ /**
+ * Lock for controlling access to a given binary dictionary and for tracking whether the
+ * dictionary is out of date. Can be shared across multiple dictionary instances that access the
+ * same filename.
+ */
+ private static class DictionaryController extends ReentrantLock {
+ private volatile long mLastUpdateTime = 0;
+ private volatile long mLastUpdateRequestTime = 0;
+
+ private boolean isOutOfDate() {
+ return (mLastUpdateRequestTime > mLastUpdateTime);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 97a4a1816..f5886aa12 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -17,10 +17,14 @@
package com.android.inputmethod.latin;
import android.content.Context;
-import android.os.AsyncTask;
+import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
+import java.util.ArrayList;
import java.util.LinkedList;
/**
@@ -28,17 +32,12 @@ import java.util.LinkedList;
* be searched for suggestions and valid words.
*/
public class ExpandableDictionary extends Dictionary {
- /**
- * There is difference between what java and native code can handle.
- * It uses 32 because Java stack overflows when greater value is used.
- */
- protected static final int MAX_WORD_LENGTH = 32;
// Bigram frequency is a fixed point number with 1 meaning 1.2 and 255 meaning 1.8.
protected static final int BIGRAM_MAX_FREQUENCY = 255;
private Context mContext;
- private char[] mWordBuilder = new char[MAX_WORD_LENGTH];
+ private char[] mWordBuilder = new char[BinaryDictionary.MAX_WORD_LENGTH];
private int mDicTypeId;
private int mMaxDepth;
private int mInputLength;
@@ -51,11 +50,14 @@ public class ExpandableDictionary extends Dictionary {
private Object mUpdatingLock = new Object();
private static class Node {
+ Node() {}
char mCode;
int mFrequency;
boolean mTerminal;
Node mParent;
NodeArray mChildren;
+ ArrayList<char[]> mShortcutTargets;
+ boolean mShortcutOnly;
LinkedList<NextWord> mNGrams; // Supports ngram
}
@@ -80,31 +82,72 @@ public class ExpandableDictionary extends Dictionary {
}
}
- private static class NextWord {
- public final Node mWord;
- private int mFrequency;
+ protected interface NextWord {
+ public Node getWordNode();
+ public int getFrequency();
+ public ForgettingCurveParams getFcParams();
+ public int notifyTypedAgainAndGetFrequency();
+ }
- public NextWord(Node word, int frequency) {
+ private static class NextStaticWord implements NextWord {
+ public final Node mWord;
+ private final int mFrequency;
+ public NextStaticWord(Node word, int frequency) {
mWord = word;
mFrequency = frequency;
}
+ @Override
+ public Node getWordNode() {
+ return mWord;
+ }
+
+ @Override
public int getFrequency() {
return mFrequency;
}
- public int setFrequency(int freq) {
- mFrequency = freq;
- return mFrequency;
+ @Override
+ public ForgettingCurveParams getFcParams() {
+ return null;
}
- public int addFrequency(int add) {
- mFrequency += add;
- if (mFrequency > BIGRAM_MAX_FREQUENCY) mFrequency = BIGRAM_MAX_FREQUENCY;
+ @Override
+ public int notifyTypedAgainAndGetFrequency() {
return mFrequency;
}
}
+ private static class NextHistoryWord implements NextWord {
+ public final Node mWord;
+ public final ForgettingCurveParams mFcp;
+
+ public NextHistoryWord(Node word, ForgettingCurveParams fcp) {
+ mWord = word;
+ mFcp = fcp;
+ }
+
+ @Override
+ public Node getWordNode() {
+ return mWord;
+ }
+
+ @Override
+ public int getFrequency() {
+ return mFcp.getFrequency();
+ }
+
+ @Override
+ public ForgettingCurveParams getFcParams() {
+ return mFcp;
+ }
+
+ @Override
+ public int notifyTypedAgainAndGetFrequency() {
+ return mFcp.notifyTypedAgainAndGetFrequency();
+ }
+ }
+
private NodeArray mRoots;
private int[][] mCodes;
@@ -112,7 +155,7 @@ public class ExpandableDictionary extends Dictionary {
public ExpandableDictionary(Context context, int dicTypeId) {
mContext = context;
clearDictionary();
- mCodes = new int[MAX_WORD_LENGTH][];
+ mCodes = new int[BinaryDictionary.MAX_WORD_LENGTH][];
mDicTypeId = dicTypeId;
}
@@ -126,7 +169,7 @@ public class ExpandableDictionary extends Dictionary {
if (!mUpdatingDictionary) {
mUpdatingDictionary = true;
mRequiresReload = false;
- new LoadDictionaryTask().execute();
+ new LoadDictionaryTask().start();
}
}
@@ -150,38 +193,50 @@ public class ExpandableDictionary extends Dictionary {
}
public int getMaxWordLength() {
- return MAX_WORD_LENGTH;
+ return BinaryDictionary.MAX_WORD_LENGTH;
}
- public void addWord(String word, int frequency) {
- addWordRec(mRoots, word, 0, frequency, null);
+ public void addWord(final String word, final String shortcutTarget, final int frequency) {
+ if (word.length() >= BinaryDictionary.MAX_WORD_LENGTH) {
+ return;
+ }
+ addWordRec(mRoots, word, 0, shortcutTarget, frequency, null);
}
private void addWordRec(NodeArray children, final String word, final int depth,
- final int frequency, Node parentNode) {
+ final String shortcutTarget, final int frequency, Node parentNode) {
final int wordLength = word.length();
if (wordLength <= depth) return;
final char c = word.charAt(depth);
// Does children have the current character?
final int childrenLength = children.mLength;
Node childNode = null;
- boolean found = false;
for (int i = 0; i < childrenLength; i++) {
- childNode = children.mData[i];
- if (childNode.mCode == c) {
- found = true;
+ final Node node = children.mData[i];
+ if (node.mCode == c) {
+ childNode = node;
break;
}
}
- if (!found) {
+ final boolean isShortcutOnly = (null != shortcutTarget);
+ if (childNode == null) {
childNode = new Node();
childNode.mCode = c;
childNode.mParent = parentNode;
+ childNode.mShortcutOnly = isShortcutOnly;
children.add(childNode);
}
- if (wordLength == depth + 1) {
+ if (wordLength == depth + 1 && shortcutTarget != null) {
// Terminate this word
childNode.mTerminal = true;
+ if (isShortcutOnly) {
+ if (null == childNode.mShortcutTargets) {
+ childNode.mShortcutTargets = new ArrayList<char[]>();
+ }
+ childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
+ } else {
+ childNode.mShortcutOnly = false;
+ }
childNode.mFrequency = Math.max(frequency, childNode.mFrequency);
if (childNode.mFrequency > 255) childNode.mFrequency = 255;
return;
@@ -189,29 +244,51 @@ public class ExpandableDictionary extends Dictionary {
if (childNode.mChildren == null) {
childNode.mChildren = new NodeArray();
}
- addWordRec(childNode.mChildren, word, depth + 1, frequency, childNode);
+ addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, childNode);
}
@Override
- public void getWords(final WordComposer codes, final WordCallback callback) {
+ public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
+ final WordCallback callback, final ProximityInfo proximityInfo) {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) startDictionaryLoadingTaskLocked();
// Currently updating contacts, don't return any results.
if (mUpdatingDictionary) return;
}
+ if (codes.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
+ return;
+ }
+ final ArrayList<SuggestedWordInfo> suggestions =
+ getWordsInner(codes, prevWordForBigrams, proximityInfo);
+ Utils.addAllSuggestions(mDicTypeId, Dictionary.UNIGRAM, suggestions, callback);
+ }
+ protected final ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
+ final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<SuggestedWordInfo>();
mInputLength = codes.size();
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
+ final int[] xCoordinates = codes.getXCoordinates();
+ final int[] yCoordinates = codes.getYCoordinates();
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
- mCodes[i] = codes.getCodesAt(i);
+ // TODO: Calculate proximity info here.
+ if (mCodes[i] == null || mCodes[i].length < 1) {
+ mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE];
+ }
+ final int x = xCoordinates != null && i < xCoordinates.length ?
+ xCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ final int y = xCoordinates != null && i < yCoordinates.length ?
+ yCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]);
}
mMaxDepth = mInputLength * 3;
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions);
for (int i = 0; i < mInputLength; i++) {
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions);
}
+ return suggestions;
}
@Override
@@ -221,8 +298,35 @@ public class ExpandableDictionary extends Dictionary {
if (mRequiresReload) startDictionaryLoadingTaskLocked();
if (mUpdatingDictionary) return false;
}
- final int freq = getWordFrequency(word);
- return freq > -1;
+ final Node node = searchNode(mRoots, word, 0, word.length());
+ // If node is null, we didn't find the word, so it's not valid.
+ // If node.mShortcutOnly is true, then it exists as a shortcut but not as a word,
+ // so that means it's not a valid word.
+ // If node.mShortcutOnly is false, then it exists as a word (it may also exist as
+ // a shortcut, but this does not matter), so it's a valid word.
+ return (node == null) ? false : !node.mShortcutOnly;
+ }
+
+ protected boolean removeBigram(String word1, String word2) {
+ // Refer to addOrSetBigram() about word1.toLowerCase()
+ final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
+ final Node secondWord = searchWord(mRoots, word2, 0, null);
+ LinkedList<NextWord> bigrams = firstWord.mNGrams;
+ NextWord bigramNode = null;
+ if (bigrams == null || bigrams.size() == 0) {
+ return false;
+ } else {
+ for (NextWord nw : bigrams) {
+ if (nw.getWordNode() == secondWord) {
+ bigramNode = nw;
+ break;
+ }
+ }
+ }
+ if (bigramNode == null) {
+ return false;
+ }
+ return bigrams.remove(bigramNode);
}
/**
@@ -230,10 +334,27 @@ public class ExpandableDictionary extends Dictionary {
*/
protected int getWordFrequency(CharSequence word) {
// Case-sensitive search
- Node node = searchNode(mRoots, word, 0, word.length());
+ final Node node = searchNode(mRoots, word, 0, word.length());
return (node == null) ? -1 : node.mFrequency;
}
+ protected NextWord getBigramWord(String word1, String word2) {
+ // Refer to addOrSetBigram() about word1.toLowerCase()
+ final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
+ final Node secondWord = searchWord(mRoots, word2, 0, null);
+ LinkedList<NextWord> bigrams = firstWord.mNGrams;
+ if (bigrams == null || bigrams.size() == 0) {
+ return null;
+ } else {
+ for (NextWord nw : bigrams) {
+ if (nw.getWordNode() == secondWord) {
+ return nw;
+ }
+ }
+ }
+ return null;
+ }
+
private static int computeSkippedWordFinalFreq(int freq, int snr, int inputLength) {
// The computation itself makes sense for >= 2, but the == 2 case returns 0
// anyway so we may as well test against 3 instead and return the constant
@@ -245,6 +366,39 @@ public class ExpandableDictionary extends Dictionary {
}
/**
+ * Helper method to add a word and its shortcuts.
+ *
+ * @param node the terminal node
+ * @param word the word to insert, as an array of code points
+ * @param depth the depth of the node in the tree
+ * @param finalFreq the frequency for this word
+ * @param suggestions the suggestion collection to add the suggestions to
+ * @return whether there is still space for more words.
+ */
+ private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
+ final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) {
+ if (finalFreq > 0 && !node.mShortcutOnly) {
+ // Use KIND_CORRECTION always. This dictionary does not really have a notion of
+ // COMPLETION against CORRECTION; we could artificially add one by looking at
+ // the respective size of the typed word and the suggestion if it matters sometime
+ // in the future.
+ suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
+ SuggestedWordInfo.KIND_CORRECTION));
+ if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
+ }
+ if (null != node.mShortcutTargets) {
+ final int length = node.mShortcutTargets.size();
+ for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
+ final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
+ suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
+ finalFreq, SuggestedWordInfo.KIND_SHORTCUT));
+ if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
+ }
+ }
+ return true;
+ }
+
+ /**
* Recursively traverse the tree for words that match the input. Input consists of
* a list of arrays. Each item in the list is one input character position. An input
* character is actually an array of multiple possible candidates. This function is not
@@ -261,21 +415,21 @@ public class ExpandableDictionary extends Dictionary {
* case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
* "wouldve", it could be matching "would've", so the depth will be one more than the
* inputIndex
- * @param callback the callback class for adding a word
+ * @param suggestions the list in which to add suggestions
*/
// TODO: Share this routine with the native code for BinaryDictionary
protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
- final int depth, boolean completion, int snr, int inputIndex, int skipPos,
- WordCallback callback) {
+ final int depth, final boolean completion, int snr, int inputIndex, int skipPos,
+ final ArrayList<SuggestedWordInfo> suggestions) {
final int count = roots.mLength;
final int codeSize = mInputLength;
// Optimization: Prune out words that are too long compared to how much was typed.
if (depth > mMaxDepth) {
return;
}
- int[] currentChars = null;
+ final int[] currentChars;
if (codeSize <= inputIndex) {
- completion = true;
+ currentChars = null;
} else {
currentChars = mCodes[inputIndex];
}
@@ -287,7 +441,7 @@ public class ExpandableDictionary extends Dictionary {
final boolean terminal = node.mTerminal;
final NodeArray children = node.mChildren;
final int freq = node.mFrequency;
- if (completion) {
+ if (completion || currentChars == null) {
word[depth] = c;
if (terminal) {
final int finalFreq;
@@ -296,14 +450,14 @@ public class ExpandableDictionary extends Dictionary {
} else {
finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
}
- if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
- DataType.UNIGRAM)) {
+ if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) {
+ // No space left in the queue, bail out
return;
}
}
if (children != null) {
- getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
- skipPos, callback);
+ getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex,
+ skipPos, suggestions);
}
} else if ((c == Keyboard.CODE_SINGLE_QUOTE
&& currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) {
@@ -311,15 +465,15 @@ public class ExpandableDictionary extends Dictionary {
word[depth] = c;
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
- skipPos, callback);
+ skipPos, suggestions);
}
} else {
// Don't use alternatives if we're looking for missing characters
- final int alternativesSize = skipPos >= 0? 1 : currentChars.length;
+ final int alternativesSize = skipPos >= 0 ? 1 : currentChars.length;
for (int j = 0; j < alternativesSize; j++) {
final int addedAttenuation = (j > 0 ? 1 : 2);
final int currentChar = currentChars[j];
- if (currentChar == -1) {
+ if (currentChar == KeyDetector.NOT_A_CODE) {
break;
}
if (currentChar == lowerC || currentChar == c) {
@@ -327,29 +481,29 @@ public class ExpandableDictionary extends Dictionary {
if (codeSize == inputIndex + 1) {
if (terminal) {
- if (INCLUDE_TYPED_WORD_IF_VALID
- || !same(word, depth + 1, codes.getTypedWord())) {
- final int finalFreq;
- if (skipPos < 0) {
- finalFreq = freq * snr * addedAttenuation
- * FULL_WORD_SCORE_MULTIPLIER;
- } else {
- finalFreq = computeSkippedWordFinalFreq(freq,
- snr * addedAttenuation, mInputLength);
- }
- callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
- DataType.UNIGRAM);
+ final int finalFreq;
+ if (skipPos < 0) {
+ finalFreq = freq * snr * addedAttenuation
+ * FULL_WORD_SCORE_MULTIPLIER;
+ } else {
+ finalFreq = computeSkippedWordFinalFreq(freq,
+ snr * addedAttenuation, mInputLength);
+ }
+ if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
+ suggestions)) {
+ // No space left in the queue, bail out
+ return;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1,
true, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
} else if (children != null) {
getWordsRec(children, codes, word, depth + 1,
false, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
}
}
@@ -357,43 +511,47 @@ public class ExpandableDictionary extends Dictionary {
}
}
- protected int setBigram(String word1, String word2, int frequency) {
- return addOrSetBigram(word1, word2, frequency, false);
+ public int setBigramAndGetFrequency(String word1, String word2, int frequency) {
+ return setBigramAndGetFrequency(word1, word2, frequency, null /* unused */);
}
- protected int addBigram(String word1, String word2, int frequency) {
- return addOrSetBigram(word1, word2, frequency, true);
+ public int setBigramAndGetFrequency(String word1, String word2, ForgettingCurveParams fcp) {
+ return setBigramAndGetFrequency(word1, word2, 0 /* unused */, fcp);
}
/**
* Adds bigrams to the in-memory trie structure that is being used to retrieve any word
+ * @param word1 the first word of this bigram
+ * @param word2 the second word of this bigram
* @param frequency frequency for this bigram
- * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
- * @return returns the final frequency
+ * @param fcp an instance of ForgettingCurveParams to use for decay policy
+ * @return returns the final bigram frequency
*/
- private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) {
+ private int setBigramAndGetFrequency(
+ String word1, String word2, int frequency, ForgettingCurveParams fcp) {
// We don't want results to be different according to case of the looked up left hand side
// word. We do want however to return the correct case for the right hand side.
// So we want to squash the case of the left hand side, and preserve that of the right
// hand side word.
Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
Node secondWord = searchWord(mRoots, word2, 0, null);
- LinkedList<NextWord> bigram = firstWord.mNGrams;
- if (bigram == null || bigram.size() == 0) {
+ LinkedList<NextWord> bigrams = firstWord.mNGrams;
+ if (bigrams == null || bigrams.size() == 0) {
firstWord.mNGrams = new LinkedList<NextWord>();
- bigram = firstWord.mNGrams;
+ bigrams = firstWord.mNGrams;
} else {
- for (NextWord nw : bigram) {
- if (nw.mWord == secondWord) {
- if (addFrequency) {
- return nw.addFrequency(frequency);
- } else {
- return nw.setFrequency(frequency);
- }
+ for (NextWord nw : bigrams) {
+ if (nw.getWordNode() == secondWord) {
+ return nw.notifyTypedAgainAndGetFrequency();
}
}
}
- firstWord.mNGrams.add(new NextWord(secondWord, frequency));
+ if (fcp != null) {
+ // history
+ firstWord.mNGrams.add(new NextHistoryWord(secondWord, fcp));
+ } else {
+ firstWord.mNGrams.add(new NextStaticWord(secondWord, frequency));
+ }
return frequency;
}
@@ -407,15 +565,14 @@ public class ExpandableDictionary extends Dictionary {
// Does children have the current character?
final int childrenLength = children.mLength;
Node childNode = null;
- boolean found = false;
for (int i = 0; i < childrenLength; i++) {
- childNode = children.mData[i];
- if (childNode.mCode == c) {
- found = true;
+ final Node node = children.mData[i];
+ if (node.mCode == c) {
+ childNode = node;
break;
}
}
- if (!found) {
+ if (childNode == null) {
childNode = new Node();
childNode.mCode = c;
childNode.mParent = parentNode;
@@ -462,7 +619,7 @@ public class ExpandableDictionary extends Dictionary {
}
/**
- * Used only for testing purposes
+ * Used for testing purposes and in the spell checker
* This function will wait for loading from database to be done
*/
void waitForDictionaryLoading() {
@@ -475,8 +632,13 @@ public class ExpandableDictionary extends Dictionary {
}
}
+ protected final void blockingReloadDictionaryIfRequired() {
+ reloadDictionaryIfRequired();
+ waitForDictionaryLoading();
+ }
+
// Local to reverseLookUp, but do not allocate each time.
- private final char[] mLookedUpString = new char[MAX_WORD_LENGTH];
+ private final char[] mLookedUpString = new char[BinaryDictionary.MAX_WORD_LENGTH];
/**
* reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
@@ -488,17 +650,19 @@ public class ExpandableDictionary extends Dictionary {
Node node;
int freq;
for (NextWord nextWord : terminalNodes) {
- node = nextWord.mWord;
+ node = nextWord.getWordNode();
freq = nextWord.getFrequency();
- int index = MAX_WORD_LENGTH;
+ int index = BinaryDictionary.MAX_WORD_LENGTH;
do {
--index;
mLookedUpString[index] = node.mCode;
node = node.mParent;
} while (node != null);
- callback.addWord(mLookedUpString, index, MAX_WORD_LENGTH - index, freq, mDicTypeId,
- DataType.BIGRAM);
+ if (freq >= 0) {
+ callback.addWord(mLookedUpString, index, BinaryDictionary.MAX_WORD_LENGTH - index,
+ freq, mDicTypeId, Dictionary.BIGRAM);
+ }
}
}
@@ -539,14 +703,14 @@ public class ExpandableDictionary extends Dictionary {
mRoots = new NodeArray();
}
- private class LoadDictionaryTask extends AsyncTask<Void, Void, Void> {
+ private class LoadDictionaryTask extends Thread {
+ LoadDictionaryTask() {}
@Override
- protected Void doInBackground(Void... v) {
+ public void run() {
loadDictionaryAsync();
synchronized (mUpdatingLock) {
mUpdatingDictionary = false;
}
- return null;
}
}
@@ -570,167 +734,167 @@ public class ExpandableDictionary extends Dictionary {
* is combined.
*/
private static final char BASE_CHARS[] = {
- 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
- 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
- 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
- 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
- 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
- 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
- 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
- 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
- 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
- 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
- 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
- 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
- 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
- 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
- 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
- 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
- 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
- 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
- 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
- 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
- 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
- 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020,
- 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
- 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
- 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043,
- 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
- 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7,
+ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
+ 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
+ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
+ 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
+ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
+ 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
+ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
+ 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
+ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
+ 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
+ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
+ 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
+ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
+ 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
+ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
+ 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
+ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
+ 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
+ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
+ 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
+ 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
+ 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020,
+ 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
+ 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
+ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043,
+ 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
+ 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7,
0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f
// Manually changed df to 73
- 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
- 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
- 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
+ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
+ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
+ 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f
- 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
- 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
- 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
- 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
- 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
- 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
- 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b,
- 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c,
- 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e,
- 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f,
- 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
- 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
- 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
- 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
- 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
- 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073,
- 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
- 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f,
- 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
- 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f,
- 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7,
- 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055,
- 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7,
- 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf,
- 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c,
- 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049,
- 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc,
- 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4,
- 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067,
- 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292,
- 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7,
- 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8,
- 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
- 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f,
- 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
- 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068,
- 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
- 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f,
- 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
- 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f,
- 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
- 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f,
- 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
- 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
- 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
- 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
- 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
- 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
- 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
- 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
- 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
- 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
- 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
- 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
- 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
- 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
- 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
- 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
- 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
- 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
- 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
- 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
- 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
- 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
- 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
- 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f,
- 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
- 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f,
- 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
- 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f,
- 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
- 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f,
- 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
- 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f,
- 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
- 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
- 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
- 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
- 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377,
- 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
- 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7,
- 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9,
- 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
- 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f,
- 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7,
- 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
- 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
- 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
- 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
- 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf,
- 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7,
- 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df,
- 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7,
- 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef,
- 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7,
- 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff,
- 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
- 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f,
- 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
- 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f,
- 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
- 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
- 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
- 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
- 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
- 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
- 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
- 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
- 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
- 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f,
- 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
- 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f,
- 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
- 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f,
- 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
- 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f,
- 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7,
- 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af,
- 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7,
- 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf,
- 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7,
- 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf,
- 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435,
- 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437,
- 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e,
- 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443,
- 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
- 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
+ 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
+ 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
+ 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
+ 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
+ 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
+ 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
+ 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b,
+ 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c,
+ 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e,
+ 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f,
+ 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
+ 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
+ 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
+ 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
+ 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
+ 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073,
+ 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
+ 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f,
+ 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
+ 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f,
+ 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7,
+ 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055,
+ 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7,
+ 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf,
+ 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c,
+ 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049,
+ 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc,
+ 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4,
+ 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067,
+ 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292,
+ 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7,
+ 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8,
+ 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
+ 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f,
+ 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
+ 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068,
+ 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
+ 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f,
+ 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
+ 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f,
+ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
+ 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f,
+ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
+ 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
+ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
+ 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
+ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
+ 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
+ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
+ 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
+ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
+ 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
+ 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
+ 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
+ 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
+ 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
+ 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
+ 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
+ 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
+ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
+ 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
+ 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
+ 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
+ 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
+ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
+ 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f,
+ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
+ 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f,
+ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
+ 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f,
+ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
+ 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f,
+ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
+ 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f,
+ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
+ 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
+ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
+ 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
+ 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377,
+ 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
+ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7,
+ 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9,
+ 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
+ 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f,
+ 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7,
+ 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
+ 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
+ 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
+ 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
+ 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf,
+ 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7,
+ 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df,
+ 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7,
+ 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef,
+ 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7,
+ 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff,
+ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
+ 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f,
+ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
+ 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f,
+ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
+ 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
+ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
+ 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
+ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
+ 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
+ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
+ 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
+ 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
+ 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f,
+ 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
+ 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f,
+ 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
+ 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f,
+ 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
+ 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f,
+ 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7,
+ 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af,
+ 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7,
+ 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf,
+ 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7,
+ 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf,
+ 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435,
+ 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437,
+ 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e,
+ 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443,
+ 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
+ 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
};
// generated with:
diff --git a/java/src/com/android/inputmethod/latin/FileTransforms.java b/java/src/com/android/inputmethod/latin/FileTransforms.java
new file mode 100644
index 000000000..80159521c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/FileTransforms.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.zip.GZIPInputStream;
+
+public class FileTransforms {
+ public static OutputStream getCryptedStream(OutputStream out) {
+ // Crypt the stream.
+ return out;
+ }
+
+ public static InputStream getDecryptedStream(InputStream in) {
+ // Decrypt the stream.
+ return in;
+ }
+
+ public static InputStream getUncompressedStream(InputStream in) throws IOException {
+ return new GZIPInputStream(in);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Flag.java b/java/src/com/android/inputmethod/latin/Flag.java
deleted file mode 100644
index 3cb8f7e17..000000000
--- a/java/src/com/android/inputmethod/latin/Flag.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-public class Flag {
- public final String mName;
- public final int mResource;
- public final int mMask;
- public final int mSource;
-
- static private final int SOURCE_CONFIG = 1;
- static private final int SOURCE_EXTRAVALUE = 2;
-
- public Flag(int resourceId, int mask) {
- mName = null;
- mResource = resourceId;
- mSource = SOURCE_CONFIG;
- mMask = mask;
- }
-
- public Flag(String name, int mask) {
- mName = name;
- mResource = 0;
- mSource = SOURCE_EXTRAVALUE;
- mMask = mask;
- }
-
- // If context/switcher are null, set all related flags in flagArray to on.
- public static int initFlags(Flag[] flagArray, Context context, SubtypeSwitcher switcher) {
- int flags = 0;
- final Resources res = null == context ? null : context.getResources();
- for (Flag entry : flagArray) {
- switch (entry.mSource) {
- case Flag.SOURCE_CONFIG:
- if (res == null || res.getBoolean(entry.mResource))
- flags |= entry.mMask;
- break;
- case Flag.SOURCE_EXTRAVALUE:
- if (switcher == null ||
- switcher.currentSubtypeContainsExtraValueKey(entry.mName))
- flags |= entry.mMask;
- break;
- }
- }
- return flags;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/ImfUtils.java b/java/src/com/android/inputmethod/latin/ImfUtils.java
new file mode 100644
index 000000000..b882a4860
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ImfUtils.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+
+import android.content.Context;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class for Input Method Framework
+ */
+public class ImfUtils {
+ private ImfUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static InputMethodManager sInputMethodManager;
+
+ public static InputMethodManager getInputMethodManager(Context context) {
+ if (sInputMethodManager == null) {
+ sInputMethodManager = (InputMethodManager)context.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ }
+ return sInputMethodManager;
+ }
+
+ private static InputMethodInfo sInputMethodInfoOfThisIme;
+
+ public static InputMethodInfo getInputMethodInfoOfThisIme(Context context) {
+ if (sInputMethodInfoOfThisIme == null) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final String packageName = context.getPackageName();
+ for (final InputMethodInfo imi : imm.getInputMethodList()) {
+ if (imi.getPackageName().equals(packageName))
+ return imi;
+ }
+ throw new RuntimeException("Can not find input method id for " + packageName);
+ }
+ return sInputMethodInfoOfThisIme;
+ }
+
+ public static String getInputMethodIdOfThisIme(Context context) {
+ return getInputMethodInfoOfThisIme(context).getId();
+ }
+
+ public static boolean checkIfSubtypeBelongsToThisImeAndEnabled(Context context,
+ InputMethodSubtype ims) {
+ final InputMethodInfo myImi = getInputMethodInfoOfThisIme(context);
+ final InputMethodManager imm = getInputMethodManager(context);
+ // TODO: Cache all subtypes of this IME for optimization
+ final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(myImi, true);
+ for (final InputMethodSubtype subtype : subtypes) {
+ if (subtype.equals(ims)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean checkIfSubtypeBelongsToThisIme(Context context,
+ InputMethodSubtype ims) {
+ final InputMethodInfo myImi = getInputMethodInfoOfThisIme(context);
+ final int count = myImi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
+ if (subtype.equals(ims)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean hasMultipleEnabledIMEsOrSubtypes(Context context,
+ final boolean shouldIncludeAuxiliarySubtypes) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
+ return hasMultipleEnabledSubtypes(context, shouldIncludeAuxiliarySubtypes, enabledImis);
+ }
+
+ public static boolean hasMultipleEnabledSubtypesInThisIme(Context context,
+ final boolean shouldIncludeAuxiliarySubtypes) {
+ final InputMethodInfo myImi = getInputMethodInfoOfThisIme(context);
+ final List<InputMethodInfo> imiList = Collections.singletonList(myImi);
+ return hasMultipleEnabledSubtypes(context, shouldIncludeAuxiliarySubtypes, imiList);
+ }
+
+ private static boolean hasMultipleEnabledSubtypes(Context context,
+ final boolean shouldIncludeAuxiliarySubtypes, List<InputMethodInfo> imiList) {
+ final InputMethodManager imm = getInputMethodManager(context);
+
+ // Number of the filtered IMEs
+ int filteredImisCount = 0;
+
+ for (InputMethodInfo imi : imiList) {
+ // We can return true immediately after we find two or more filtered IMEs.
+ if (filteredImisCount > 1) return true;
+ final List<InputMethodSubtype> subtypes =
+ imm.getEnabledInputMethodSubtypeList(imi, true);
+ // IMEs that have no subtypes should be counted.
+ if (subtypes.isEmpty()) {
+ ++filteredImisCount;
+ continue;
+ }
+
+ int auxCount = 0;
+ for (InputMethodSubtype subtype : subtypes) {
+ if (subtype.isAuxiliary()) {
+ ++auxCount;
+ }
+ }
+ final int nonAuxCount = subtypes.size() - auxCount;
+
+ // IMEs that have one or more non-auxiliary subtypes should be counted.
+ // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
+ // subtypes should be counted as well.
+ if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
+ ++filteredImisCount;
+ continue;
+ }
+ }
+
+ if (filteredImisCount > 1) {
+ return true;
+ }
+ final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(null, true);
+ int keyboardCount = 0;
+ // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
+ // both explicitly and implicitly enabled input method subtype.
+ // (The current IME should be LatinIME.)
+ for (InputMethodSubtype subtype : subtypes) {
+ if (KEYBOARD_MODE.equals(subtype.getMode())) {
+ ++keyboardCount;
+ }
+ }
+ return keyboardCount > 1;
+ }
+
+ public static InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(
+ Context context, String localeString, String keyboardLayoutSetName) {
+ final InputMethodInfo imi = getInputMethodInfoOfThisIme(context);
+ final int count = imi.getSubtypeCount();
+ for (int i = 0; i < count; i++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(i);
+ final String layoutName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ if (localeString.equals(subtype.getLocale())
+ && keyboardLayoutSetName.equals(layoutName)) {
+ return subtype;
+ }
+ }
+ return null;
+ }
+
+ public static void setAdditionalInputMethodSubtypes(Context context,
+ InputMethodSubtype[] subtypes) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final String imiId = getInputMethodIdOfThisIme(context);
+ imm.setAdditionalInputMethodSubtypes(imiId, subtypes);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
new file mode 100644
index 000000000..229ae2f3c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.text.InputType;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+
+/**
+ * Class to hold attributes of the input field.
+ */
+public class InputAttributes {
+ private final String TAG = InputAttributes.class.getSimpleName();
+
+ final public boolean mInputTypeNoAutoCorrect;
+ final public boolean mIsSettingsSuggestionStripOn;
+ final public boolean mApplicationSpecifiedCompletionOn;
+ final public int mEditorAction;
+
+ public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
+ final int inputType = null != editorInfo ? editorInfo.inputType : 0;
+ final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
+ if (inputClass != InputType.TYPE_CLASS_TEXT) {
+ // If we are not looking at a TYPE_CLASS_TEXT field, the following strange
+ // cases may arise, so we do a couple sanity checks for them. If it's a
+ // TYPE_CLASS_TEXT field, these special cases cannot happen, by construction
+ // of the flags.
+ if (null == editorInfo) {
+ Log.w(TAG, "No editor info for this field. Bug?");
+ } else if (InputType.TYPE_NULL == inputType) {
+ // TODO: We should honor TYPE_NULL specification.
+ Log.i(TAG, "InputType.TYPE_NULL is specified");
+ } else if (inputClass == 0) {
+ // TODO: is this check still necessary?
+ Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x"
+ + " imeOptions=0x%08x",
+ inputType, editorInfo.imeOptions));
+ }
+ mIsSettingsSuggestionStripOn = false;
+ mInputTypeNoAutoCorrect = false;
+ mApplicationSpecifiedCompletionOn = false;
+ } else {
+ final int variation = inputType & InputType.TYPE_MASK_VARIATION;
+ final boolean flagNoSuggestions =
+ 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ final boolean flagMultiLine =
+ 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ final boolean flagAutoCorrect =
+ 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
+ final boolean flagAutoComplete =
+ 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
+
+ // Make sure that passwords are not displayed in {@link SuggestionsView}.
+ if (InputTypeUtils.isPasswordInputType(inputType)
+ || InputTypeUtils.isVisiblePasswordInputType(inputType)
+ || InputTypeUtils.isEmailVariation(variation)
+ || InputType.TYPE_TEXT_VARIATION_URI == variation
+ || InputType.TYPE_TEXT_VARIATION_FILTER == variation
+ || flagNoSuggestions
+ || flagAutoComplete) {
+ mIsSettingsSuggestionStripOn = false;
+ } else {
+ mIsSettingsSuggestionStripOn = true;
+ }
+
+ // If it's a browser edit field and auto correct is not ON explicitly, then
+ // disable auto correction, but keep suggestions on.
+ // If NO_SUGGESTIONS is set, don't do prediction.
+ // If it's not multiline and the autoCorrect flag is not set, then don't correct
+ if ((variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
+ && !flagAutoCorrect)
+ || flagNoSuggestions
+ || (!flagAutoCorrect && !flagMultiLine)) {
+ mInputTypeNoAutoCorrect = true;
+ } else {
+ mInputTypeNoAutoCorrect = false;
+ }
+
+ mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode;
+ }
+ mEditorAction = (editorInfo == null) ? EditorInfo.IME_ACTION_UNSPECIFIED
+ : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ }
+
+ @SuppressWarnings("unused")
+ private void dumpFlags(final int inputType) {
+ Log.i(TAG, "Input class:");
+ final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
+ if (inputClass == InputType.TYPE_CLASS_TEXT)
+ Log.i(TAG, " TYPE_CLASS_TEXT");
+ if (inputClass == InputType.TYPE_CLASS_PHONE)
+ Log.i(TAG, " TYPE_CLASS_PHONE");
+ if (inputClass == InputType.TYPE_CLASS_NUMBER)
+ Log.i(TAG, " TYPE_CLASS_NUMBER");
+ if (inputClass == InputType.TYPE_CLASS_DATETIME)
+ Log.i(TAG, " TYPE_CLASS_DATETIME");
+ Log.i(TAG, "Variation:");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_ADDRESS");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_SUBJECT");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_FILTER))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_FILTER");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_LONG_MESSAGE");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_NORMAL))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_NORMAL");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_PASSWORD");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_PERSON_NAME))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_PERSON_NAME");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_PHONETIC))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_PHONETIC");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_POSTAL_ADDRESS");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_SHORT_MESSAGE");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_URI))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_URI");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_VISIBLE_PASSWORD");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EDIT_TEXT");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS");
+ if (0 != (inputType & InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD))
+ Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_PASSWORD");
+ Log.i(TAG, "Flags:");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS))
+ Log.i(TAG, " TYPE_TEXT_FLAG_NO_SUGGESTIONS");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE))
+ Log.i(TAG, " TYPE_TEXT_FLAG_MULTI_LINE");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE))
+ Log.i(TAG, " TYPE_TEXT_FLAG_IME_MULTI_LINE");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS))
+ Log.i(TAG, " TYPE_TEXT_FLAG_CAP_WORDS");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES))
+ Log.i(TAG, " TYPE_TEXT_FLAG_CAP_SENTENCES");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS))
+ Log.i(TAG, " TYPE_TEXT_FLAG_CAP_CHARACTERS");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT))
+ Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_CORRECT");
+ if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE))
+ Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_COMPLETE");
+ }
+
+ // Pretty print
+ @Override
+ public String toString() {
+ return "\n mInputTypeNoAutoCorrect = " + mInputTypeNoAutoCorrect
+ + "\n mIsSettingsSuggestionStripOn = " + mIsSettingsSuggestionStripOn
+ + "\n mApplicationSpecifiedCompletionOn = " + mApplicationSpecifiedCompletionOn;
+ }
+
+ public static boolean inPrivateImeOptions(String packageName, String key,
+ EditorInfo editorInfo) {
+ if (editorInfo == null) return false;
+ final String findingKey = (packageName != null) ? packageName + "." + key
+ : key;
+ return StringUtils.containsInCsv(findingKey, editorInfo.privateImeOptions);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
new file mode 100644
index 000000000..40c3b765e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+import android.text.InputType;
+
+public class InputTypeUtils implements InputType {
+ private static final int WEB_TEXT_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD;
+ private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ private static final int NUMBER_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD;
+ private static final int TEXT_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD;
+ private static final int TEXT_VISIBLE_PASSWORD_INPUT_TYPE =
+ TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
+
+ private InputTypeUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static boolean isWebEditTextInputType(int inputType) {
+ return inputType == (TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+ }
+
+ private static boolean isWebPasswordInputType(int inputType) {
+ return WEB_TEXT_PASSWORD_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressInputType(int inputType) {
+ return WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE != 0
+ && inputType == WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE;
+ }
+
+ private static boolean isNumberPasswordInputType(int inputType) {
+ return NUMBER_PASSWORD_INPUT_TYPE != 0
+ && inputType == NUMBER_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isTextPasswordInputType(int inputType) {
+ return inputType == TEXT_PASSWORD_INPUT_TYPE;
+ }
+
+ private static boolean isWebEmailAddressVariation(int variation) {
+ return variation == TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
+ }
+
+ public static boolean isEmailVariation(int variation) {
+ return variation == TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+ || isWebEmailAddressVariation(variation);
+ }
+
+ public static boolean isWebInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return isWebEditTextInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isWebEmailAddressInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isPasswordInputType
+ public static boolean isPasswordInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return isTextPasswordInputType(maskedInputType) || isWebPasswordInputType(maskedInputType)
+ || isNumberPasswordInputType(maskedInputType);
+ }
+
+ // Please refer to TextView.isVisiblePasswordInputType
+ public static boolean isVisiblePasswordInputType(int inputType) {
+ final int maskedInputType =
+ inputType & (TYPE_MASK_CLASS | TYPE_MASK_VARIATION);
+ return maskedInputType == TEXT_VISIBLE_PASSWORD_INPUT_TYPE;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
new file mode 100644
index 000000000..0dcb811b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+
+public class InputView extends LinearLayout {
+ private View mSuggestionsContainer;
+ private View mKeyboardView;
+ private int mKeyboardTopPadding;
+
+ private boolean mIsForwardingEvent;
+ private final Rect mInputViewRect = new Rect();
+ private final Rect mEventForwardingRect = new Rect();
+ private final Rect mEventReceivingRect = new Rect();
+
+ public InputView(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ }
+
+ public void setKeyboardGeometry(int keyboardTopPadding) {
+ mKeyboardTopPadding = keyboardTopPadding;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mSuggestionsContainer = findViewById(R.id.suggestions_container);
+ mKeyboardView = findViewById(R.id.keyboard_view);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent me) {
+ if (mSuggestionsContainer.getVisibility() == VISIBLE
+ && mKeyboardView.getVisibility() == VISIBLE
+ && forwardTouchEvent(me)) {
+ return true;
+ }
+ return super.dispatchTouchEvent(me);
+ }
+
+ // The touch events that hit the top padding of keyboard should be forwarded to SuggestionsView.
+ private boolean forwardTouchEvent(MotionEvent me) {
+ final Rect rect = mInputViewRect;
+ this.getGlobalVisibleRect(rect);
+ final int x = (int)me.getX() + rect.left;
+ final int y = (int)me.getY() + rect.top;
+
+ final Rect forwardingRect = mEventForwardingRect;
+ mKeyboardView.getGlobalVisibleRect(forwardingRect);
+ if (!mIsForwardingEvent && !forwardingRect.contains(x, y)) {
+ return false;
+ }
+
+ final int forwardingLimitY = forwardingRect.top + mKeyboardTopPadding;
+ boolean sendToTarget = false;
+
+ switch (me.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (y < forwardingLimitY) {
+ // This down event and further move and up events should be forwarded to the target.
+ mIsForwardingEvent = true;
+ sendToTarget = true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ sendToTarget = mIsForwardingEvent;
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ sendToTarget = mIsForwardingEvent;
+ mIsForwardingEvent = false;
+ break;
+ }
+
+ if (!sendToTarget) {
+ return false;
+ }
+
+ final Rect receivingRect = mEventReceivingRect;
+ mSuggestionsContainer.getGlobalVisibleRect(receivingRect);
+ final int translatedX = x - receivingRect.left;
+ final int translatedY;
+ if (y < forwardingLimitY) {
+ // The forwarded event should have coordinates that are inside of the target.
+ translatedY = Math.min(y - receivingRect.top, receivingRect.height() - 1);
+ } else {
+ translatedY = y - receivingRect.top;
+ }
+ me.setLocation(translatedX, translatedY);
+ mSuggestionsContainer.dispatchTouchEvent(me);
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java
new file mode 100644
index 000000000..4808b867a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/JniUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+import android.util.Log;
+
+import com.android.inputmethod.latin.define.JniLibName;
+
+public class JniUtils {
+ private static final String TAG = JniUtils.class.getSimpleName();
+
+ private JniUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void loadNativeLibrary() {
+ try {
+ System.loadLibrary(JniLibName.JNI_LIB_NAME);
+ } catch (UnsatisfiedLinkError ule) {
+ Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME);
+ if (LatinImeLogger.sDBG) {
+ throw new RuntimeException(
+ "Could not load native library " + JniLibName.JNI_LIB_NAME);
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
new file mode 100644
index 000000000..4e1f5fe92
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+
+/**
+ * This class encapsulates data about a word previously composed, but that has been
+ * committed already. This is used for resuming suggestion, and cancel auto-correction.
+ */
+public class LastComposedWord {
+ // COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with
+ // no hinting from the IME. It happens when some external event happens (rotating the device,
+ // for example) or when auto-correction is off by settings or editor attributes.
+ public static final int COMMIT_TYPE_USER_TYPED_WORD = 0;
+ // COMMIT_TYPE_MANUAL_PICK is used when the user pressed a field in the suggestion strip.
+ public static final int COMMIT_TYPE_MANUAL_PICK = 1;
+ // COMMIT_TYPE_DECIDED_WORD is used when the IME commits the word it decided was best
+ // for the current user input. It may be different from what the user typed (true auto-correct)
+ // or it may be exactly what the user typed if it's in the dictionary or the IME does not have
+ // enough confidence in any suggestion to auto-correct (auto-correct to typed word).
+ public static final int COMMIT_TYPE_DECIDED_WORD = 2;
+ // COMMIT_TYPE_CANCEL_AUTO_CORRECT is used upon committing back the old word upon cancelling
+ // an auto-correction.
+ public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
+
+ public static final int NOT_A_SEPARATOR = -1;
+
+ public final int[] mPrimaryKeyCodes;
+ public final int[] mXCoordinates;
+ public final int[] mYCoordinates;
+ public final String mTypedWord;
+ public final String mCommittedWord;
+ public final int mSeparatorCode;
+ public final CharSequence mPrevWord;
+
+ private boolean mActive;
+
+ public static final LastComposedWord NOT_A_COMPOSED_WORD =
+ new LastComposedWord(null, null, null, "", "", NOT_A_SEPARATOR, null);
+
+ // Warning: this is using the passed objects as is and fully expects them to be
+ // immutable. Do not fiddle with their contents after you passed them to this constructor.
+ public LastComposedWord(final int[] primaryKeyCodes, final int[] xCoordinates,
+ final int[] yCoordinates, final String typedWord, final String committedWord,
+ final int separatorCode, final CharSequence prevWord) {
+ mPrimaryKeyCodes = primaryKeyCodes;
+ mXCoordinates = xCoordinates;
+ mYCoordinates = yCoordinates;
+ mTypedWord = typedWord;
+ mCommittedWord = committedWord;
+ mSeparatorCode = separatorCode;
+ mActive = true;
+ mPrevWord = prevWord;
+ }
+
+ public void deactivate() {
+ mActive = false;
+ }
+
+ public boolean canRevertCommit() {
+ return mActive && !TextUtils.isEmpty(mCommittedWord);
+ }
+
+ public boolean didCommitTypedWord() {
+ return TextUtils.equals(mTypedWord, mCommittedWord);
+ }
+
+ public static int getSeparatorLength(final int separatorCode) {
+ return NOT_A_SEPARATOR == separatorCode ? 0 : 1;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 874d77f19..8a5fc495e 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -16,6 +16,10 @@
package com.android.inputmethod.latin;
+import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
+import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
+import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -23,8 +27,10 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.net.ConnectivityManager;
@@ -36,77 +42,51 @@ import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.util.PrintWriterPrinter;
import android.util.Printer;
-import android.view.HapticFeedbackConstants;
+import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.compat.CompatUtils;
-import com.android.inputmethod.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.compat.InputConnectionCompatUtils;
import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
-import com.android.inputmethod.compat.InputTypeCompatUtils;
import com.android.inputmethod.compat.SuggestionSpanUtils;
-import com.android.inputmethod.deprecated.LanguageSwitcherProxy;
-import com.android.inputmethod.deprecated.VoiceProxy;
-import com.android.inputmethod.deprecated.recorrection.Recorrection;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
+import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.keyboard.LatinKeyboard;
import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.suggestions.SuggestionsView;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Locale;
/**
* Input method implementation for Qwerty'ish keyboard.
*/
-public class LatinIME extends InputMethodServiceCompatWrapper implements KeyboardActionListener,
- CandidateView.Listener {
+public class LatinIME extends InputMethodService implements KeyboardActionListener,
+ SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
private static final String TAG = LatinIME.class.getSimpleName();
- private static final boolean PERF_DEBUG = false;
private static final boolean TRACE = false;
private static boolean DEBUG;
- /**
- * The private IME option used to indicate that no microphone should be
- * shown for a given text field. For instance, this is specified by the
- * search dialog when the dialog is already showing a voice search button.
- *
- * @deprecated Use {@link LatinIME#IME_OPTION_NO_MICROPHONE} with package name prefixed.
- */
- @SuppressWarnings("dep-ann")
- public static final String IME_OPTION_NO_MICROPHONE_COMPAT = "nm";
-
- /**
- * The private IME option used to indicate that no microphone should be
- * shown for a given text field. For instance, this is specified by the
- * search dialog when the dialog is already showing a voice search button.
- */
- public static final String IME_OPTION_NO_MICROPHONE = "noMicrophoneKey";
-
- /**
- * The private IME option used to indicate that no settings key should be
- * shown for a given text field.
- */
- public static final String IME_OPTION_NO_SETTINGS_KEY = "noSettingsKey";
-
private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
// How many continuous deletes at which to start deleting at a higher speed.
@@ -114,74 +94,63 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
// Key events coming any faster than this are long-presses.
private static final int QUICK_PRESS = 200;
+ private static final int PENDING_IMS_CALLBACK_DURATION = 800;
+
/**
* The name of the scheme used by the Package Manager to warn of a new package installation,
* replacement or removal.
*/
private static final String SCHEME_PACKAGE = "package";
- private int mSuggestionVisibility;
- private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
- = R.string.prefs_suggestion_visibility_show_value;
- private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- = R.string.prefs_suggestion_visibility_show_only_portrait_value;
- private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
- = R.string.prefs_suggestion_visibility_hide_value;
-
- private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
- SUGGESTION_VISIBILILTY_SHOW_VALUE,
- SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
- SUGGESTION_VISIBILILTY_HIDE_VALUE
- };
-
- private Settings.Values mSettingsValues;
-
- private View mCandidateViewContainer;
- private int mCandidateStripHeight;
- private CandidateView mCandidateView;
- private Suggest mSuggest;
+ private static final int SPACE_STATE_NONE = 0;
+ // Double space: the state where the user pressed space twice quickly, which LatinIME
+ // resolved as period-space. Undoing this converts the period to a space.
+ private static final int SPACE_STATE_DOUBLE = 1;
+ // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
+ // have just been swapped. Undoing this swaps them back; the space is still considered weak.
+ private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
+ // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
+ // spaces happen when the user presses space, accepting the current suggestion (whether
+ // it's an auto-correction or not).
+ private static final int SPACE_STATE_WEAK = 3;
+ // Phantom space: a not-yet-inserted space that should get inserted on the next input,
+ // character provided it's not a separator. If it's a separator, the phantom space is dropped.
+ // Phantom spaces happen when a user chooses a word from the suggestion strip.
+ private static final int SPACE_STATE_PHANTOM = 4;
+
+ // Current space state of the input method. This can be any of the above constants.
+ private int mSpaceState;
+
+ private SettingsValues mCurrentSettings;
+
+ private View mExtractArea;
+ private View mKeyPreviewBackingView;
+ private View mSuggestionsContainer;
+ private SuggestionsView mSuggestionsView;
+ /* package for tests */ Suggest mSuggest;
private CompletionInfo[] mApplicationSpecifiedCompletions;
-
- private AlertDialog mOptionsDialog;
+ private ApplicationInfo mTargetApplicationInfo;
private InputMethodManagerCompatWrapper mImm;
private Resources mResources;
private SharedPreferences mPrefs;
- private String mInputMethodId;
- private KeyboardSwitcher mKeyboardSwitcher;
- private SubtypeSwitcher mSubtypeSwitcher;
- private VoiceProxy mVoiceProxy;
- private Recorrection mRecorrection;
-
- private UserDictionary mUserDictionary;
- private UserBigramDictionary mUserBigramDictionary;
- private AutoDictionary mAutoDictionary;
-
- // TODO: Create an inner class to group options and pseudo-options to improve readability.
- // These variables are initialized according to the {@link EditorInfo#inputType}.
- private boolean mShouldInsertMagicSpace;
- private boolean mInputTypeNoAutoCorrect;
- private boolean mIsSettingsSuggestionStripOn;
- private boolean mApplicationSpecifiedCompletionOn;
-
- private final StringBuilder mComposing = new StringBuilder();
- private WordComposer mWord = new WordComposer();
- private CharSequence mBestWord;
- private boolean mHasUncommittedTypedChars;
- private boolean mHasDictionary;
- // Magic space: a space that should disappear on space/apostrophe insertion, move after the
- // punctuation on punctuation insertion, and become a real space on alpha char insertion.
- private boolean mJustAddedMagicSpace; // This indicates whether the last char is a magic space.
- // This indicates whether the last keypress resulted in processing of double space replacement
- // with period-space.
- private boolean mJustReplacedDoubleSpace;
-
- private int mCorrectionMode;
- private int mCommittedLength;
- private int mOrientation;
+ /* package for tests */ final KeyboardSwitcher mKeyboardSwitcher;
+ private final SubtypeSwitcher mSubtypeSwitcher;
+ private boolean mShouldSwitchToLastSubtype = true;
+
+ private boolean mIsMainDictionaryAvailable;
+ private UserBinaryDictionary mUserDictionary;
+ private UserHistoryDictionary mUserHistoryDictionary;
+ private boolean mIsUserDictionaryAvailable;
+
+ private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ private WordComposer mWordComposer = new WordComposer();
+ private RichInputConnection mConnection = new RichInputConnection();
+
// Keep track of the last selection range to decide if we need to show word alternatives
- private int mLastSelectionStart;
- private int mLastSelectionEnd;
+ private static final int NOT_A_CURSOR_POSITION = -1;
+ private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
+ private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
// Whether we are expecting an onUpdateSelection event to fire. If it does when we don't
// "expect" it, it means the user actually moved the cursor.
@@ -189,13 +158,10 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
private int mDeleteCount;
private long mLastKeyTime;
- private AudioManager mAudioManager;
- // Align sound effect volume on music volume
- private static final float FX_VOLUME = -1.0f;
- private boolean mSilentModeOn; // System-wide current configuration
+ private AudioAndHapticFeedbackManager mFeedbackManager;
- // TODO: Move this flag to VoiceProxy
- private boolean mConfigurationChanging;
+ // Member variables for remembering the current device orientation.
+ private int mDisplayOrientation;
// Object for reacting to adding/removing a dictionary pack.
private BroadcastReceiver mDictionaryPackInstallReceiver =
@@ -204,73 +170,57 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
// Keeps track of most recently inserted text (multi-character key) for reverting
private CharSequence mEnteredText;
+ private boolean mIsAutoCorrectionIndicatorOn;
+
+ private AlertDialog mOptionsDialog;
public final UIHandler mHandler = new UIHandler(this);
public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
- private static final int MSG_UPDATE_SUGGESTIONS = 0;
- private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1;
- private static final int MSG_UPDATE_SHIFT_STATE = 2;
- private static final int MSG_VOICE_RESULTS = 3;
- private static final int MSG_FADEOUT_LANGUAGE_ON_SPACEBAR = 4;
- private static final int MSG_DISMISS_LANGUAGE_ON_SPACEBAR = 5;
- private static final int MSG_SPACE_TYPED = 6;
- private static final int MSG_SET_BIGRAM_PREDICTIONS = 7;
+ private static final int MSG_UPDATE_SHIFT_STATE = 1;
+ private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
+ private static final int MSG_PENDING_IMS_CALLBACK = 6;
+ private static final int MSG_UPDATE_SUGGESTIONS = 7;
+
+ private int mDelayUpdateSuggestions;
+ private int mDelayUpdateShiftState;
+ private long mDoubleSpacesTurnIntoPeriodTimeout;
+ private long mDoubleSpaceTimerStart;
public UIHandler(LatinIME outerInstance) {
super(outerInstance);
}
+ public void onCreate() {
+ final Resources res = getOuterInstance().getResources();
+ mDelayUpdateSuggestions =
+ res.getInteger(R.integer.config_delay_update_suggestions);
+ mDelayUpdateShiftState =
+ res.getInteger(R.integer.config_delay_update_shift_state);
+ mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
+ R.integer.config_double_spaces_turn_into_period_timeout);
+ }
+
@Override
public void handleMessage(Message msg) {
final LatinIME latinIme = getOuterInstance();
final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
- final LatinKeyboardView inputView = switcher.getKeyboardView();
switch (msg.what) {
case MSG_UPDATE_SUGGESTIONS:
latinIme.updateSuggestions();
break;
- case MSG_UPDATE_OLD_SUGGESTIONS:
- latinIme.mRecorrection.fetchAndDisplayRecorrectionSuggestions(
- latinIme.mVoiceProxy, latinIme.mCandidateView,
- latinIme.mSuggest, latinIme.mKeyboardSwitcher, latinIme.mWord,
- latinIme.mHasUncommittedTypedChars, latinIme.mLastSelectionStart,
- latinIme.mLastSelectionEnd, latinIme.mSettingsValues.mWordSeparators);
- break;
case MSG_UPDATE_SHIFT_STATE:
switcher.updateShiftState();
break;
case MSG_SET_BIGRAM_PREDICTIONS:
latinIme.updateBigramPredictions();
break;
- case MSG_VOICE_RESULTS:
- latinIme.mVoiceProxy.handleVoiceResults(latinIme.preferCapitalization()
- || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked()));
- break;
- case MSG_FADEOUT_LANGUAGE_ON_SPACEBAR:
- if (inputView != null) {
- inputView.setSpacebarTextFadeFactor(
- (1.0f + latinIme.mSettingsValues.
- mFinalFadeoutFactorOfLanguageOnSpacebar) / 2,
- (LatinKeyboard)msg.obj);
- }
- sendMessageDelayed(obtainMessage(MSG_DISMISS_LANGUAGE_ON_SPACEBAR, msg.obj),
- latinIme.mSettingsValues.mDurationOfFadeoutLanguageOnSpacebar);
- break;
- case MSG_DISMISS_LANGUAGE_ON_SPACEBAR:
- if (inputView != null) {
- inputView.setSpacebarTextFadeFactor(
- latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
- (LatinKeyboard)msg.obj);
- }
- break;
}
}
public void postUpdateSuggestions() {
removeMessages(MSG_UPDATE_SUGGESTIONS);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS),
- getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
}
public void cancelUpdateSuggestions() {
@@ -281,20 +231,9 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
return hasMessages(MSG_UPDATE_SUGGESTIONS);
}
- public void postUpdateOldSuggestions() {
- removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS),
- getOuterInstance().mSettingsValues.mDelayUpdateOldSuggestions);
- }
-
- public void cancelUpdateOldSuggestions() {
- removeMessages(MSG_UPDATE_OLD_SUGGESTIONS);
- }
-
- public void postUpdateShiftKeyState() {
+ public void postUpdateShiftState() {
removeMessages(MSG_UPDATE_SHIFT_STATE);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE),
- getOuterInstance().mSettingsValues.mDelayUpdateShiftState);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
}
public void cancelUpdateShiftState() {
@@ -303,85 +242,156 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
public void postUpdateBigramPredictions() {
removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
- sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS),
- getOuterInstance().mSettingsValues.mDelayUpdateSuggestions);
+ sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
}
public void cancelUpdateBigramPredictions() {
removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
}
- public void updateVoiceResults() {
- sendMessage(obtainMessage(MSG_VOICE_RESULTS));
+ public void startDoubleSpacesTimer() {
+ mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
}
- public void startDisplayLanguageOnSpacebar(boolean localeChanged) {
+ public void cancelDoubleSpacesTimer() {
+ mDoubleSpaceTimerStart = 0;
+ }
+
+ public boolean isAcceptingDoubleSpaces() {
+ return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
+ < mDoubleSpacesTurnIntoPeriodTimeout;
+ }
+
+ // Working variables for the following methods.
+ private boolean mIsOrientationChanging;
+ private boolean mPendingSuccessiveImsCallback;
+ private boolean mHasPendingStartInput;
+ private boolean mHasPendingFinishInputView;
+ private boolean mHasPendingFinishInput;
+ private EditorInfo mAppliedEditorInfo;
+
+ public void startOrientationChanging() {
+ removeMessages(MSG_PENDING_IMS_CALLBACK);
+ resetPendingImsCallback();
+ mIsOrientationChanging = true;
final LatinIME latinIme = getOuterInstance();
- removeMessages(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR);
- removeMessages(MSG_DISMISS_LANGUAGE_ON_SPACEBAR);
- final LatinKeyboardView inputView = latinIme.mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) {
- final LatinKeyboard keyboard = latinIme.mKeyboardSwitcher.getLatinKeyboard();
- // The language is always displayed when the delay is negative.
- final boolean needsToDisplayLanguage = localeChanged
- || latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar < 0;
- // The language is never displayed when the delay is zero.
- if (latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar != 0) {
- inputView.setSpacebarTextFadeFactor(needsToDisplayLanguage ? 1.0f
- : latinIme.mSettingsValues.mFinalFadeoutFactorOfLanguageOnSpacebar,
- keyboard);
- }
- // The fadeout animation will start when the delay is positive.
- if (localeChanged
- && latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar > 0) {
- sendMessageDelayed(obtainMessage(MSG_FADEOUT_LANGUAGE_ON_SPACEBAR, keyboard),
- latinIme.mSettingsValues.mDelayBeforeFadeoutLanguageOnSpacebar);
+ if (latinIme.isInputViewShown()) {
+ latinIme.mKeyboardSwitcher.saveKeyboardState();
+ }
+ }
+
+ private void resetPendingImsCallback() {
+ mHasPendingFinishInputView = false;
+ mHasPendingFinishInput = false;
+ mHasPendingStartInput = false;
+ }
+
+ private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
+ boolean restarting) {
+ if (mHasPendingFinishInputView)
+ latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
+ if (mHasPendingFinishInput)
+ latinIme.onFinishInputInternal();
+ if (mHasPendingStartInput)
+ latinIme.onStartInputInternal(editorInfo, restarting);
+ resetPendingImsCallback();
+ }
+
+ public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+ if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
+ // Typically this is the second onStartInput after orientation changed.
+ mHasPendingStartInput = true;
+ } else {
+ if (mIsOrientationChanging && restarting) {
+ // This is the first onStartInput after orientation changed.
+ mIsOrientationChanging = false;
+ mPendingSuccessiveImsCallback = true;
}
+ final LatinIME latinIme = getOuterInstance();
+ executePendingImsCallback(latinIme, editorInfo, restarting);
+ latinIme.onStartInputInternal(editorInfo, restarting);
}
}
- public void startDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
- sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED),
- getOuterInstance().mSettingsValues.mDoubleSpacesTurnIntoPeriodTimeout);
+ public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+ if (hasMessages(MSG_PENDING_IMS_CALLBACK)
+ && KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
+ // Typically this is the second onStartInputView after orientation changed.
+ resetPendingImsCallback();
+ } else {
+ if (mPendingSuccessiveImsCallback) {
+ // This is the first onStartInputView after orientation changed.
+ mPendingSuccessiveImsCallback = false;
+ resetPendingImsCallback();
+ sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK),
+ PENDING_IMS_CALLBACK_DURATION);
+ }
+ final LatinIME latinIme = getOuterInstance();
+ executePendingImsCallback(latinIme, editorInfo, restarting);
+ latinIme.onStartInputViewInternal(editorInfo, restarting);
+ mAppliedEditorInfo = editorInfo;
+ }
}
- public void cancelDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
+ public void onFinishInputView(boolean finishingInput) {
+ if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
+ // Typically this is the first onFinishInputView after orientation changed.
+ mHasPendingFinishInputView = true;
+ } else {
+ final LatinIME latinIme = getOuterInstance();
+ latinIme.onFinishInputViewInternal(finishingInput);
+ mAppliedEditorInfo = null;
+ }
}
- public boolean isAcceptingDoubleSpaces() {
- return hasMessages(MSG_SPACE_TYPED);
+ public void onFinishInput() {
+ if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
+ // Typically this is the first onFinishInput after orientation changed.
+ mHasPendingFinishInput = true;
+ } else {
+ final LatinIME latinIme = getOuterInstance();
+ executePendingImsCallback(latinIme, null, false);
+ latinIme.onFinishInputInternal();
+ }
}
}
+ public LatinIME() {
+ super();
+ mSubtypeSwitcher = SubtypeSwitcher.getInstance();
+ mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+ }
+
@Override
public void onCreate() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
mPrefs = prefs;
LatinImeLogger.init(this, prefs);
- LanguageSwitcherProxy.init(this, prefs);
- SubtypeSwitcher.init(this, prefs);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().init(this, prefs);
+ }
+ InputMethodManagerCompatWrapper.init(this);
+ SubtypeSwitcher.init(this);
KeyboardSwitcher.init(this, prefs);
- Recorrection.init(this, prefs);
- AccessibilityUtils.init(this, prefs);
+ AccessibilityUtils.init(this);
super.onCreate();
- mImm = InputMethodManagerCompatWrapper.getInstance(this);
- mInputMethodId = Utils.getInputMethodId(mImm, getPackageName());
- mSubtypeSwitcher = SubtypeSwitcher.getInstance();
- mKeyboardSwitcher = KeyboardSwitcher.getInstance();
- mRecorrection = Recorrection.getInstance();
+ mImm = InputMethodManagerCompatWrapper.getInstance();
+ mHandler.onCreate();
DEBUG = LatinImeLogger.sDBG;
- loadSettings();
-
final Resources res = getResources();
mResources = res;
+ loadSettings();
+
+ ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
+
Utils.GCUtils.getInstance().reset();
boolean tryGC = true;
+ // Shouldn't this be removed? I think that from Honeycomb on, the GC is now actually working
+ // as expected and this code is useless.
for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
initSuggest();
@@ -391,15 +401,14 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
}
- mOrientation = res.getConfiguration().orientation;
+ mDisplayOrientation = res.getConfiguration().orientation;
// Register to receive ringer mode change and network state change.
// Also receive installation and removal of a dictionary pack.
final IntentFilter filter = new IntentFilter();
- filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
registerReceiver(mReceiver, filter);
- mVoiceProxy = VoiceProxy.init(this, prefs, mHandler);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -415,57 +424,103 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
// Has to be package-visible for unit tests
/* package */ void loadSettings() {
+ // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
+ // is not guaranteed. It may even be called at the same time on a different thread.
if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
- if (null == mSubtypeSwitcher) mSubtypeSwitcher = SubtypeSwitcher.getInstance();
- mSettingsValues = new Settings.Values(mPrefs, this, mSubtypeSwitcher.getInputLocaleStr());
- resetContactsDictionary();
+ final InputAttributes inputAttributes =
+ new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
+ final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
+ @Override
+ protected SettingsValues job(Resources res) {
+ return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
+ }
+ };
+ mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+ mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
+ resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
}
private void initSuggest() {
- final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
- final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
+ final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ final String localeStr = subtypeLocale.toString();
- final Resources res = mResources;
- final Locale savedLocale = Utils.setSystemLocale(res, keyboardLocale);
+ final ContactsBinaryDictionary oldContactsDictionary;
if (mSuggest != null) {
+ oldContactsDictionary = mSuggest.getContactsDictionary();
mSuggest.close();
+ } else {
+ oldContactsDictionary = null;
+ }
+ mSuggest = new Suggest(this, subtypeLocale);
+ if (mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
}
- int mainDicResId = Utils.getMainDictionaryResourceId(res);
- mSuggest = new Suggest(this, mainDicResId, keyboardLocale);
- if (mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().initSuggest(mSuggest);
}
- updateAutoTextEnabled();
- mUserDictionary = new UserDictionary(this, localeStr);
+ mUserDictionary = new UserBinaryDictionary(this, localeStr);
+ mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
mSuggest.setUserDictionary(mUserDictionary);
- resetContactsDictionary();
-
- mAutoDictionary = new AutoDictionary(this, this, localeStr, Suggest.DIC_AUTO);
- mSuggest.setAutoDictionary(mAutoDictionary);
+ resetContactsDictionary(oldContactsDictionary);
- mUserBigramDictionary = new UserBigramDictionary(this, this, localeStr, Suggest.DIC_USER);
- mSuggest.setUserBigramDictionary(mUserBigramDictionary);
-
- updateCorrectionMode();
-
- Utils.setSystemLocale(res, savedLocale);
+ // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
+ // is not guaranteed. It may even be called at the same time on a different thread.
+ if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mUserHistoryDictionary = UserHistoryDictionary.getInstance(
+ this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs);
+ mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
}
- private void resetContactsDictionary() {
- if (null == mSuggest) return;
- ContactsDictionary contactsDictionary = mSettingsValues.mUseContactsDict
- ? new ContactsDictionary(this, Suggest.DIC_CONTACTS) : null;
- mSuggest.setContactsDictionary(contactsDictionary);
+ /**
+ * Resets the contacts dictionary in mSuggest according to the user settings.
+ *
+ * This method takes an optional contacts dictionary to use when the locale hasn't changed
+ * since the contacts dictionary can be opened or closed as necessary depending on the settings.
+ *
+ * @param oldContactsDictionary an optional dictionary to use, or null
+ */
+ private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+ final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
+
+ final ContactsBinaryDictionary dictionaryToUse;
+ if (!shouldSetDictionary) {
+ // Make sure the dictionary is closed. If it is already closed, this is a no-op,
+ // so it's safe to call it anyways.
+ if (null != oldContactsDictionary) oldContactsDictionary.close();
+ dictionaryToUse = null;
+ } else {
+ final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ if (null != oldContactsDictionary) {
+ if (!oldContactsDictionary.mLocale.equals(locale)) {
+ // If the locale has changed then recreate the contacts dictionary. This
+ // allows locale dependent rules for handling bigram name predictions.
+ oldContactsDictionary.close();
+ dictionaryToUse = new ContactsBinaryDictionary(
+ this, Suggest.DIC_CONTACTS, locale);
+ } else {
+ // Make sure the old contacts dictionary is opened. If it is already open,
+ // this is a no-op, so it's safe to call it anyways.
+ oldContactsDictionary.reopen(this);
+ dictionaryToUse = oldContactsDictionary;
+ }
+ } else {
+ dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS, locale);
+ }
+ }
+
+ if (null != mSuggest) {
+ mSuggest.setContactsDictionary(dictionaryToUse);
+ }
}
/* package private */ void resetSuggestMainDict() {
- final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
- final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
- int mainDicResId = Utils.getMainDictionaryResourceId(mResources);
- mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
+ final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ mSuggest.resetMainDict(this, subtypeLocale);
+ mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
}
@Override
@@ -476,7 +531,6 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
unregisterReceiver(mReceiver);
unregisterReceiver(mDictionaryPackInstallReceiver);
- mVoiceProxy.destroy();
LatinImeLogger.commit();
LatinImeLogger.onDestroy();
super.onDestroy();
@@ -486,22 +540,17 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
public void onConfigurationChanged(Configuration conf) {
mSubtypeSwitcher.onConfigurationChanged(conf);
// If orientation changed while predicting, commit the change
- if (conf.orientation != mOrientation) {
- InputConnection ic = getCurrentInputConnection();
- commitTyped(ic);
- if (ic != null) ic.finishComposingText(); // For voice input
- mOrientation = conf.orientation;
+ if (mDisplayOrientation != conf.orientation) {
+ mDisplayOrientation = conf.orientation;
+ mHandler.startOrientationChanging();
+ mConnection.beginBatchEdit(getCurrentInputConnection());
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mConnection.finishComposingText();
+ mConnection.endBatchEdit();
if (isShowingOptionDialog())
mOptionsDialog.dismiss();
}
-
- mConfigurationChanging = true;
super.onConfigurationChanged(conf);
- mVoiceProxy.onConfigurationChanged(conf);
- mConfigurationChanging = false;
-
- // This will work only when the subtype is not supported.
- LanguageSwitcherProxy.onConfigurationChanged(conf);
}
@Override
@@ -512,11 +561,16 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
@Override
public void setInputView(View view) {
super.setInputView(view);
- mCandidateViewContainer = view.findViewById(R.id.candidates_container);
- mCandidateView = (CandidateView) view.findViewById(R.id.candidates);
- if (mCandidateView != null)
- mCandidateView.setListener(this, view);
- mCandidateStripHeight = (int)mResources.getDimension(R.dimen.candidate_strip_height);
+ mExtractArea = getWindow().getWindow().getDecorView()
+ .findViewById(android.R.id.extractArea);
+ mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
+ mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
+ mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
+ if (mSuggestionsView != null)
+ mSuggestionsView.setListener(this, view);
+ if (LatinImeLogger.sVISUALDEBUG) {
+ mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
+ }
}
@Override
@@ -526,177 +580,175 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
@Override
- public void onStartInputView(EditorInfo attribute, boolean restarting) {
+ public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+ mHandler.onStartInput(editorInfo, restarting);
+ }
+
+ @Override
+ public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+ mHandler.onStartInputView(editorInfo, restarting);
+ }
+
+ @Override
+ public void onFinishInputView(boolean finishingInput) {
+ mHandler.onFinishInputView(finishingInput);
+ }
+
+ @Override
+ public void onFinishInput() {
+ mHandler.onFinishInput();
+ }
+
+ @Override
+ public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
+ // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
+ // is not guaranteed. It may even be called at the same time on a different thread.
+ mSubtypeSwitcher.updateSubtype(subtype);
+ }
+
+ private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
+ super.onStartInput(editorInfo, restarting);
+ }
+
+ @SuppressWarnings("deprecation")
+ private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+ super.onStartInputView(editorInfo, restarting);
final KeyboardSwitcher switcher = mKeyboardSwitcher;
LatinKeyboardView inputView = switcher.getKeyboardView();
- if (DEBUG) {
- Log.d(TAG, "onStartInputView: attribute:" + ((attribute == null) ? "none"
- : String.format("inputType=0x%08x imeOptions=0x%08x",
- attribute.inputType, attribute.imeOptions)));
+ if (editorInfo == null) {
+ Log.e(TAG, "Null EditorInfo in onStartInputView()");
+ if (LatinImeLogger.sDBG) {
+ throw new NullPointerException("Null EditorInfo in onStartInputView()");
+ }
+ return;
}
+ if (DEBUG) {
+ Log.d(TAG, "onStartInputView: editorInfo:"
+ + String.format("inputType=0x%08x imeOptions=0x%08x",
+ editorInfo.inputType, editorInfo.imeOptions));
+ Log.d(TAG, "All caps = "
+ + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0)
+ + ", sentence caps = "
+ + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0)
+ + ", word caps = "
+ + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0));
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().start();
+ ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs);
+ }
+ if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) {
+ Log.w(TAG, "Deprecated private IME option specified: "
+ + editorInfo.privateImeOptions);
+ Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead");
+ }
+ if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) {
+ Log.w(TAG, "Deprecated private IME option specified: "
+ + editorInfo.privateImeOptions);
+ Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead");
+ }
+
+ mTargetApplicationInfo =
+ TargetApplicationGetter.getCachedApplicationInfo(editorInfo.packageName);
+ if (null == mTargetApplicationInfo) {
+ new TargetApplicationGetter(this /* context */, this /* listener */)
+ .execute(editorInfo.packageName);
+ }
+
+ LatinImeLogger.onStartInputView(editorInfo);
// In landscape mode, this method gets called without the input view being created.
if (inputView == null) {
return;
}
- mSubtypeSwitcher.updateParametersOnStartInputView();
-
- TextEntryState.reset();
+ // Forward this event to the accessibility utilities, if enabled.
+ final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
+ if (accessUtils.isTouchExplorationEnabled()) {
+ accessUtils.onStartInputViewInternal(editorInfo, restarting);
+ }
- // Most such things we decide below in initializeInputAttributesAndGetMode, but we need to
- // know now whether this is a password text field, because we need to know now whether we
- // want to enable the voice button.
- final VoiceProxy voiceIme = mVoiceProxy;
- voiceIme.resetVoiceStates(InputTypeCompatUtils.isPasswordInputType(attribute.inputType)
- || InputTypeCompatUtils.isVisiblePasswordInputType(attribute.inputType));
+ mSubtypeSwitcher.updateParametersOnStartInputView();
- initializeInputAttributes(attribute);
+ // The EditorInfo might have a flag that affects fullscreen mode.
+ // Note: This call should be done by InputMethodService?
+ updateFullscreenMode();
+ mLastSelectionStart = editorInfo.initialSelStart;
+ mLastSelectionEnd = editorInfo.initialSelEnd;
+ mApplicationSpecifiedCompletions = null;
inputView.closing();
mEnteredText = null;
- mComposing.setLength(0);
- mHasUncommittedTypedChars = false;
+ resetComposingState(true /* alsoResetLastComposedWord */);
mDeleteCount = 0;
- mJustAddedMagicSpace = false;
- mJustReplacedDoubleSpace = false;
+ mSpaceState = SPACE_STATE_NONE;
loadSettings();
- updateCorrectionMode();
- updateAutoTextEnabled();
- updateSuggestionVisibility(mPrefs, mResources);
- if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
- }
- mVoiceProxy.loadSettings(attribute, mPrefs);
- // This will work only when the subtype is not supported.
- LanguageSwitcherProxy.loadSettings();
-
- if (mSubtypeSwitcher.isKeyboardMode()) {
- switcher.loadKeyboard(attribute,
- mSubtypeSwitcher.isShortcutImeEnabled() && voiceIme.isVoiceButtonEnabled(),
- voiceIme.isVoiceButtonOnPrimary());
- switcher.updateShiftState();
+ if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
}
- setSuggestionStripShownInternal(isCandidateStripVisible(), /* needsInputViewShown */ false);
+ switcher.loadKeyboard(editorInfo, mCurrentSettings);
+
+ if (mSuggestionsView != null)
+ mSuggestionsView.clear();
+ setSuggestionStripShownInternal(
+ isSuggestionsStripVisible(), /* needsInputViewShown */ false);
// Delay updating suggestions because keyboard input view may not be shown at this point.
mHandler.postUpdateSuggestions();
+ mHandler.cancelDoubleSpacesTimer();
- updateCorrectionMode();
-
- inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
- mSettingsValues.mKeyPreviewPopupDismissDelay);
+ inputView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
+ mCurrentSettings.mKeyPreviewPopupDismissDelay);
inputView.setProximityCorrectionEnabled(true);
- // If we just entered a text field, maybe it has some old text that requires correction
- mRecorrection.checkRecorrectionOnStart();
-
- voiceIme.onStartInputView(inputView.getWindowToken());
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
- private void initializeInputAttributes(EditorInfo attribute) {
- if (attribute == null)
- return;
- final int inputType = attribute.inputType;
- final int variation = inputType & InputType.TYPE_MASK_VARIATION;
- mShouldInsertMagicSpace = false;
- mInputTypeNoAutoCorrect = false;
- mIsSettingsSuggestionStripOn = false;
- mApplicationSpecifiedCompletionOn = false;
- mApplicationSpecifiedCompletions = null;
-
- if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
- mIsSettingsSuggestionStripOn = true;
- // Make sure that passwords are not displayed in candidate view
- if (InputTypeCompatUtils.isPasswordInputType(inputType)
- || InputTypeCompatUtils.isVisiblePasswordInputType(inputType)) {
- mIsSettingsSuggestionStripOn = false;
- }
- if (InputTypeCompatUtils.isEmailVariation(variation)
- || variation == InputType.TYPE_TEXT_VARIATION_PERSON_NAME) {
- mShouldInsertMagicSpace = false;
- } else {
- mShouldInsertMagicSpace = true;
- }
- if (InputTypeCompatUtils.isEmailVariation(variation)) {
- mIsSettingsSuggestionStripOn = false;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
- mIsSettingsSuggestionStripOn = false;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- mIsSettingsSuggestionStripOn = false;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) {
- // If it's a browser edit field and auto correct is not ON explicitly, then
- // disable auto correction, but keep suggestions on.
- if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0) {
- mInputTypeNoAutoCorrect = true;
- }
- }
-
- // If NO_SUGGESTIONS is set, don't do prediction.
- if ((inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0) {
- mIsSettingsSuggestionStripOn = false;
- mInputTypeNoAutoCorrect = true;
- }
- // If it's not multiline and the autoCorrect flag is not set, then don't correct
- if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) == 0
- && (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
- mInputTypeNoAutoCorrect = true;
- }
- if ((inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
- mIsSettingsSuggestionStripOn = false;
- mApplicationSpecifiedCompletionOn = isFullscreenMode();
- }
- }
+ @Override
+ public void onTargetApplicationKnown(final ApplicationInfo info) {
+ mTargetApplicationInfo = info;
}
@Override
public void onWindowHidden() {
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+ getCurrentInputConnection());
+ }
super.onWindowHidden();
KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
if (inputView != null) inputView.closing();
}
- @Override
- public void onFinishInput() {
+ private void onFinishInputInternal() {
super.onFinishInput();
LatinImeLogger.commit();
- mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
-
- mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().stop();
+ }
KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
if (inputView != null) inputView.closing();
- if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites();
- if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites();
}
- @Override
- public void onFinishInputView(boolean finishingInput) {
+ private void onFinishInputViewInternal(boolean finishingInput) {
super.onFinishInputView(finishingInput);
+ mKeyboardSwitcher.onFinishInputView();
KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.cancelAllMessage();
+ if (inputView != null) inputView.cancelAllMessages();
// Remove pending messages related to update suggestions
mHandler.cancelUpdateSuggestions();
- mHandler.cancelUpdateOldSuggestions();
- }
-
- @Override
- public void onUpdateExtractedText(int token, ExtractedText text) {
- super.onUpdateExtractedText(token, text);
- mVoiceProxy.showPunctuationHintIfNecessary();
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd,
int newSelStart, int newSelEnd,
- int candidatesStart, int candidatesEnd) {
+ int composingSpanStart, int composingSpanEnd) {
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
- candidatesStart, candidatesEnd);
-
+ composingSpanStart, composingSpanEnd);
if (DEBUG) {
Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ ", ose=" + oldSelEnd
@@ -704,79 +756,79 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
+ ", lse=" + mLastSelectionEnd
+ ", nss=" + newSelStart
+ ", nse=" + newSelEnd
- + ", cs=" + candidatesStart
- + ", ce=" + candidatesEnd);
- }
-
- mVoiceProxy.setCursorAndSelection(newSelEnd, newSelStart);
-
- // If the current selection in the text view changes, we should
- // clear whatever candidate text we have.
- final boolean selectionChanged = (newSelStart != candidatesEnd
- || newSelEnd != candidatesEnd) && mLastSelectionStart != newSelStart;
- final boolean candidatesCleared = candidatesStart == -1 && candidatesEnd == -1;
- if (((mComposing.length() > 0 && mHasUncommittedTypedChars)
- || mVoiceProxy.isVoiceInputHighlighted())
- && (selectionChanged || candidatesCleared)) {
- if (candidatesCleared) {
- // If the composing span has been cleared, save the typed word in the history for
- // recorrection before we reset the candidate strip. Then, we'll be able to show
- // suggestions for recorrection right away.
- mRecorrection.saveRecorrectionSuggestion(mWord, mComposing);
- }
- mComposing.setLength(0);
- mHasUncommittedTypedChars = false;
- if (isCursorTouchingWord()) {
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
- } else {
- setPunctuationSuggestions();
- }
- TextEntryState.reset();
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.finishComposingText();
- }
- mVoiceProxy.setVoiceInputHighlighted(false);
- } else if (!mHasUncommittedTypedChars && !mExpectingUpdateSelection) {
- if (TextEntryState.isAcceptedDefault() || TextEntryState.isSpaceAfterPicked()) {
- if (TextEntryState.isAcceptedDefault())
- TextEntryState.reset();
+ + ", cs=" + composingSpanStart
+ + ", ce=" + composingSpanEnd);
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ final boolean expectingUpdateSelectionFromLogger =
+ ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
+ ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
+ oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
+ composingSpanEnd, mExpectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, mConnection);
+ if (expectingUpdateSelectionFromLogger) {
+ // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
+ return;
}
}
+
+ // TODO: refactor the following code to be less contrived.
+ // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
+ // that the cursor is not at the end of the composing span, or there is a selection.
+ // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
+ // as last time we were called (if there is a selection, it means the start hasn't
+ // changed, so it's the end that did).
+ final boolean selectionChanged = (newSelStart != composingSpanEnd
+ || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
+ // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
+ // span in the view - we can use that to narrow down whether the cursor was moved
+ // by us or not. If we are composing a word but there is no composing span, then
+ // we know for sure the cursor moved while we were composing and we should reset
+ // the state.
+ final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
if (!mExpectingUpdateSelection) {
- mJustAddedMagicSpace = false; // The user moved the cursor.
- mJustReplacedDoubleSpace = false;
+ // TAKE CARE: there is a race condition when we enter this test even when the user
+ // did not explicitly move the cursor. This happens when typing fast, where two keys
+ // turn this flag on in succession and both onUpdateSelection() calls arrive after
+ // the second one - the first call successfully avoids this test, but the second one
+ // enters. For the moment we rely on noComposingSpan to further reduce the impact.
+
+ // TODO: the following is probably better done in resetEntireInputState().
+ // it should only happen when the cursor moved, and the very purpose of the
+ // test below is to narrow down whether this happened or not. Likewise with
+ // the call to postUpdateShiftState.
+ // We set this to NONE because after a cursor move, we don't want the space
+ // state-related special processing to kick in.
+ mSpaceState = SPACE_STATE_NONE;
+
+ if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
+ resetEntireInputState();
+ }
+
+ mHandler.postUpdateShiftState();
}
mExpectingUpdateSelection = false;
- mHandler.postUpdateShiftKeyState();
+ // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
+ // here. It would probably be too expensive to call directly here but we may want to post a
+ // message to delay it. The point would be to unify behavior between backspace to the
+ // end of a word and manually put the pointer at the end of the word.
// Make a note of the cursor position
mLastSelectionStart = newSelStart;
mLastSelectionEnd = newSelEnd;
-
- mRecorrection.updateRecorrectionSelection(mKeyboardSwitcher,
- mCandidateView, candidatesStart, candidatesEnd, newSelStart,
- newSelEnd, oldSelStart, mLastSelectionStart,
- mLastSelectionEnd, mHasUncommittedTypedChars);
- }
-
- public void setLastSelection(int start, int end) {
- mLastSelectionStart = start;
- mLastSelectionEnd = end;
}
/**
* This is called when the user has clicked on the extracted text view,
* when running in fullscreen mode. The default implementation hides
- * the candidates view when this happens, but only if the extracted text
+ * the suggestions view when this happens, but only if the extracted text
* editor has a vertical scroll bar because its text doesn't fit.
* Here we override the behavior due to the possibility that a re-correction could
- * cause the candidate strip to disappear and re-appear.
+ * cause the suggestions strip to disappear and re-appear.
*/
@Override
public void onExtractedTextClicked() {
- if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return;
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedTextClicked();
}
@@ -784,15 +836,15 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
/**
* This is called when the user has performed a cursor movement in the
* extracted text view, when it is running in fullscreen mode. The default
- * implementation hides the candidates view when a vertical movement
+ * implementation hides the suggestions view when a vertical movement
* happens, but only if the extracted text editor has a vertical scroll bar
* because its text doesn't fit.
* Here we override the behavior due to the possibility that a re-correction could
- * cause the candidate strip to disappear and re-appear.
+ * cause the suggestions strip to disappear and re-appear.
*/
@Override
public void onExtractedCursorMovement(int dx, int dy) {
- if (mRecorrection.isRecorrectionEnabled() && isSuggestionsRequested()) return;
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedCursorMovement(dx, dy);
}
@@ -800,15 +852,13 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
@Override
public void hideWindow() {
LatinImeLogger.commit();
- mKeyboardSwitcher.onAutoCorrectionStateChanged(false);
+ mKeyboardSwitcher.onHideWindow();
if (TRACE) Debug.stopMethodTracing();
if (mOptionsDialog != null && mOptionsDialog.isShowing()) {
mOptionsDialog.dismiss();
mOptionsDialog = null;
}
- mVoiceProxy.hideVoiceWindow(mConfigurationChanging);
- mRecorrection.clearWordsInHistory();
super.hideWindow();
}
@@ -822,39 +872,50 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
}
}
- if (mApplicationSpecifiedCompletionOn) {
- mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
- if (applicationSpecifiedCompletions == null) {
- clearSuggestions();
- return;
- }
-
- SuggestedWords.Builder builder = new SuggestedWords.Builder()
- .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions)
- .setTypedWordValid(true)
- .setHasMinimalSuggestion(true);
- // When in fullscreen mode, show completions generated by the application
- setSuggestions(builder.build());
- mBestWord = null;
- setSuggestionStripShown(true);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
}
+ if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+ mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+ if (applicationSpecifiedCompletions == null) {
+ clearSuggestions();
+ return;
+ }
+
+ final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+ SuggestedWords.getFromApplicationSpecifiedCompletions(
+ applicationSpecifiedCompletions);
+ final SuggestedWords suggestedWords = new SuggestedWords(
+ applicationSuggestedWords,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* allowsToBeAutoCorrected */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ // When in fullscreen mode, show completions generated by the application
+ final boolean isAutoCorrection = false;
+ setSuggestions(suggestedWords, isAutoCorrection);
+ setAutoCorrectionIndicator(isAutoCorrection);
+ // TODO: is this the right thing to do? What should we auto-correct to in
+ // this case? This says to keep whatever the user typed.
+ mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+ setSuggestionStripShown(true);
}
private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
- // TODO: Modify this if we support candidates with hard keyboard
- if (onEvaluateInputViewShown() && mCandidateViewContainer != null) {
- final boolean shouldShowCandidates = shown
- && (needsInputViewShown ? mKeyboardSwitcher.isInputViewShown() : true);
- if (isExtractViewShown()) {
- // No need to have extra space to show the key preview.
- mCandidateViewContainer.setMinimumHeight(0);
- mCandidateViewContainer.setVisibility(
- shouldShowCandidates ? View.VISIBLE : View.GONE);
+ // TODO: Modify this if we support suggestions with hard keyboard
+ if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
+ final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
+ final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
+ final boolean shouldShowSuggestions = shown
+ && (needsInputViewShown ? inputViewShown : true);
+ if (isFullscreenMode()) {
+ mSuggestionsContainer.setVisibility(
+ shouldShowSuggestions ? View.VISIBLE : View.GONE);
} else {
- // We must control the visibility of the suggestion strip in order to avoid clipped
- // key previews, even when we don't show the suggestion strip.
- mCandidateViewContainer.setVisibility(
- shouldShowCandidates ? View.VISIBLE : View.INVISIBLE);
+ mSuggestionsContainer.setVisibility(
+ shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
}
}
}
@@ -863,28 +924,60 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
}
+ private int getAdjustedBackingViewHeight() {
+ final int currentHeight = mKeyPreviewBackingView.getHeight();
+ if (currentHeight > 0) {
+ return currentHeight;
+ }
+
+ final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
+ if (keyboardView == null) {
+ return 0;
+ }
+ final int keyboardHeight = keyboardView.getHeight();
+ final int suggestionsHeight = mSuggestionsContainer.getHeight();
+ final int displayHeight = mResources.getDisplayMetrics().heightPixels;
+ final Rect rect = new Rect();
+ mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
+ final int notificationBarHeight = rect.top;
+ final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight
+ - keyboardHeight;
+
+ final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
+ params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
+ mKeyPreviewBackingView.setLayoutParams(params);
+ return params.height;
+ }
+
@Override
public void onComputeInsets(InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView == null || mCandidateViewContainer == null)
+ if (inputView == null || mSuggestionsContainer == null)
return;
- final int containerHeight = mCandidateViewContainer.getHeight();
- int touchY = containerHeight;
+ final int adjustedBackingHeight = getAdjustedBackingViewHeight();
+ final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
+ final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
+ // In fullscreen mode, the height of the extract area managed by InputMethodService should
+ // be considered.
+ // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
+ final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
+ final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
+ : mSuggestionsContainer.getHeight();
+ final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
+ int touchY = extraHeight;
// Need to set touchable region only if input view is being shown
- if (mKeyboardSwitcher.isInputViewShown()) {
- if (mCandidateViewContainer.getVisibility() == View.VISIBLE) {
- touchY -= mCandidateStripHeight;
+ final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
+ if (keyboardView != null && keyboardView.isShown()) {
+ if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
+ touchY -= suggestionsHeight;
}
final int touchWidth = inputView.getWidth();
- final int touchHeight = inputView.getHeight() + containerHeight
+ final int touchHeight = inputView.getHeight() + extraHeight
// Extend touchable region below the keyboard.
+ EXTENDED_TOUCHABLE_REGION_HEIGHT;
- if (DEBUG) {
- Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
- + " height=" + touchHeight);
- }
- setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
+ outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
+ outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight);
}
outInsets.contentTopInsets = touchY;
outInsets.visibleTopInsets = touchY;
@@ -892,278 +985,364 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
@Override
public boolean onEvaluateFullscreenMode() {
- final Resources res = mResources;
- DisplayMetrics dm = res.getDisplayMetrics();
- float displayHeight = dm.heightPixels;
- // If the display is more than X inches high, don't go to fullscreen mode
- float dimen = res.getDimension(R.dimen.max_height_for_fullscreen);
- if (displayHeight > dimen) {
- return false;
- } else {
- return super.onEvaluateFullscreenMode();
- }
+ // Reread resource value here, because this method is called by framework anytime as needed.
+ final boolean isFullscreenModeAllowed =
+ mCurrentSettings.isFullscreenModeAllowed(getResources());
+ return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
}
@Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getKeyboardView() != null) {
- if (mKeyboardSwitcher.getKeyboardView().handleBack()) {
- return true;
- }
- }
- break;
- }
- return super.onKeyDown(keyCode, event);
+ public void updateFullscreenMode() {
+ super.updateFullscreenMode();
+
+ if (mKeyPreviewBackingView == null) return;
+ // In fullscreen mode, no need to have extra space to show the key preview.
+ // If not, we should have extra space above the keyboard to show the key preview.
+ mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
}
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- // Enable shift key and DPAD to do selections
- if (mKeyboardSwitcher.isInputViewShown()
- && mKeyboardSwitcher.isShiftedOrShiftLocked()) {
- KeyEvent newEvent = new KeyEvent(event.getDownTime(), event.getEventTime(),
- event.getAction(), event.getKeyCode(), event.getRepeatCount(),
- event.getDeviceId(), event.getScanCode(),
- KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_ON);
- InputConnection ic = getCurrentInputConnection();
- if (ic != null)
- ic.sendKeyEvent(newEvent);
- return true;
- }
- break;
- }
- return super.onKeyUp(keyCode, event);
+ // This will reset the whole input state to the starting state. It will clear
+ // the composing word, reset the last composed word, tell the inputconnection about it.
+ private void resetEntireInputState() {
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ updateSuggestions();
+ mConnection.finishComposingText();
}
- public void commitTyped(InputConnection inputConnection) {
- if (mHasUncommittedTypedChars) {
- mHasUncommittedTypedChars = false;
- if (mComposing.length() > 0) {
- if (inputConnection != null) {
- inputConnection.commitText(mComposing, 1);
- }
- mCommittedLength = mComposing.length();
- TextEntryState.acceptedTyped(mComposing);
- addToAutoAndUserBigramDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED);
+ private void resetComposingState(final boolean alsoResetLastComposedWord) {
+ mWordComposer.reset();
+ if (alsoResetLastComposedWord)
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ }
+
+ public void commitTyped(final int separatorCode) {
+ if (!mWordComposer.isComposingWord()) return;
+ final CharSequence typedWord = mWordComposer.getTypedWord();
+ if (typedWord.length() > 0) {
+ mConnection.commitText(typedWord, 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_commitText(typedWord);
}
- updateSuggestions();
+ final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
+ mLastComposedWord = mWordComposer.commitWord(
+ LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
+ separatorCode, prevWord);
}
+ updateSuggestions();
}
- public boolean getCurrentAutoCapsState() {
- InputConnection ic = getCurrentInputConnection();
- EditorInfo ei = getCurrentInputEditorInfo();
- if (mSettingsValues.mAutoCap && ic != null && ei != null
- && ei.inputType != InputType.TYPE_NULL) {
- return ic.getCursorCapsMode(ei.inputType) != 0;
+ public int getCurrentAutoCapsState() {
+ if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+
+ 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;
}
- return false;
+
+ 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);
}
private void swapSwapperAndSpace() {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+ CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
// It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
if (lastTwo != null && lastTwo.length() == 2
&& lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
- ic.beginBatchEdit();
- ic.deleteSurroundingText(2, 0);
- ic.commitText(lastTwo.charAt(1) + " ", 1);
- ic.endBatchEdit();
+ mConnection.deleteSurroundingText(2, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(2);
+ }
+ mConnection.commitText(lastTwo.charAt(1) + " ", 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+ }
mKeyboardSwitcher.updateShiftState();
}
}
- private void maybeDoubleSpace() {
- if (mCorrectionMode == Suggest.CORRECTION_NONE) return;
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+ private boolean maybeDoubleSpace() {
+ if (!mCurrentSettings.mCorrectionEnabled) return false;
+ if (!mHandler.isAcceptingDoubleSpaces()) return false;
+ final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
if (lastThree != null && lastThree.length() == 3
- && Character.isLetterOrDigit(lastThree.charAt(0))
+ && canBeFollowedByPeriod(lastThree.charAt(0))
&& lastThree.charAt(1) == Keyboard.CODE_SPACE
- && lastThree.charAt(2) == Keyboard.CODE_SPACE
- && mHandler.isAcceptingDoubleSpaces()) {
+ && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
mHandler.cancelDoubleSpacesTimer();
- ic.beginBatchEdit();
- ic.deleteSurroundingText(2, 0);
- ic.commitText(". ", 1);
- ic.endBatchEdit();
+ mConnection.deleteSurroundingText(2, 0);
+ mConnection.commitText(". ", 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_doubleSpaceAutoPeriod();
+ }
mKeyboardSwitcher.updateShiftState();
- mJustReplacedDoubleSpace = true;
- } else {
- mHandler.startDoubleSpacesTimer();
+ return true;
}
+ return false;
}
- private void maybeRemovePreviousPeriod(CharSequence text) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
-
- // When the text's first character is '.', remove the previous period
- // if there is one.
- CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Keyboard.CODE_PERIOD
- && text.charAt(0) == Keyboard.CODE_PERIOD) {
- ic.deleteSurroundingText(1, 0);
- }
- }
-
- private void removeTrailingSpace() {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
-
- CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
- ic.deleteSurroundingText(1, 0);
- }
+ private static boolean canBeFollowedByPeriod(final int codePoint) {
+ // TODO: Check again whether there really ain't a better way to check this.
+ // TODO: This should probably be language-dependant...
+ return Character.isLetterOrDigit(codePoint)
+ || codePoint == Keyboard.CODE_SINGLE_QUOTE
+ || codePoint == Keyboard.CODE_DOUBLE_QUOTE
+ || codePoint == Keyboard.CODE_CLOSING_PARENTHESIS
+ || codePoint == Keyboard.CODE_CLOSING_SQUARE_BRACKET
+ || codePoint == Keyboard.CODE_CLOSING_CURLY_BRACKET
+ || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
}
@Override
public boolean addWordToDictionary(String word) {
- mUserDictionary.addWord(word, 128);
+ mUserDictionary.addWordToUserDictionary(word, 128);
// Suggestion strip should be updated after the operation of adding word to the
// user dictionary
mHandler.postUpdateSuggestions();
return true;
}
- private boolean isAlphabet(int code) {
- if (Character.isLetter(code)) {
- return true;
- } else {
- return false;
- }
+ private static boolean isAlphabet(int code) {
+ return Character.isLetter(code);
}
private void onSettingsKeyPressed() {
- if (isShowingOptionDialog())
- return;
- if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
- showSubtypeSelectorAndSettings();
- } else if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
- showOptionsMenu();
- } else {
- launchSettings();
- }
+ if (isShowingOptionDialog()) return;
+ showSubtypeSelectorAndSettings();
}
- private void onSettingsKeyLongPressed() {
- if (!isShowingOptionDialog()) {
- if (Utils.hasMultipleEnabledIMEsOrSubtypes(mImm)) {
+ // Virtual codes representing custom requests. These are used in onCustomRequest() below.
+ public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
+
+ @Override
+ public boolean onCustomRequest(int requestCode) {
+ if (isShowingOptionDialog()) return false;
+ switch (requestCode) {
+ case CODE_SHOW_INPUT_METHOD_PICKER:
+ if (ImfUtils.hasMultipleEnabledIMEsOrSubtypes(
+ this, true /* include aux subtypes */)) {
mImm.showInputMethodPicker();
- } else {
- launchSettings();
+ return true;
}
+ return false;
}
+ return false;
}
private boolean isShowingOptionDialog() {
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
+ private static int getActionId(Keyboard keyboard) {
+ return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
+ }
+
+ private void performEditorAction(int actionId) {
+ mConnection.performEditorAction(actionId);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_performEditorAction(actionId);
+ }
+ }
+
+ private void handleLanguageSwitchKey() {
+ final boolean includesOtherImes = mCurrentSettings.mIncludesOtherImesInLanguageSwitchList;
+ final IBinder token = getWindow().getWindow().getAttributes().token;
+ if (mShouldSwitchToLastSubtype) {
+ final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
+ final boolean lastSubtypeBelongsToThisIme =
+ ImfUtils.checkIfSubtypeBelongsToThisImeAndEnabled(this, lastSubtype);
+ if ((includesOtherImes || lastSubtypeBelongsToThisIme)
+ && mImm.switchToLastInputMethod(token)) {
+ mShouldSwitchToLastSubtype = false;
+ } else {
+ mImm.switchToNextInputMethod(token, !includesOtherImes);
+ mShouldSwitchToLastSubtype = true;
+ }
+ } else {
+ mImm.switchToNextInputMethod(token, !includesOtherImes);
+ }
+ }
+
+ private void sendUpDownEnterOrBackspace(final int code) {
+ final long eventTime = SystemClock.uptimeMillis();
+ mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+ mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+ }
+
+ private void sendKeyCodePoint(int code) {
+ // TODO: Remove this special handling of digit letters.
+ // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
+ if (code >= '0' && code <= '9') {
+ super.sendKeyChar((char)code);
+ return;
+ }
+
+ // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+ // we want to be able to compile against the Ice Cream Sandwich SDK.
+ if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
+ && mTargetApplicationInfo.targetSdkVersion < 16) {
+ // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+ // a hardware keyboard event on pressing enter or delete. This is bad for many
+ // reasons (there are race conditions with commits) but some applications are
+ // relying on this behavior so we continue to support it for older apps.
+ sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER);
+ } else {
+ final String text = new String(new int[] { code }, 0, 1);
+ mConnection.commitText(text, text.length());
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_sendKeyCodePoint(code);
+ }
+ }
+
// Implementation of {@link KeyboardActionListener}.
@Override
- public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
- long when = SystemClock.uptimeMillis();
+ public void onCodeInput(int primaryCode, int x, int y) {
+ final long when = SystemClock.uptimeMillis();
if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
mDeleteCount = 0;
}
mLastKeyTime = when;
- KeyboardSwitcher switcher = mKeyboardSwitcher;
- final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
- final boolean lastStateOfJustReplacedDoubleSpace = mJustReplacedDoubleSpace;
- mJustReplacedDoubleSpace = false;
+ mConnection.beginBatchEdit(getCurrentInputConnection());
+
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
+ }
+
+ final KeyboardSwitcher switcher = mKeyboardSwitcher;
+ // The space state depends only on the last character pressed and its own previous
+ // state. Here, we revert the space state to neutral if the key is actually modifying
+ // the input contents (any non-shift key), which is what we should do for
+ // all inputs that do not result in a special state. Each character handling is then
+ // free to override the state as they see fit.
+ final int spaceState = mSpaceState;
+ if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
+
+ // TODO: Consolidate the double space timer, mLastKeyTime, and the space state.
+ if (primaryCode != Keyboard.CODE_SPACE) {
+ mHandler.cancelDoubleSpacesTimer();
+ }
+
+ boolean didAutoCorrect = false;
switch (primaryCode) {
case Keyboard.CODE_DELETE:
- handleBackspace(lastStateOfJustReplacedDoubleSpace);
+ mSpaceState = SPACE_STATE_NONE;
+ handleBackspace(spaceState);
mDeleteCount++;
mExpectingUpdateSelection = true;
- LatinImeLogger.logOnDelete();
+ mShouldSwitchToLastSubtype = true;
+ LatinImeLogger.logOnDelete(x, y);
break;
case Keyboard.CODE_SHIFT:
- // Shift key is handled in onPress() when device has distinct multi-touch panel.
- if (!distinctMultiTouch)
- switcher.toggleShift();
- break;
case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
- // Symbol key is handled in onPress() when device has distinct multi-touch panel.
- if (!distinctMultiTouch)
- switcher.changeKeyboardMode();
- break;
- case Keyboard.CODE_CANCEL:
- if (!isShowingOptionDialog()) {
- handleClose();
- }
+ // Shift and symbol key is handled in onPressKey() and onReleaseKey().
break;
case Keyboard.CODE_SETTINGS:
onSettingsKeyPressed();
break;
- case Keyboard.CODE_SETTINGS_LONGPRESS:
- onSettingsKeyLongPressed();
+ case Keyboard.CODE_SHORTCUT:
+ mSubtypeSwitcher.switchToShortcutIME();
break;
- case LatinKeyboard.CODE_NEXT_LANGUAGE:
- toggleLanguage(true);
+ case Keyboard.CODE_ACTION_ENTER:
+ performEditorAction(getActionId(switcher.getKeyboard()));
break;
- case LatinKeyboard.CODE_PREV_LANGUAGE:
- toggleLanguage(false);
+ case Keyboard.CODE_ACTION_NEXT:
+ performEditorAction(EditorInfo.IME_ACTION_NEXT);
break;
- case Keyboard.CODE_CAPSLOCK:
- switcher.toggleCapsLock();
+ case Keyboard.CODE_ACTION_PREVIOUS:
+ performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
break;
- case Keyboard.CODE_SHORTCUT:
- mSubtypeSwitcher.switchToShortcutIME();
+ case Keyboard.CODE_LANGUAGE_SWITCH:
+ handleLanguageSwitchKey();
break;
- case Keyboard.CODE_TAB:
- handleTab();
- // There are two cases for tab. Either we send a "next" event, that may change the
- // focus but will never move the cursor. Or, we send a real tab keycode, which some
- // applications may accept or ignore, and we don't know whether this will move the
- // cursor or not. So actually, we don't really know.
- // So to go with the safer option, we'd rather behave as if the user moved the
- // cursor when they didn't than the opposite. We also expect that most applications
- // will actually use tab only for focus movement.
- // To sum it up: do not update mExpectingUpdateSelection here.
+ case Keyboard.CODE_RESEARCH:
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().presentResearchDialog(this);
+ }
break;
default:
- if (mSettingsValues.isWordSeparator(primaryCode)) {
- handleSeparator(primaryCode, x, y);
+ if (primaryCode == Keyboard.CODE_TAB && mCurrentSettings.isEditorActionNext()) {
+ performEditorAction(EditorInfo.IME_ACTION_NEXT);
+ break;
+ }
+ mSpaceState = SPACE_STATE_NONE;
+ if (mCurrentSettings.isWordSeparator(primaryCode)) {
+ didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
} else {
- handleCharacter(primaryCode, keyCodes, x, y);
+ final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
+ if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
+ handleCharacter(primaryCode, x, y, spaceState);
+ } else {
+ handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
+ spaceState);
+ }
}
mExpectingUpdateSelection = true;
+ mShouldSwitchToLastSubtype = true;
break;
}
- switcher.onKey(primaryCode);
- // Reset after any single keystroke
+ switcher.onCodeInput(primaryCode);
+ // Reset after any single keystroke, except shift and symbol-shift
+ if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
+ && primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
+ mLastComposedWord.deactivate();
mEnteredText = null;
+ mConnection.endBatchEdit();
}
@Override
public void onTextInput(CharSequence text) {
- mVoiceProxy.commitVoiceInput();
- InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- mRecorrection.abortRecorrection(false);
- ic.beginBatchEdit();
- commitTyped(ic);
- maybeRemovePreviousPeriod(text);
- ic.commitText(text, 1);
- ic.endBatchEdit();
+ mConnection.beginBatchEdit(getCurrentInputConnection());
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ text = specificTldProcessingOnTextInput(text);
+ if (SPACE_STATE_PHANTOM == mSpaceState) {
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
+ }
+ mConnection.commitText(text, 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_commitText(text);
+ }
+ mConnection.endBatchEdit();
mKeyboardSwitcher.updateShiftState();
- mKeyboardSwitcher.onKey(Keyboard.CODE_DUMMY);
- mJustAddedMagicSpace = false;
+ mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
+ mSpaceState = SPACE_STATE_NONE;
mEnteredText = text;
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ }
+
+ private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
+ if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
+ || !Character.isLetter(text.charAt(1))) {
+ // Not a tld: do nothing.
+ return text;
+ }
+ // We have a TLD (or something that looks like this): make sure we don't add
+ // a space even if currently in phantom mode.
+ mSpaceState = SPACE_STATE_NONE;
+ final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
+ return text.subSequence(1, text.length());
+ } else {
+ return text;
+ }
}
@Override
@@ -1172,288 +1351,304 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
mKeyboardSwitcher.onCancelInput();
}
- private void handleBackspace(boolean justReplacedDoubleSpace) {
- if (mVoiceProxy.logAndRevertVoiceInput()) return;
+ private void handleBackspace(final int spaceState) {
+ // In many cases, we may have to put the keyboard in auto-shift state again.
+ mHandler.postUpdateShiftState();
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ic.beginBatchEdit();
-
- mVoiceProxy.handleBackspace();
+ if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
+ // 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.
+ final int length = mEnteredText.length();
+ mConnection.deleteSurroundingText(length, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(length);
+ }
+ // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
+ // In addition we know that spaceState is false, and that we should not be
+ // reverting any autocorrect at this point. So we can safely return.
+ return;
+ }
- boolean deleteChar = false;
- if (mHasUncommittedTypedChars) {
- final int length = mComposing.length();
+ if (mWordComposer.isComposingWord()) {
+ final int length = mWordComposer.size();
if (length > 0) {
- mComposing.delete(length - 1, length);
- mWord.deleteLast();
- ic.setComposingText(mComposing, 1);
- if (mComposing.length() == 0) {
- mHasUncommittedTypedChars = false;
- }
- if (1 == length) {
- // 1 == length means we are about to erase the last character of the word,
- // so we can show bigrams.
+ mWordComposer.deleteLast();
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ // If we have deleted the last remaining character of a word, then we are not
+ // isComposingWord() any more.
+ if (!mWordComposer.isComposingWord()) {
+ // Not composing word any more, so we can show bigrams.
mHandler.postUpdateBigramPredictions();
} else {
- // length > 1, so we still have letters to deduce a suggestion from.
+ // Still composing a word, so we still have letters to deduce a suggestion from.
mHandler.postUpdateSuggestions();
}
} else {
- ic.deleteSurroundingText(1, 0);
+ mConnection.deleteSurroundingText(1, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(1);
+ }
}
} else {
- deleteChar = true;
- }
- mHandler.postUpdateShiftKeyState();
-
- TextEntryState.backspace();
- if (TextEntryState.isUndoCommit()) {
- revertLastWord(deleteChar);
- ic.endBatchEdit();
- return;
- }
- if (justReplacedDoubleSpace) {
- if (revertDoubleSpace()) {
- ic.endBatchEdit();
- return;
+ if (mLastComposedWord.canRevertCommit()) {
+ Utils.Stats.onAutoCorrectionCancellation();
+ revertCommit();
+ return;
+ }
+ if (SPACE_STATE_DOUBLE == spaceState) {
+ mHandler.cancelDoubleSpacesTimer();
+ if (mConnection.revertDoubleSpace()) {
+ // No need to reset mSpaceState, it has already be done (that's why we
+ // receive it as a parameter)
+ return;
+ }
+ } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
+ if (mConnection.revertSwapPunctuation()) {
+ // Likewise
+ return;
+ }
}
- }
- if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
- ic.deleteSurroundingText(mEnteredText.length(), 0);
- } else if (deleteChar) {
- if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
- // Go back to the suggestion mode if the user canceled the
- // "Touch again to save".
- // NOTE: In gerenal, we don't revert the word when backspacing
- // from a manual suggestion pick. We deliberately chose a
- // different behavior only in the case of picking the first
- // suggestion (typed word). It's intentional to have made this
- // inconsistent with backspacing after selecting other suggestions.
- revertLastWord(deleteChar);
+ // No cancelling of commit/double space/swap: we have a regular backspace.
+ // We should backspace one char and restart suggestion if at the end of a word.
+ if (mLastSelectionStart != mLastSelectionEnd) {
+ // If there is a selection, remove it.
+ final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
+ mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
+ }
} else {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ // There is no selection, just delete one character.
+ if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
+ // This should never happen.
+ Log.e(TAG, "Backspace when we don't know the selection position");
+ }
+ // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+ // we want to be able to compile against the Ice Cream Sandwich SDK.
+ if (mTargetApplicationInfo != null
+ && mTargetApplicationInfo.targetSdkVersion < 16) {
+ // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+ // a hardware keyboard event on pressing enter or delete. This is bad for many
+ // reasons (there are race conditions with commits) but some applications are
+ // relying on this behavior so we continue to support it for older apps.
+ sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL);
+ } else {
+ mConnection.deleteSurroundingText(1, 0);
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(1);
+ }
if (mDeleteCount > DELETE_ACCELERATE_AT) {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ mConnection.deleteSurroundingText(1, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(1);
+ }
}
}
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
+ }
}
- ic.endBatchEdit();
}
- private void handleTab() {
- final int imeOptions = getCurrentInputEditorInfo().imeOptions;
- if (!EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
- && !EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)) {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_TAB);
- return;
- }
-
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null)
- return;
-
- // True if keyboard is in either chording shift or manual temporary upper case mode.
- final boolean isManualTemporaryUpperCase = mKeyboardSwitcher.isManualTemporaryUpperCase();
- if (EditorInfoCompatUtils.hasFlagNavigateNext(imeOptions)
- && !isManualTemporaryUpperCase) {
- EditorInfoCompatUtils.performEditorActionNext(ic);
- } else if (EditorInfoCompatUtils.hasFlagNavigatePrevious(imeOptions)
- && isManualTemporaryUpperCase) {
- EditorInfoCompatUtils.performEditorActionPrevious(ic);
+ private boolean maybeStripSpace(final int code,
+ final int spaceState, final boolean isFromSuggestionStrip) {
+ if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
+ mConnection.removeTrailingSpace();
+ return false;
+ } else if ((SPACE_STATE_WEAK == spaceState
+ || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
+ && isFromSuggestionStrip) {
+ if (mCurrentSettings.isWeakSpaceSwapper(code)) {
+ return true;
+ } else {
+ if (mCurrentSettings.isWeakSpaceStripper(code)) {
+ mConnection.removeTrailingSpace();
+ }
+ return false;
+ }
+ } else {
+ return false;
}
}
- private void handleCharacter(int primaryCode, int[] keyCodes, int x, int y) {
- mVoiceProxy.handleCharacter();
+ private void handleCharacter(final int primaryCode, final int x,
+ final int y, final int spaceState) {
+ boolean isComposingWord = mWordComposer.isComposingWord();
- if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceStripper(primaryCode)) {
- removeTrailingSpace();
- }
-
- if (mLastSelectionStart == mLastSelectionEnd) {
- mRecorrection.abortRecorrection(false);
- }
-
- int code = primaryCode;
- if (isAlphabet(code) && isSuggestionsRequested() && !isCursorTouchingWord()) {
- if (!mHasUncommittedTypedChars) {
- mHasUncommittedTypedChars = true;
- mComposing.setLength(0);
- mRecorrection.saveRecorrectionSuggestion(mWord, mBestWord);
- mWord.reset();
- clearSuggestions();
- }
- }
- final KeyboardSwitcher switcher = mKeyboardSwitcher;
- if (switcher.isShiftedOrShiftLocked()) {
- if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT
- || keyCodes[0] > Character.MAX_CODE_POINT) {
- return;
+ if (SPACE_STATE_PHANTOM == spaceState &&
+ !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
+ if (isComposingWord) {
+ // Sanity check
+ throw new RuntimeException("Should not be composing here");
}
- code = keyCodes[0];
- if (switcher.isAlphabetMode() && Character.isLowerCase(code)) {
- // In some locales, such as Turkish, Character.toUpperCase() may return a wrong
- // character because it doesn't take care of locale.
- final String upperCaseString = new String(new int[] {code}, 0, 1)
- .toUpperCase(mSubtypeSwitcher.getInputLocale());
- if (upperCaseString.codePointCount(0, upperCaseString.length()) == 1) {
- code = upperCaseString.codePointAt(0);
- } else {
- // Some keys, such as [eszett], have upper case as multi-characters.
- onTextInput(upperCaseString);
- return;
- }
- }
- }
- if (mHasUncommittedTypedChars) {
- if (mComposing.length() == 0 && switcher.isAlphabetMode()
- && switcher.isShiftedOrShiftLocked()) {
- mWord.setFirstCharCapitalized(true);
- }
- mComposing.append((char) code);
- mWord.add(code, keyCodes, x, y);
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- // If it's the first letter, make note of auto-caps state
- if (mWord.size() == 1) {
- mWord.setAutoCapitalized(getCurrentAutoCapsState());
- }
- ic.setComposingText(mComposing, 1);
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
+ }
+
+ // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
+ // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
+ // thread here.
+ if (!isComposingWord && (isAlphabet(primaryCode)
+ || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
+ && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
+ !mConnection.isCursorTouchingWord(mCurrentSettings)) {
+ // Reset entirely the composing state anyway, then start composing a new word unless
+ // the character is a single quote. The idea here is, single quote is not a
+ // separator and it should be treated as a normal character, except in the first
+ // position where it should not start composing a word.
+ isComposingWord = (Keyboard.CODE_SINGLE_QUOTE != primaryCode);
+ // Here we don't need to reset the last composed word. It will be reset
+ // when we commit this one, if we ever do; if on the other hand we backspace
+ // it entirely and resume suggestions on the previous word, we'd like to still
+ // have touch coordinates for it.
+ resetComposingState(false /* alsoResetLastComposedWord */);
+ clearSuggestions();
+ }
+ if (isComposingWord) {
+ mWordComposer.add(
+ primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
+ // If it's the first letter, make note of auto-caps state
+ if (mWordComposer.size() == 1) {
+ mWordComposer.setAutoCapitalized(
+ getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
}
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
mHandler.postUpdateSuggestions();
} else {
- sendKeyChar((char)code);
- }
- if (mJustAddedMagicSpace && mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
- swapSwapperAndSpace();
- } else {
- mJustAddedMagicSpace = false;
- }
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode,
+ spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
- switcher.updateShiftState();
- if (LatinIME.PERF_DEBUG) measureCps();
- TextEntryState.typedCharacter((char) code, mSettingsValues.isWordSeparator(code), x, y);
- }
+ sendKeyCodePoint(primaryCode);
- private void handleSeparator(int primaryCode, int x, int y) {
- mVoiceProxy.handleSeparator();
+ if (swapWeakSpace) {
+ swapSwapperAndSpace();
+ mSpaceState = SPACE_STATE_WEAK;
+ }
+ // Some characters are not word separators, yet they don't start a new
+ // composing span. For these, we haven't changed the suggestion strip, and
+ // if the "add to dictionary" hint is shown, we should do so now. Examples of
+ // such characters include single quote, dollar, and others; the exact list is
+ // the list of characters for which we enter handleCharacterWhileInBatchEdit
+ // that don't match the test if ((isAlphabet...)) at the top of this method.
+ if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
+ mHandler.postUpdateBigramPredictions();
+ }
+ }
+ Utils.Stats.onNonSeparator((char)primaryCode, x, y);
+ }
+ // Returns true if we did an autocorrection, false otherwise.
+ private boolean handleSeparator(final int primaryCode, final int x, final int y,
+ final int spaceState) {
// Should dismiss the "Touch again to save" message when handling separator
- if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
+ if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
mHandler.cancelUpdateBigramPredictions();
mHandler.postUpdateSuggestions();
}
- boolean pickedDefault = false;
+ boolean didAutoCorrect = false;
// Handle separator
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.beginBatchEdit();
- mRecorrection.abortRecorrection(false);
- }
- if (mHasUncommittedTypedChars) {
+ if (mWordComposer.isComposingWord()) {
// In certain languages where single quote is a separator, it's better
// not to auto correct, but accept the typed word. For instance,
// in Italian dov' should not be expanded to dove' because the elision
// requires the last vowel to be removed.
- final boolean shouldAutoCorrect =
- (mSettingsValues.mAutoCorrectEnabled || mSettingsValues.mQuickFixes)
- && !mInputTypeNoAutoCorrect && mHasDictionary;
- if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
- pickedDefault = pickDefaultSuggestion(primaryCode);
+ if (mCurrentSettings.mCorrectionEnabled && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
+ commitCurrentAutoCorrection(primaryCode);
+ didAutoCorrect = true;
} else {
- commitTyped(ic);
+ commitTyped(primaryCode);
}
}
- if (mJustAddedMagicSpace) {
- if (mSettingsValues.isMagicSpaceSwapper(primaryCode)) {
- sendKeyChar((char)primaryCode);
- swapSwapperAndSpace();
- } else {
- if (mSettingsValues.isMagicSpaceStripper(primaryCode)) removeTrailingSpace();
- sendKeyChar((char)primaryCode);
- mJustAddedMagicSpace = false;
- }
- } else {
- sendKeyChar((char)primaryCode);
- }
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
+ KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
- if (isSuggestionsRequested() && primaryCode == Keyboard.CODE_SPACE) {
- maybeDoubleSpace();
+ if (SPACE_STATE_PHANTOM == spaceState &&
+ mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
}
+ sendKeyCodePoint(primaryCode);
- TextEntryState.typedCharacter((char) primaryCode, true, x, y);
-
- if (pickedDefault) {
- CharSequence typedWord = mWord.getTypedWord();
- TextEntryState.backToAcceptedDefault(typedWord);
- if (!TextUtils.isEmpty(typedWord) && !typedWord.equals(mBestWord)) {
- InputConnectionCompatUtils.commitCorrection(
- ic, mLastSelectionEnd - typedWord.length(), typedWord, mBestWord);
- if (mCandidateView != null)
- mCandidateView.onAutoCorrectionInverted(mBestWord);
- }
- }
if (Keyboard.CODE_SPACE == primaryCode) {
- if (!isCursorTouchingWord()) {
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ if (maybeDoubleSpace()) {
+ mSpaceState = SPACE_STATE_DOUBLE;
+ } else if (!isShowingPunctuationList()) {
+ mSpaceState = SPACE_STATE_WEAK;
+ }
+ }
+
+ mHandler.startDoubleSpacesTimer();
+ if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
mHandler.cancelUpdateSuggestions();
- mHandler.cancelUpdateOldSuggestions();
mHandler.postUpdateBigramPredictions();
}
} else {
+ if (swapWeakSpace) {
+ swapSwapperAndSpace();
+ mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
+ } else if (SPACE_STATE_PHANTOM == spaceState) {
+ // If we are in phantom space state, and the user presses a separator, we want to
+ // stay in phantom space state so that the next keypress has a chance to add the
+ // space. For example, if I type "Good dat", pick "day" from the suggestion strip
+ // then insert a comma and go on to typing the next word, I want the space to be
+ // inserted automatically before the next word, the same way it is when I don't
+ // input the comma.
+ mSpaceState = SPACE_STATE_PHANTOM;
+ }
+
// Set punctuation right away. onUpdateSelection will fire but tests whether it is
// already displayed or not, so it's okay.
setPunctuationSuggestions();
}
- mKeyboardSwitcher.updateShiftState();
- if (ic != null) {
- ic.endBatchEdit();
- }
+
+ Utils.Stats.onSeparator((char)primaryCode, x, y);
+
+ return didAutoCorrect;
+ }
+
+ private CharSequence getTextWithUnderline(final CharSequence text) {
+ return mIsAutoCorrectionIndicatorOn
+ ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
+ : text;
}
private void handleClose() {
- commitTyped(getCurrentInputConnection());
- mVoiceProxy.handleClose();
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
requestHideSelf(0);
LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
if (inputView != null)
inputView.closing();
}
- public boolean isSuggestionsRequested() {
- return mIsSettingsSuggestionStripOn
- && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
- }
-
public boolean isShowingPunctuationList() {
- return mSettingsValues.mSuggestPuncList == mCandidateView.getSuggestions();
+ if (mSuggestionsView == null) return false;
+ return mCurrentSettings.mSuggestPuncList == mSuggestionsView.getSuggestions();
}
- public boolean isShowingSuggestionsStrip() {
- return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
- || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- && mOrientation == Configuration.ORIENTATION_PORTRAIT);
- }
-
- public boolean isCandidateStripVisible() {
- if (mCandidateView == null)
+ public boolean isSuggestionsStripVisible() {
+ if (mSuggestionsView == null)
return false;
- if (mCandidateView.isShowingAddToDictionaryHint() || TextEntryState.isRecorrecting())
+ if (mSuggestionsView.isShowingAddToDictionaryHint())
return true;
- if (!isShowingSuggestionsStrip())
+ if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
return false;
- if (mApplicationSpecifiedCompletionOn)
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
return true;
- return isSuggestionsRequested();
+ return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
}
public void switchToKeyboardView() {
if (DEBUG) {
Log.d(TAG, "Switch to keyboard view.");
}
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_switchToKeyboardView();
+ }
View v = mKeyboardSwitcher.getKeyboardView();
if (v != null) {
// Confirms that the keyboard view doesn't have parent view.
@@ -1463,56 +1658,59 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
setInputView(v);
}
- setSuggestionStripShown(isCandidateStripVisible());
+ setSuggestionStripShown(isSuggestionsStripVisible());
updateInputViewShown();
mHandler.postUpdateSuggestions();
}
public void clearSuggestions() {
- setSuggestions(SuggestedWords.EMPTY);
+ setSuggestions(SuggestedWords.EMPTY, false);
+ setAutoCorrectionIndicator(false);
}
- public void setSuggestions(SuggestedWords words) {
- if (mCandidateView != null) {
- mCandidateView.setSuggestions(words);
- mKeyboardSwitcher.onAutoCorrectionStateChanged(
- words.hasWordAboveAutoCorrectionScoreThreshold());
+ private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
+ if (mSuggestionsView != null) {
+ mSuggestionsView.setSuggestions(words);
+ mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
+ }
+ }
+
+ private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
+ // Put a blue underline to a word in TextView which will be auto-corrected.
+ if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
+ && mWordComposer.isComposingWord()) {
+ mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
+ final CharSequence textWithUnderline =
+ getTextWithUnderline(mWordComposer.getTypedWord());
+ mConnection.setComposingText(textWithUnderline, 1);
}
}
public void updateSuggestions() {
// Check if we have a suggestion engine attached.
- if ((mSuggest == null || !isSuggestionsRequested())
- && !mVoiceProxy.isVoiceInputHighlighted()) {
+ if ((mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation))) {
+ if (mWordComposer.isComposingWord()) {
+ Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
+ mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+ }
return;
}
- if (!mHasUncommittedTypedChars) {
+ mHandler.cancelUpdateSuggestions();
+ mHandler.cancelUpdateBigramPredictions();
+
+ if (!mWordComposer.isComposingWord()) {
setPunctuationSuggestions();
return;
}
- showSuggestions(mWord);
- }
- private void showSuggestions(WordComposer word) {
// TODO: May need a better way of retrieving previous word
- CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
- mSettingsValues.mWordSeparators);
- SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
- mKeyboardSwitcher.getKeyboardView(), word, prevWord);
-
- boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasAutoCorrection();
- final CharSequence typedWord = word.getTypedWord();
- // Here, we want to promote a whitelisted word if exists.
- final boolean typedWordValid = AutoCorrection.isValidWordForAutoCorrection(
- mSuggest.getUnigramDictionaries(), typedWord, preferCapitalization());
- if (mCorrectionMode == Suggest.CORRECTION_FULL
- || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
- correctionAvailable |= typedWordValid;
- }
- // Don't auto-correct words with multiple capital letter
- correctionAvailable &= !word.isMostlyCaps();
- correctionAvailable &= !TextEntryState.isRecorrecting();
+ final CharSequence prevWord = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
+ final CharSequence typedWord = mWordComposer.getTypedWord();
+ // getSuggestedWords handles gracefully a null value of prevWord
+ final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
+ prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
+ mCurrentSettings.mCorrectionEnabled);
// Basically, we update the suggestion strip only when suggestion count > 1. However,
// there is an exception: We update the suggestion strip whenever typed word's length
@@ -1520,151 +1718,159 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
// in most cases, suggestion count is 1 when typed word's length is 1, but we do always
// need to clear the previous state when the user starts typing a word (i.e. typed word's
// length == 1).
- if (typedWord != null) {
- if (builder.size() > 1 || typedWord.length() == 1 || typedWordValid
- || mCandidateView.isShowingAddToDictionaryHint()) {
- builder.setTypedWordValid(typedWordValid).setHasMinimalSuggestion(
- correctionAvailable);
- } else {
- final SuggestedWords previousSuggestions = mCandidateView.getSuggestions();
- if (previousSuggestions == mSettingsValues.mSuggestPuncList)
- return;
- builder.addTypedWordAndPreviousSuggestions(typedWord, previousSuggestions);
+ if (suggestedWords.size() > 1 || typedWord.length() == 1
+ || !suggestedWords.mAllowsToBeAutoCorrected
+ || mSuggestionsView.isShowingAddToDictionaryHint()) {
+ showSuggestions(suggestedWords, typedWord);
+ } else {
+ SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
+ if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
+ previousSuggestions = SuggestedWords.EMPTY;
}
- }
- showSuggestions(builder.build(), typedWord);
- }
-
- public void showSuggestions(SuggestedWords suggestedWords, CharSequence typedWord) {
- setSuggestions(suggestedWords);
+ final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
+ SuggestedWords.getTypedWordAndPreviousSuggestions(
+ typedWord, previousSuggestions);
+ final SuggestedWords obsoleteSuggestedWords =
+ new SuggestedWords(typedWordAndPreviousSuggestions,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* allowsToBeAutoCorrected */,
+ false /* isPunctuationSuggestions */,
+ true /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ showSuggestions(obsoleteSuggestedWords, typedWord);
+ }
+ }
+
+ public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
+ final CharSequence autoCorrection;
if (suggestedWords.size() > 0) {
- if (Utils.shouldBlockedBySafetyNetForAutoCorrection(suggestedWords, mSuggest)) {
- mBestWord = typedWord;
- } else if (suggestedWords.hasAutoCorrectionWord()) {
- mBestWord = suggestedWords.getWord(1);
+ if (suggestedWords.hasAutoCorrectionWord()) {
+ autoCorrection = suggestedWords.getWord(1);
} else {
- mBestWord = typedWord;
+ autoCorrection = typedWord;
}
} else {
- mBestWord = null;
+ autoCorrection = null;
}
- setSuggestionStripShown(isCandidateStripVisible());
+ mWordComposer.setAutoCorrection(autoCorrection);
+ final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
+ setSuggestions(suggestedWords, isAutoCorrection);
+ setAutoCorrectionIndicator(isAutoCorrection);
+ setSuggestionStripShown(isSuggestionsStripVisible());
}
- private boolean pickDefaultSuggestion(int separatorCode) {
- // Complete any pending candidate query first
+ private void commitCurrentAutoCorrection(final int separatorCodePoint) {
+ // Complete any pending suggestions query first
if (mHandler.hasPendingUpdateSuggestions()) {
mHandler.cancelUpdateSuggestions();
updateSuggestions();
}
- if (mBestWord != null && mBestWord.length() > 0) {
- TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord, separatorCode);
+ final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ if (autoCorrection != null) {
+ final String typedWord = mWordComposer.getTypedWord();
+ if (TextUtils.isEmpty(typedWord)) {
+ 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(), separatorCodePoint);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
+ autoCorrection.toString());
+ }
mExpectingUpdateSelection = true;
- commitBestWord(mBestWord);
- // Add the word to the auto dictionary if it's not a known word
- addToAutoAndUserBigramDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED);
- return true;
+ commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
+ separatorCodePoint);
+ if (!typedWord.equals(autoCorrection)) {
+ // This will make the correction flash for a short while as a visual clue
+ // to the user that auto-correction happened.
+ mConnection.commitCorrection(
+ new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+ typedWord, autoCorrection));
+ }
}
- return false;
}
@Override
- public void pickSuggestionManually(int index, CharSequence suggestion) {
- SuggestedWords suggestions = mCandidateView.getSuggestions();
- mVoiceProxy.flushAndLogAllTextModificationCounters(index, suggestion,
- mSettingsValues.mWordSeparators);
+ public void pickSuggestionManually(final int index, final CharSequence suggestion,
+ final int x, final int y) {
+ final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+ // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
+ if (suggestion.length() == 1 && isShowingPunctuationList()) {
+ // Word separators are suggested before the user inputs something.
+ // So, LatinImeLogger logs "" as a user's input.
+ LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
+ // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
+ }
+ final int primaryCode = suggestion.charAt(0);
+ onCodeInput(primaryCode,
+ KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
+ KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
+ return;
+ }
- final boolean recorrecting = TextEntryState.isRecorrecting();
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.beginBatchEdit();
+ if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
+ int firstChar = Character.codePointAt(suggestion, 0);
+ if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
+ && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
+ }
}
- if (mApplicationSpecifiedCompletionOn && mApplicationSpecifiedCompletions != null
+
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
+ && mApplicationSpecifiedCompletions != null
&& index >= 0 && index < mApplicationSpecifiedCompletions.length) {
- CompletionInfo ci = mApplicationSpecifiedCompletions[index];
- if (ic != null) {
- ic.commitCompletion(ci);
- }
- mCommittedLength = suggestion.length();
- if (mCandidateView != null) {
- mCandidateView.clear();
+ if (mSuggestionsView != null) {
+ mSuggestionsView.clear();
}
mKeyboardSwitcher.updateShiftState();
- if (ic != null) {
- ic.endBatchEdit();
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
+ mConnection.beginBatchEdit(getCurrentInputConnection());
+ mConnection.commitCompletion(completionInfo);
+ mConnection.endBatchEdit();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
+ completionInfo.getText(), x, y);
}
return;
}
- // If this is a punctuation, apply it through the normal key press
- if (suggestion.length() == 1 && (mSettingsValues.isWordSeparator(suggestion.charAt(0))
- || mSettingsValues.isSuggestedPunctuation(suggestion.charAt(0)))) {
- // Word separators are suggested before the user inputs something.
- // So, LatinImeLogger logs "" as a user's input.
- LatinImeLogger.logOnManualSuggestion(
- "", suggestion.toString(), index, suggestions.mWords);
- // Find out whether the previous character is a space. If it is, as a special case
- // for punctuation entered through the suggestion strip, it should be considered
- // a magic space even if it was a normal space. This is meant to help in case the user
- // pressed space on purpose of displaying the suggestion strip punctuation.
- final char primaryCode = suggestion.charAt(0);
- final CharSequence beforeText = ic != null ? ic.getTextBeforeCursor(1, 0) : "";
- final int toLeft = (ic == null || TextUtils.isEmpty(beforeText))
- ? 0 : beforeText.charAt(0);
- final boolean oldMagicSpace = mJustAddedMagicSpace;
- if (Keyboard.CODE_SPACE == toLeft) mJustAddedMagicSpace = true;
- onCodeInput(primaryCode, new int[] { primaryCode },
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
- mJustAddedMagicSpace = oldMagicSpace;
- if (ic != null) {
- ic.endBatchEdit();
- }
- return;
- }
- if (!mHasUncommittedTypedChars) {
- // If we are not composing a word, then it was a suggestion inferred from
- // context - no user input. We should reset the word composer.
- mWord.reset();
+ // We need to log before we commit, because the word composer will store away the user
+ // typed word.
+ final String replacedWord = mWordComposer.getTypedWord().toString();
+ LatinImeLogger.logOnManualSuggestion(replacedWord,
+ suggestion.toString(), index, suggestedWords);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
}
mExpectingUpdateSelection = true;
- commitBestWord(suggestion);
- // Add the word to the auto dictionary if it's not a known word
- if (index == 0) {
- addToAutoAndUserBigramDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED);
- } else {
- addToOnlyBigramDictionary(suggestion, 1);
- }
- LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(),
- index, suggestions.mWords);
- TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion);
- // Follow it with a space
- if (mShouldInsertMagicSpace && !recorrecting) {
- sendMagicSpace();
- }
+ commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
+ LastComposedWord.NOT_A_SEPARATOR);
+ // Don't allow cancellation of manual pick
+ mLastComposedWord.deactivate();
+ mSpaceState = SPACE_STATE_PHANTOM;
+ // TODO: is this necessary?
+ mKeyboardSwitcher.updateShiftState();
- // We should show the hint if the user pressed the first entry AND either:
+ // We should show the "Touch again to save" hint if the user pressed the first entry
+ // AND either:
// - There is no dictionary (we know that because we tried to load it => null != mSuggest
- // AND mHasDictionary is false)
+ // AND mSuggest.hasMainDictionary() is false)
// - There is a dictionary and the word is not in it
// Please note that if mSuggest is null, it means that everything is off: suggestion
// and correction, so we shouldn't try to show the hint
- // We used to look at mCorrectionMode here, but showing the hint should have nothing
- // to do with the autocorrection setting.
final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
// If there is no dictionary the hint should be shown.
- && (!mHasDictionary
+ && (!mSuggest.hasMainDictionary()
// If "suggestion" is not in the dictionary, the hint should be shown.
|| !AutoCorrection.isValidWord(
mSuggest.getUnigramDictionaries(), suggestion, true));
- if (!recorrecting) {
- // Fool the state watcher so that a subsequent backspace will not do a revert, unless
- // we just did a correction, in which case we need to stay in
- // TextEntryState.State.PICKED_SUGGESTION state.
- TextEntryState.typedCharacter((char) Keyboard.CODE_SPACE, true,
- WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
- }
+ Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
+ WordComposer.NOT_A_COORDINATE);
if (!showingAddToDictionaryHint) {
// If we're not showing the "Touch again to save", then show corrections again.
// In case the cursor position doesn't change, make sure we show the suggestions again.
@@ -1672,377 +1878,271 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
// Updating the predictions right away may be slow and feel unresponsive on slower
// terminals. On the other hand if we just postUpdateBigramPredictions() it will
// take a noticeable delay to update them which may feel uneasy.
- }
- if (showingAddToDictionaryHint) {
- mCandidateView.showAddToDictionaryHint(suggestion);
- }
- if (ic != null) {
- ic.endBatchEdit();
+ } else {
+ if (mIsUserDictionaryAvailable) {
+ mSuggestionsView.showAddToDictionaryHint(
+ suggestion, mCurrentSettings.mHintToSaveText);
+ } else {
+ mHandler.postUpdateSuggestions();
+ }
}
}
/**
- * Commits the chosen word to the text field and saves it for later
- * retrieval.
+ * Commits the chosen word to the text field and saves it for later retrieval.
*/
- private void commitBestWord(CharSequence bestWord) {
- KeyboardSwitcher switcher = mKeyboardSwitcher;
- if (!switcher.isKeyboardAvailable())
- return;
- InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
- SuggestedWords suggestedWords = mCandidateView.getSuggestions();
- ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
- this, bestWord, suggestedWords), 1);
- }
- mRecorrection.saveRecorrectionSuggestion(mWord, bestWord);
- mHasUncommittedTypedChars = false;
- mCommittedLength = bestWord.length();
+ private void commitChosenWord(final CharSequence chosenWord, final int commitType,
+ final int separatorCode) {
+ final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+ mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+ this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_commitText(chosenWord);
+ }
+ // Add the word to the user history dictionary
+ final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
+ // TODO: figure out here if this is an auto-correct or if the best word is actually
+ // what user typed. Note: currently this is done much later in
+ // LastComposedWord#didCommitTypedWord by string equality of the remembered
+ // strings.
+ mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
+ separatorCode, prevWord);
}
- private static final WordComposer sEmptyWordComposer = new WordComposer();
public void updateBigramPredictions() {
- if (mSuggest == null || !isSuggestionsRequested())
+ if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation))
return;
- if (!mSettingsValues.mBigramPredictionEnabled) {
+ if (!mCurrentSettings.mBigramPredictionEnabled) {
setPunctuationSuggestions();
return;
}
- final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
- mSettingsValues.mWordSeparators);
- SuggestedWords.Builder builder = mSuggest.getSuggestedWordBuilder(
- mKeyboardSwitcher.getKeyboardView(), sEmptyWordComposer, prevWord);
+ final SuggestedWords suggestedWords;
+ if (mCurrentSettings.mCorrectionEnabled) {
+ final CharSequence prevWord = mConnection.getThisWord(mCurrentSettings.mWordSeparators);
+ if (!TextUtils.isEmpty(prevWord)) {
+ suggestedWords = mSuggest.getBigramPredictions(prevWord);
+ } else {
+ suggestedWords = null;
+ }
+ } else {
+ suggestedWords = null;
+ }
- if (builder.size() > 0) {
+ if (null != suggestedWords && suggestedWords.size() > 0) {
// Explicitly supply an empty typed word (the no-second-arg version of
// showSuggestions will retrieve the word near the cursor, we don't want that here)
- showSuggestions(builder.build(), "");
+ showSuggestions(suggestedWords, "");
} else {
- if (!isShowingPunctuationList()) setPunctuationSuggestions();
+ clearSuggestions();
}
}
public void setPunctuationSuggestions() {
- setSuggestions(mSettingsValues.mSuggestPuncList);
- setSuggestionStripShown(isCandidateStripVisible());
- }
-
- private void addToAutoAndUserBigramDictionaries(CharSequence suggestion, int frequencyDelta) {
- checkAddToDictionary(suggestion, frequencyDelta, false);
- }
-
- private void addToOnlyBigramDictionary(CharSequence suggestion, int frequencyDelta) {
- checkAddToDictionary(suggestion, frequencyDelta, true);
+ if (mCurrentSettings.mBigramPredictionEnabled) {
+ clearSuggestions();
+ } else {
+ setSuggestions(mCurrentSettings.mSuggestPuncList, false);
+ }
+ setAutoCorrectionIndicator(false);
+ setSuggestionStripShown(isSuggestionsStripVisible());
}
- /**
- * Adds to the UserBigramDictionary and/or AutoDictionary
- * @param selectedANotTypedWord true if it should be added to bigram dictionary if possible
- */
- private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta,
- boolean selectedANotTypedWord) {
- if (suggestion == null || suggestion.length() < 1) return;
-
- // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
- // adding words in situations where the user or application really didn't
- // want corrections enabled or learned.
- if (!(mCorrectionMode == Suggest.CORRECTION_FULL
- || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
- return;
- }
+ private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
+ if (TextUtils.isEmpty(suggestion)) return null;
- final boolean selectedATypedWordAndItsInAutoDic =
- !selectedANotTypedWord && mAutoDictionary.isValidWord(suggestion);
- final boolean isValidWord = AutoCorrection.isValidWord(
- mSuggest.getUnigramDictionaries(), suggestion, true);
- final boolean needsToAddToAutoDictionary = selectedATypedWordAndItsInAutoDic
- || !isValidWord;
- if (needsToAddToAutoDictionary) {
- mAutoDictionary.addWord(suggestion.toString(), frequencyDelta);
- }
+ // If correction is not enabled, we don't add words to the user history dictionary.
+ // That's to avoid unintended additions in some sensitive fields, or fields that
+ // expect to receive non-words.
+ if (!mCurrentSettings.mCorrectionEnabled) return null;
- if (mUserBigramDictionary != null) {
- // We don't want to register as bigrams words separated by a separator.
- // For example "I will, and you too" : we don't want the pair ("will" "and") to be
- // a bigram.
- CharSequence prevWord = EditingUtils.getPreviousWord(getCurrentInputConnection(),
- mSettingsValues.mWordSeparators);
- if (!TextUtils.isEmpty(prevWord)) {
- mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString());
+ final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
+ if (userHistoryDictionary != null) {
+ final CharSequence prevWord
+ = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
+ final String secondWord;
+ if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+ secondWord = suggestion.toString().toLowerCase(
+ mSubtypeSwitcher.getCurrentSubtypeLocale());
+ } else {
+ secondWord = suggestion.toString();
}
+ // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
+ // We don't add words with 0-frequency (assuming they would be profanity etc.).
+ final int maxFreq = AutoCorrection.getMaxFrequency(
+ mSuggest.getUnigramDictionaries(), suggestion);
+ if (maxFreq == 0) return null;
+ userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
+ secondWord, maxFreq > 0);
+ return prevWord;
}
+ return null;
}
- public boolean isCursorTouchingWord() {
- InputConnection ic = getCurrentInputConnection();
- if (ic == null) return false;
- CharSequence toLeft = ic.getTextBeforeCursor(1, 0);
- CharSequence toRight = ic.getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(toLeft)
- && !mSettingsValues.isWordSeparator(toLeft.charAt(0))
- && !mSettingsValues.isSuggestedPunctuation(toLeft.charAt(0))) {
- return true;
- }
- if (!TextUtils.isEmpty(toRight)
- && !mSettingsValues.isWordSeparator(toRight.charAt(0))
- && !mSettingsValues.isSuggestedPunctuation(toRight.charAt(0))) {
- return true;
+ /**
+ * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
+ * word, else do nothing.
+ */
+ private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
+ final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+ if (null != word) {
+ restartSuggestionsOnWordBeforeCursor(word);
}
- return false;
}
- private boolean sameAsTextBeforeCursor(InputConnection ic, CharSequence text) {
- CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
- return TextUtils.equals(text, beforeText);
+ private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
+ mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
+ final int length = word.length();
+ mConnection.deleteSurroundingText(length, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(length);
+ }
+ mConnection.setComposingText(word, 1);
+ mHandler.postUpdateSuggestions();
}
- public void revertLastWord(boolean deleteChar) {
- final int length = mComposing.length();
- if (!mHasUncommittedTypedChars && length > 0) {
- final InputConnection ic = getCurrentInputConnection();
- final CharSequence punctuation = ic.getTextBeforeCursor(1, 0);
- if (deleteChar) ic.deleteSurroundingText(1, 0);
- int toDelete = mCommittedLength;
- final CharSequence toTheLeft = ic.getTextBeforeCursor(mCommittedLength, 0);
- if (!TextUtils.isEmpty(toTheLeft)
- && mSettingsValues.isWordSeparator(toTheLeft.charAt(0))) {
- toDelete--;
+ private void revertCommit() {
+ final CharSequence previousWord = mLastComposedWord.mPrevWord;
+ final String originallyTypedWord = mLastComposedWord.mTypedWord;
+ final CharSequence committedWord = mLastComposedWord.mCommittedWord;
+ final int cancelLength = committedWord.length();
+ final int separatorLength = LastComposedWord.getSeparatorLength(
+ mLastComposedWord.mSeparatorCode);
+ // TODO: should we check our saved separator against the actual contents of the text view?
+ final int deleteLength = cancelLength + separatorLength;
+ if (DEBUG) {
+ if (mWordComposer.isComposingWord()) {
+ throw new RuntimeException("revertCommit, but we are composing a word");
}
- ic.deleteSurroundingText(toDelete, 0);
- // Re-insert punctuation only when the deleted character was word separator and the
- // composing text wasn't equal to the auto-corrected text.
- if (deleteChar
- && !TextUtils.isEmpty(punctuation)
- && mSettingsValues.isWordSeparator(punctuation.charAt(0))
- && !TextUtils.equals(mComposing, toTheLeft)) {
- ic.commitText(mComposing, 1);
- TextEntryState.acceptedTyped(mComposing);
- ic.commitText(punctuation, 1);
- TextEntryState.typedCharacter(punctuation.charAt(0), true,
- WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
- // Clear composing text
- mComposing.setLength(0);
- } else {
- mHasUncommittedTypedChars = true;
- ic.setComposingText(mComposing, 1);
- TextEntryState.backspace();
+ final String wordBeforeCursor =
+ mConnection.getTextBeforeCursor(deleteLength, 0)
+ .subSequence(0, cancelLength).toString();
+ if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
+ throw new RuntimeException("revertCommit check failed: we thought we were "
+ + "reverting \"" + committedWord
+ + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
}
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
+ }
+ mConnection.deleteSurroundingText(deleteLength, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
+ }
+ if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
+ mUserHistoryDictionary.cancelAddingUserHistory(
+ previousWord.toString(), committedWord.toString());
+ }
+ if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
+ // This is the case when we cancel a manual pick.
+ // We should restart suggestion on the word right away.
+ mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
+ mConnection.setComposingText(originallyTypedWord, 1);
} else {
- sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL);
+ mConnection.commitText(originallyTypedWord, 1);
+ // Re-insert the separator
+ sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
+ Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
+ WordComposer.NOT_A_COORDINATE);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_revertCommit(originallyTypedWord);
+ }
+ // Don't restart suggestion yet. We'll restart if the user deletes the
+ // separator.
}
- }
-
- public boolean revertDoubleSpace() {
- mHandler.cancelDoubleSpacesTimer();
- final InputConnection ic = getCurrentInputConnection();
- // Here we test whether we indeed have a period and a space before us. This should not
- // be needed, but it's there just in case something went wrong.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
- if (!". ".equals(textBeforeCursor))
- return false;
- ic.beginBatchEdit();
- ic.deleteSurroundingText(2, 0);
- ic.commitText(" ", 1);
- ic.endBatchEdit();
- return true;
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ mHandler.cancelUpdateBigramPredictions();
+ mHandler.postUpdateSuggestions();
}
public boolean isWordSeparator(int code) {
- return mSettingsValues.isWordSeparator(code);
- }
-
- private void sendMagicSpace() {
- sendKeyChar((char)Keyboard.CODE_SPACE);
- mJustAddedMagicSpace = true;
- mKeyboardSwitcher.updateShiftState();
+ return mCurrentSettings.isWordSeparator(code);
}
public boolean preferCapitalization() {
- return mWord.isFirstCharCapitalized();
+ return mWordComposer.isFirstCharCapitalized();
}
// Notify that language or mode have been changed and toggleLanguage will update KeyboardID
// according to new language or mode.
public void onRefreshKeyboard() {
- if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
- // Before Honeycomb, Voice IME is in LatinIME and it changes the current input view,
- // so that we need to re-create the keyboard input view here.
- setInputView(mKeyboardSwitcher.onCreateInputView());
- }
- // Reload keyboard because the current language has been changed.
- mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(),
- mSubtypeSwitcher.isShortcutImeEnabled() && mVoiceProxy.isVoiceButtonEnabled(),
- mVoiceProxy.isVoiceButtonOnPrimary());
+ // When the device locale is changed in SetupWizard etc., this method may get called via
+ // onConfigurationChanged before SoftInputWindow is shown.
+ if (mKeyboardSwitcher.getKeyboardView() != null) {
+ // Reload keyboard because the current language has been changed.
+ mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
+ }
initSuggest();
loadSettings();
- mKeyboardSwitcher.updateShiftState();
+ // Since we just changed languages, we should re-evaluate suggestions with whatever word
+ // we are currently composing. If we are not composing anything, we may want to display
+ // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
+ if (mConnection.isCursorTouchingWord(mCurrentSettings)) {
+ mHandler.postUpdateSuggestions();
+ } else {
+ mHandler.postUpdateBigramPredictions();
+ }
}
- // "reset" and "next" are used only for USE_SPACEBAR_LANGUAGE_SWITCHER.
- private void toggleLanguage(boolean next) {
- if (mSubtypeSwitcher.useSpacebarLanguageSwitcher()) {
- mSubtypeSwitcher.toggleLanguage(next);
- }
- // The following is necessary because on API levels < 10, we don't get notified when
- // subtype changes.
- if (!CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED)
- onRefreshKeyboard();
- }
+ // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
+ // {@link KeyboardSwitcher}.
+ public void hapticAndAudioFeedback(final int primaryCode) {
+ mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
+ }
@Override
- public void onSwipeDown() {
- if (mSettingsValues.mSwipeDownDismissKeyboardEnabled)
- handleClose();
+ public void onPressKey(int primaryCode) {
+ mKeyboardSwitcher.onPressKey(primaryCode);
}
@Override
- public void onPress(int primaryCode, boolean withSliding) {
- if (mKeyboardSwitcher.isVibrateAndSoundFeedbackRequired()) {
- vibrate();
- playKeyClick(primaryCode);
- }
- KeyboardSwitcher switcher = mKeyboardSwitcher;
- final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
- if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
- switcher.onPressShift(withSliding);
- } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
- switcher.onPressSymbol();
- } else {
- switcher.onOtherKeyPressed();
+ public void onReleaseKey(int primaryCode, boolean withSliding) {
+ mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
+
+ // If accessibility is on, ensure the user receives keyboard state updates.
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ switch (primaryCode) {
+ case Keyboard.CODE_SHIFT:
+ AccessibleKeyboardViewProxy.getInstance().notifyShiftState();
+ break;
+ case Keyboard.CODE_SWITCH_ALPHA_SYMBOL:
+ AccessibleKeyboardViewProxy.getInstance().notifySymbolsState();
+ break;
+ }
}
- }
- @Override
- public void onRelease(int primaryCode, boolean withSliding) {
- KeyboardSwitcher switcher = mKeyboardSwitcher;
- // Reset any drag flags in the keyboard
- final boolean distinctMultiTouch = switcher.hasDistinctMultitouch();
- if (distinctMultiTouch && primaryCode == Keyboard.CODE_SHIFT) {
- switcher.onReleaseShift(withSliding);
- } else if (distinctMultiTouch && primaryCode == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
- switcher.onReleaseSymbol();
+ if (Keyboard.CODE_DELETE == primaryCode) {
+ // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
+ // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
+ // pair-friendly way of deleting characters in InputConnection.
+ final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
+ if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
+ mConnection.deleteSurroundingText(1, 0);
+ }
}
}
-
// receive ringer mode change and network state change.
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
- if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
- updateRingerMode();
- } else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
mSubtypeSwitcher.onNetworkStateChanged(intent);
+ } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+ mFeedbackManager.onRingerModeChanged();
}
}
};
- // update flags for silent mode
- private void updateRingerMode() {
- if (mAudioManager == null) {
- mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- }
- if (mAudioManager != null) {
- mSilentModeOn = (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL);
- }
- }
-
- private void playKeyClick(int primaryCode) {
- // if mAudioManager is null, we don't have the ringer state yet
- // mAudioManager will be set by updateRingerMode
- if (mAudioManager == null) {
- if (mKeyboardSwitcher.getKeyboardView() != null) {
- updateRingerMode();
- }
- }
- if (isSoundOn()) {
- // FIXME: Volume and enable should come from UI settings
- // FIXME: These should be triggered after auto-repeat logic
- int sound = AudioManager.FX_KEYPRESS_STANDARD;
- switch (primaryCode) {
- case Keyboard.CODE_DELETE:
- sound = AudioManager.FX_KEYPRESS_DELETE;
- break;
- case Keyboard.CODE_ENTER:
- sound = AudioManager.FX_KEYPRESS_RETURN;
- break;
- case Keyboard.CODE_SPACE:
- sound = AudioManager.FX_KEYPRESS_SPACEBAR;
- break;
- }
- mAudioManager.playSoundEffect(sound, FX_VOLUME);
- }
- }
-
- public void vibrate() {
- if (!mSettingsValues.mVibrateOn) {
- return;
- }
- LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) {
- inputView.performHapticFeedback(
- HapticFeedbackConstants.KEYBOARD_TAP,
- HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
- }
- }
-
- public WordComposer getCurrentWord() {
- return mWord;
- }
-
- boolean isSoundOn() {
- return mSettingsValues.mSoundOn && !mSilentModeOn;
- }
-
- private void updateCorrectionMode() {
- // TODO: cleanup messy flags
- mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false;
- final boolean shouldAutoCorrect = (mSettingsValues.mAutoCorrectEnabled
- || mSettingsValues.mQuickFixes) && !mInputTypeNoAutoCorrect && mHasDictionary;
- mCorrectionMode = (shouldAutoCorrect && mSettingsValues.mAutoCorrectEnabled)
- ? Suggest.CORRECTION_FULL
- : (shouldAutoCorrect ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE);
- mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect
- && mSettingsValues.mAutoCorrectEnabled)
- ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
- if (mSuggest != null) {
- mSuggest.setCorrectionMode(mCorrectionMode);
- }
- }
-
- private void updateAutoTextEnabled() {
- if (mSuggest == null) return;
- mSuggest.setQuickFixesEnabled(mSettingsValues.mQuickFixes
- && SubtypeSwitcher.getInstance().isSystemLanguageSameAsInputLanguage());
- }
-
- private void updateSuggestionVisibility(final SharedPreferences prefs, final Resources res) {
- final String suggestionVisiblityStr = prefs.getString(
- Settings.PREF_SHOW_SUGGESTIONS_SETTING,
- res.getString(R.string.prefs_suggestion_visibility_default_value));
- for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
- if (suggestionVisiblityStr.equals(res.getString(visibility))) {
- mSuggestionVisibility = visibility;
- break;
- }
- }
- }
-
- protected void launchSettings() {
- launchSettings(Settings.class);
+ private void launchSettings() {
+ launchSettingsClass(SettingsActivity.class);
}
public void launchDebugSettings() {
- launchSettings(DebugSettings.class);
+ launchSettingsClass(DebugSettingsActivity.class);
}
- protected void launchSettings(Class<? extends PreferenceActivity> settingsClass) {
+ private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
handleClose();
Intent intent = new Intent();
intent.setClass(LatinIME.this, settingsClass);
@@ -2057,6 +2157,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
getString(R.string.language_selection_title),
getString(R.string.english_ime_settings),
};
+ final Context context = this;
final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface di, int position) {
@@ -2064,7 +2165,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
switch (position) {
case 0:
Intent intent = CompatUtils.getInputLanguageSelectionIntent(
- mInputMethodId, Intent.FLAG_ACTIVITY_NEW_TASK
+ ImfUtils.getInputMethodIdOfThisIme(context),
+ Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
@@ -2075,51 +2177,28 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
}
}
};
- showOptionsMenuInternal(title, items, listener);
- }
-
- private void showOptionsMenu() {
- final CharSequence title = getString(R.string.english_ime_input_options);
- final CharSequence[] items = new CharSequence[] {
- getString(R.string.selectInputMethod),
- getString(R.string.english_ime_settings),
- };
- final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface di, int position) {
- di.dismiss();
- switch (position) {
- case 0:
- mImm.showInputMethodPicker();
- break;
- case 1:
- launchSettings();
- break;
- }
- }
- };
- showOptionsMenuInternal(title, items, listener);
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setItems(items, listener)
+ .setTitle(title);
+ showOptionDialog(builder.create());
}
- private void showOptionsMenuInternal(CharSequence title, CharSequence[] items,
- DialogInterface.OnClickListener listener) {
+ /* package */ void showOptionDialog(AlertDialog dialog) {
final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
if (windowToken == null) return;
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setCancelable(true);
- builder.setIcon(R.drawable.ic_dialog_keyboard);
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.setItems(items, listener);
- builder.setTitle(title);
- mOptionsDialog = builder.create();
- mOptionsDialog.setCanceledOnTouchOutside(true);
- Window window = mOptionsDialog.getWindow();
- WindowManager.LayoutParams lp = window.getAttributes();
+
+ dialog.setCancelable(true);
+ dialog.setCanceledOnTouchOutside(true);
+
+ final Window window = dialog.getWindow();
+ final WindowManager.LayoutParams lp = window.getAttributes();
lp.token = windowToken;
lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
window.setAttributes(lp);
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
- mOptionsDialog.show();
+
+ mOptionsDialog = dialog;
+ dialog.show();
}
@Override
@@ -2128,35 +2207,16 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar
final Printer p = new PrintWriterPrinter(fout);
p.println("LatinIME state :");
- p.println(" Keyboard mode = " + mKeyboardSwitcher.getKeyboardMode());
- p.println(" mComposing=" + mComposing.toString());
- p.println(" mIsSuggestionsRequested=" + mIsSettingsSuggestionStripOn);
- p.println(" mCorrectionMode=" + mCorrectionMode);
- p.println(" mHasUncommittedTypedChars=" + mHasUncommittedTypedChars);
- p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
- p.println(" mShouldInsertMagicSpace=" + mShouldInsertMagicSpace);
- p.println(" mApplicationSpecifiedCompletionOn=" + mApplicationSpecifiedCompletionOn);
- p.println(" TextEntryState.state=" + TextEntryState.getState());
- p.println(" mSoundOn=" + mSettingsValues.mSoundOn);
- p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn);
- p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
- }
-
- // Characters per second measurement
-
- private long mLastCpsTime;
- private static final int CPS_BUFFER_SIZE = 16;
- private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE];
- private int mCpsIndex;
-
- private void measureCps() {
- long now = System.currentTimeMillis();
- if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial
- mCpsIntervals[mCpsIndex] = now - mLastCpsTime;
- mLastCpsTime = now;
- mCpsIndex = (mCpsIndex + 1) % CPS_BUFFER_SIZE;
- long total = 0;
- for (int i = 0; i < CPS_BUFFER_SIZE; i++) total += mCpsIntervals[i];
- System.out.println("CPS = " + ((CPS_BUFFER_SIZE * 1000f) / total));
+ final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
+ final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
+ p.println(" Keyboard mode = " + keyboardMode);
+ p.println(" mIsSuggestionsSuggestionsRequested = "
+ + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
+ p.println(" mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
+ p.println(" isComposingWord=" + mWordComposer.isComposingWord());
+ p.println(" mSoundOn=" + mCurrentSettings.mSoundOn);
+ p.println(" mVibrateOn=" + mCurrentSettings.mVibrateOn);
+ p.println(" mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
+ p.println(" inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index e460471a5..dc0868e7c 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -16,23 +16,22 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.Dictionary.DataType;
-
-import android.content.Context;
import android.content.SharedPreferences;
+import android.view.inputmethod.EditorInfo;
-import java.util.List;
+import com.android.inputmethod.keyboard.Keyboard;
public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
public static boolean sDBG = false;
+ public static boolean sVISUALDEBUG = false;
+ public static boolean sUsabilityStudy = false;
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
}
- public static void init(Context context, SharedPreferences prefs) {
+ public static void init(LatinIME context, SharedPreferences prefs) {
}
public static void commit() {
@@ -42,8 +41,8 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
}
public static void logOnManualSuggestion(
- String before, String after, int position, List<CharSequence> suggestions) {
- }
+ String before, String after, int position, SuggestedWords suggestedWords) {
+ }
public static void logOnAutoCorrection(String before, String after, int separatorCode) {
}
@@ -51,7 +50,7 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void logOnAutoCorrectionCancelled() {
}
- public static void logOnDelete() {
+ public static void logOnDelete(int x, int y) {
}
public static void logOnInputChar() {
@@ -66,10 +65,13 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void logOnWarning(String warning) {
}
+ public static void onStartInputView(EditorInfo editorInfo) {
+ }
+
public static void onStartSuggestion(CharSequence previousWords) {
}
- public static void onAddSuggestedWord(String word, int typeId, DataType dataType) {
+ public static void onAddSuggestedWord(String word, int typeId, int dataType) {
}
public static void onSetKeyboard(Keyboard kb) {
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
new file mode 100644
index 000000000..b938dd336
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.Locale;
+
+/**
+ * A class to help with handling Locales in string form.
+ *
+ * This file has the same meaning and features (and shares all of its code) with
+ * the one in the dictionary pack. They need to be kept synchronized; for any
+ * update/bugfix to this file, consider also updating/fixing the version in the
+ * dictionary pack.
+ */
+public class LocaleUtils {
+ private LocaleUtils() {
+ // Intentional empty constructor for utility class.
+ }
+
+ // Locale match level constants.
+ // A higher level of match is guaranteed to have a higher numerical value.
+ // Some room is left within constants to add match cases that may arise necessary
+ // in the future, for example differentiating between the case where the countries
+ // are both present and different, and the case where one of the locales does not
+ // specify the countries. This difference is not needed now.
+
+ // Nothing matches.
+ public static final int LOCALE_NO_MATCH = 0;
+ // The languages matches, but the country are different. Or, the reference locale requires a
+ // country and the tested locale does not have one.
+ public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3;
+ // The languages and country match, but the variants are different. Or, the reference locale
+ // requires a variant and the tested locale does not have one.
+ public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6;
+ // The required locale is null or empty so it will accept anything, and the tested locale
+ // is non-null and non-empty.
+ public static final int LOCALE_ANY_MATCH = 10;
+ // The language matches, and the tested locale specifies a country but the reference locale
+ // does not require one.
+ public static final int LOCALE_LANGUAGE_MATCH = 15;
+ // The language and the country match, and the tested locale specifies a variant but the
+ // reference locale does not require one.
+ public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20;
+ // The compared locales are fully identical. This is the best match level.
+ public static final int LOCALE_FULL_MATCH = 30;
+
+ // The level at which a match is "normally" considered a locale match with standard algorithms.
+ // Don't use this directly, use #isMatch to test.
+ private static final int LOCALE_MATCH = LOCALE_ANY_MATCH;
+
+ // Make this match the maximum match level. If this evolves to have more than 2 digits
+ // when written in base 10, also adjust the getMatchLevelSortedString method.
+ private static final int MATCH_LEVEL_MAX = 30;
+
+ /**
+ * Return how well a tested locale matches a reference locale.
+ *
+ * This will check the tested locale against the reference locale and return a measure of how
+ * a well it matches the reference. The general idea is that the tested locale has to match
+ * every specified part of the required locale. A full match occur when they are equal, a
+ * partial match when the tested locale agrees with the reference locale but is more specific,
+ * and a difference when the tested locale does not comply with all requirements from the
+ * reference locale.
+ * In more detail, if the reference locale specifies at least a language and the testedLocale
+ * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
+ * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
+ * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
+ * tested locale agree on the language, but not on the country,
+ * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
+ * and LOCALE_LANGUAGE_MATCH otherwise.
+ * If they agree on both the language and the country, but not on the variant,
+ * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
+ * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
+ * LOCALE_FULL_MATCH is returned.
+ * Examples:
+ * en <=> en_US => LOCALE_LANGUAGE_MATCH
+ * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
+ * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
+ * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
+ * sp_US <=> en_US => LOCALE_NO_MATCH
+ * de <=> de => LOCALE_FULL_MATCH
+ * en_US <=> en_US => LOCALE_FULL_MATCH
+ * "" <=> en_US => LOCALE_ANY_MATCH
+ *
+ * @param referenceLocale the reference locale to test against.
+ * @param testedLocale the locale to test.
+ * @return a constant that measures how well the tested locale matches the reference locale.
+ */
+ public static int getMatchLevel(String referenceLocale, String testedLocale) {
+ if (TextUtils.isEmpty(referenceLocale)) {
+ return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
+ }
+ if (null == testedLocale) return LOCALE_NO_MATCH;
+ String[] referenceParams = referenceLocale.split("_", 3);
+ String[] testedParams = testedLocale.split("_", 3);
+ // By spec of String#split, [0] cannot be null and length cannot be 0.
+ if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
+ switch (referenceParams.length) {
+ case 1:
+ return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
+ case 2:
+ if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (!referenceParams[1].equals(testedParams[1]))
+ return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
+ return LOCALE_FULL_MATCH;
+ case 3:
+ if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (!referenceParams[1].equals(testedParams[1]))
+ return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
+ if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
+ if (!referenceParams[2].equals(testedParams[2]))
+ return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
+ return LOCALE_FULL_MATCH;
+ }
+ // It should be impossible to come here
+ return LOCALE_NO_MATCH;
+ }
+
+ /**
+ * Return a string that represents this match level, with better matches first.
+ *
+ * The strings are sorted in lexicographic order: a better match will always be less than
+ * a worse match when compared together.
+ */
+ public static String getMatchLevelSortedString(int matchLevel) {
+ // This works because the match levels are 0~99 (actually 0~30)
+ // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
+ return String.format("%02d", MATCH_LEVEL_MAX - matchLevel);
+ }
+
+ /**
+ * Find out whether a match level should be considered a match.
+ *
+ * This method takes a match level as returned by the #getMatchLevel method, and returns whether
+ * it should be considered a match in the usual sense with standard Locale functions.
+ *
+ * @param level the match level, as returned by getMatchLevel.
+ * @return whether this is a match or not.
+ */
+ public static boolean isMatch(int level) {
+ return LOCALE_MATCH <= level;
+ }
+
+ static final Object sLockForRunInLocale = new Object();
+
+ public abstract static class RunInLocale<T> {
+ protected abstract T job(Resources res);
+
+ /**
+ * Execute {@link #job(Resources)} method in specified system locale exclusively.
+ *
+ * @param res the resources to use. Pass current resources.
+ * @param newLocale the locale to change to
+ * @return the value returned from {@link #job(Resources)}.
+ */
+ public T runInLocale(final Resources res, final Locale newLocale) {
+ synchronized (sLockForRunInLocale) {
+ final Configuration conf = res.getConfiguration();
+ final Locale oldLocale = conf.locale;
+ try {
+ if (newLocale != null && !newLocale.equals(oldLocale)) {
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, null);
+ }
+ return job(res);
+ } finally {
+ if (newLocale != null && !newLocale.equals(oldLocale)) {
+ conf.locale = oldLocale;
+ res.updateConfiguration(conf, null);
+ }
+ }
+ }
+ }
+ }
+
+ private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
+
+ /**
+ * Creates a locale from a string specification.
+ */
+ public static Locale constructLocaleFromString(final String localeStr) {
+ if (localeStr == null)
+ return null;
+ synchronized (sLocaleCache) {
+ if (sLocaleCache.containsKey(localeStr))
+ return sLocaleCache.get(localeStr);
+ Locale retval = null;
+ String[] localeParams = localeStr.split("_", 3);
+ if (localeParams.length == 1) {
+ retval = new Locale(localeParams[0]);
+ } else if (localeParams.length == 2) {
+ retval = new Locale(localeParams[0], localeParams[1]);
+ } else if (localeParams.length == 3) {
+ retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
+ }
+ if (retval != null) {
+ sLocaleCache.put(localeStr, retval);
+ }
+ return retval;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java b/java/src/com/android/inputmethod/latin/NativeUtils.java
index 8518a4a78..9cc2bc02e 100644
--- a/java/src/com/android/inputmethod/compat/MotionEventCompatUtils.java
+++ b/java/src/com/android/inputmethod/latin/NativeUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * 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.
@@ -14,10 +14,19 @@
* limitations under the License.
*/
-package com.android.inputmethod.compat;
+package com.android.inputmethod.latin;
-public class MotionEventCompatUtils {
- public static final int ACTION_HOVER_MOVE = 0x7;
- public static final int ACTION_HOVER_ENTER = 0x9;
- public static final int ACTION_HOVER_EXIT = 0xA;
+public class NativeUtils {
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
+ private NativeUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ /**
+ * This method just calls up libm's powf() directly.
+ */
+ public static native float powf(float x, float y);
}
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
new file mode 100644
index 000000000..cf3cc7873
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -0,0 +1,1043 @@
+/*
+ * 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;
+
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.inputmethodservice.InputMethodService;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.RichInputConnection.Range;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
+ */
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = ResearchLogger.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info
+ /* package */ static boolean sIsLogging = false;
+ private static final int OUTPUT_FORMAT_VERSION = 1;
+ private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+ private static final String FILENAME_PREFIX = "researchLog";
+ private static final String FILENAME_SUFFIX = ".txt";
+ private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
+ new OutputStreamWriter(new NullOutputStream()));
+ private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
+ new SimpleDateFormat("yyyyMMddHHmmss", Locale.US);
+
+ // constants related to specific log points
+ private static final String WHITESPACE_SEPARATORS = " \t\n\r";
+ private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
+ private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
+
+ private static final ResearchLogger sInstance = new ResearchLogger();
+ private HandlerThread mHandlerThread;
+ /* package */ Handler mLoggingHandler;
+ // to write to a different filename, e.g., for testing, set mFile before calling start()
+ private File mFilesDir;
+ /* package */ File mFile;
+ private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null
+
+ private int mLoggingState;
+ private static final int LOGGING_STATE_OFF = 0;
+ private static final int LOGGING_STATE_ON = 1;
+ private static final int LOGGING_STATE_STOPPING = 2;
+ private boolean mIsPasswordView = false;
+
+ // digits entered by the user are replaced with this codepoint.
+ /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
+ Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area"
+ // U+E001 is in the "private-use area"
+ /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
+ // set when LatinIME should ignore an onUpdateSelection() callback that
+ // arises from operations in this class
+ private static boolean sLatinIMEExpectingUpdateSelection = false;
+
+ // used to check whether words are not unique
+ private Suggest mSuggest;
+ private Dictionary mDictionary;
+
+ private static class NullOutputStream extends OutputStream {
+ /** {@inheritDoc} */
+ @Override
+ public void write(byte[] buffer, int offset, int count) {
+ // nop
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write(byte[] buffer) {
+ // nop
+ }
+
+ @Override
+ public void write(int oneByte) {
+ }
+ }
+
+ private ResearchLogger() {
+ mLoggingState = LOGGING_STATE_OFF;
+ }
+
+ public static ResearchLogger getInstance() {
+ return sInstance;
+ }
+
+ public void init(final InputMethodService ims, final SharedPreferences prefs) {
+ assert ims != null;
+ if (ims == null) {
+ Log.w(TAG, "IMS is null; logging is off");
+ } else {
+ mFilesDir = ims.getFilesDir();
+ if (mFilesDir == null || !mFilesDir.exists()) {
+ Log.w(TAG, "IME storage directory does not exist.");
+ }
+ }
+ if (prefs != null) {
+ sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ }
+ }
+
+ public synchronized void start() {
+ Log.d(TAG, "start called");
+ if (!sIsLogging) {
+ // Log.w(TAG, "not in usability mode; not logging");
+ return;
+ }
+ if (mFilesDir == null || !mFilesDir.exists()) {
+ Log.w(TAG, "IME storage directory does not exist. Cannot start logging.");
+ } else {
+ if (mHandlerThread == null || !mHandlerThread.isAlive()) {
+ mHandlerThread = new HandlerThread("ResearchLogger logging task",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mHandlerThread.start();
+ mLoggingHandler = null;
+ mLoggingState = LOGGING_STATE_OFF;
+ }
+ if (mLoggingHandler == null) {
+ mLoggingHandler = new Handler(mHandlerThread.getLooper());
+ mLoggingState = LOGGING_STATE_OFF;
+ }
+ if (mFile == null) {
+ final String timestampString = TIMESTAMP_DATEFORMAT.format(new Date());
+ mFile = new File(mFilesDir, FILENAME_PREFIX + timestampString + FILENAME_SUFFIX);
+ }
+ if (mLoggingState == LOGGING_STATE_OFF) {
+ try {
+ mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
+ mJsonWriter.setLenient(true);
+ mJsonWriter.beginArray();
+ mLoggingState = LOGGING_STATE_ON;
+ } catch (IOException e) {
+ Log.w(TAG, "cannot start JsonWriter");
+ mJsonWriter = NULL_JSON_WRITER;
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public synchronized void stop() {
+ Log.d(TAG, "stop called");
+ if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
+ mLoggingState = LOGGING_STATE_STOPPING;
+ // put this in the Handler queue so pending writes are processed first.
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Log.d(TAG, "closing jsonwriter");
+ mJsonWriter.endArray();
+ mJsonWriter.flush();
+ mJsonWriter.close();
+ } catch (IllegalStateException e1) {
+ // assume that this is just the json not being terminated properly.
+ // ignore
+ e1.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ mJsonWriter = NULL_JSON_WRITER;
+ mFile = null;
+ mLoggingState = LOGGING_STATE_OFF;
+ if (DEBUG) {
+ Log.d(TAG, "logfile closed");
+ }
+ Log.d(TAG, "finished stop(), notifying");
+ synchronized (ResearchLogger.this) {
+ ResearchLogger.this.notify();
+ }
+ }
+ }
+ });
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public synchronized boolean abort() {
+ Log.d(TAG, "abort called");
+ boolean isLogFileDeleted = false;
+ if (mLoggingHandler != null && mLoggingState == LOGGING_STATE_ON) {
+ mLoggingState = LOGGING_STATE_STOPPING;
+ try {
+ Log.d(TAG, "closing jsonwriter");
+ mJsonWriter.endArray();
+ mJsonWriter.close();
+ } catch (IllegalStateException e1) {
+ // assume that this is just the json not being terminated properly.
+ // ignore
+ e1.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ mJsonWriter = NULL_JSON_WRITER;
+ // delete file
+ final boolean isDeleted = mFile.delete();
+ if (isDeleted) {
+ isLogFileDeleted = true;
+ }
+ mFile = null;
+ mLoggingState = LOGGING_STATE_OFF;
+ if (DEBUG) {
+ Log.d(TAG, "logfile closed");
+ }
+ }
+ }
+ return isLogFileDeleted;
+ }
+
+ /* package */ synchronized void flush() {
+ try {
+ mJsonWriter.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (key == null || prefs == null) {
+ return;
+ }
+ sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+ if (sIsLogging == false) {
+ abort();
+ }
+ }
+
+ /* package */ void presentResearchDialog(final LatinIME latinIME) {
+ final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
+ final CharSequence[] items = new CharSequence[] {
+ latinIME.getString(R.string.note_timestamp_for_researchlog),
+ latinIME.getString(R.string.do_not_log_this_session),
+ };
+ final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int position) {
+ di.dismiss();
+ switch (position) {
+ case 0:
+ ResearchLogger.getInstance().userTimestamp();
+ Toast.makeText(latinIME, R.string.notify_recorded_timestamp,
+ Toast.LENGTH_LONG).show();
+ break;
+ case 1:
+ Toast toast = Toast.makeText(latinIME,
+ R.string.notify_session_log_deleting, Toast.LENGTH_LONG);
+ toast.show();
+ final ResearchLogger logger = ResearchLogger.getInstance();
+ boolean isLogDeleted = logger.abort();
+ toast.cancel();
+ if (isLogDeleted) {
+ Toast.makeText(latinIME, R.string.notify_session_log_deleted,
+ Toast.LENGTH_LONG).show();
+ } else {
+ Toast.makeText(latinIME,
+ R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG)
+ .show();
+ }
+ break;
+ }
+ }
+ };
+ final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
+ .setItems(items, listener)
+ .setTitle(title);
+ latinIME.showOptionDialog(builder.create());
+ }
+
+ public void initSuggest(Suggest suggest) {
+ mSuggest = suggest;
+ }
+
+ private void setIsPasswordView(boolean isPasswordView) {
+ mIsPasswordView = isPasswordView;
+ }
+
+ private boolean isAllowedToLog() {
+ return mLoggingState == LOGGING_STATE_ON && !mIsPasswordView;
+ }
+
+ private static final String CURRENT_TIME_KEY = "_ct";
+ private static final String UPTIME_KEY = "_ut";
+ private static final String EVENT_TYPE_KEY = "_ty";
+ private static final Object[] EVENTKEYS_NULLVALUES = {};
+
+ private LogUnit mCurrentLogUnit = new LogUnit();
+
+ /**
+ * Buffer a research log event, flagging it as privacy-sensitive.
+ *
+ * This event contains potentially private information. If the word that this event is a part
+ * of is determined to be privacy-sensitive, then this event should not be included in the
+ * output log. The system waits to output until the containing word is known.
+ *
+ * @param keys an array containing a descriptive name for the event, followed by the keys
+ * @param values an array of values, either a String or Number. length should be one
+ * less than the keys array
+ */
+ private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
+ final Object[] values) {
+ assert values.length + 1 == keys.length;
+ mCurrentLogUnit.addLogAtom(keys, values, true);
+ }
+
+ /**
+ * Buffer a research log event, flaggint it as not privacy-sensitive.
+ *
+ * This event contains no potentially private information. Even if the word that this event
+ * is privacy-sensitive, this event can still safely be sent to the output log. The system
+ * waits until the containing word is known so that this event can be written in the proper
+ * temporal order with other events that may be privacy sensitive.
+ *
+ * @param keys an array containing a descriptive name for the event, followed by the keys
+ * @param values an array of values, either a String or Number. length should be one
+ * less than the keys array
+ */
+ private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
+ assert values.length + 1 == keys.length;
+ mCurrentLogUnit.addLogAtom(keys, values, false);
+ }
+
+ /* package for test */ boolean isPrivacyThreat(String word) {
+ // currently: word not in dictionary or contains numbers.
+ if (TextUtils.isEmpty(word)) {
+ return false;
+ }
+ final int length = word.length();
+ boolean hasLetter = false;
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = Character.codePointAt(word, i);
+ if (Character.isDigit(codePoint)) {
+ return true;
+ }
+ if (Character.isLetter(codePoint)) {
+ hasLetter = true;
+ break; // Word may contain digits, but will only be allowed if in the dictionary.
+ }
+ }
+ if (hasLetter) {
+ if (mDictionary == null && mSuggest != null && mSuggest.hasMainDictionary()) {
+ mDictionary = mSuggest.getMainDictionary();
+ }
+ if (mDictionary == null) {
+ // Can't access dictionary. Assume privacy threat.
+ return true;
+ }
+ return !(mDictionary.isValidWord(word));
+ }
+ // No letters, no numbers. Punctuation, space, or something else.
+ return false;
+ }
+
+ /**
+ * Write out enqueued LogEvents to the log, possibly dropping privacy sensitive events.
+ */
+ /* package for test */ synchronized void flushQueue(boolean removePotentiallyPrivateEvents) {
+ if (isAllowedToLog()) {
+ mCurrentLogUnit.setRemovePotentiallyPrivateEvents(removePotentiallyPrivateEvents);
+ mLoggingHandler.post(mCurrentLogUnit);
+ mCurrentLogUnit = new LogUnit();
+ }
+ }
+
+ private synchronized void outputEvent(final String[] keys, final Object[] values) {
+ try {
+ mJsonWriter.beginObject();
+ mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+ mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
+ mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
+ final int length = values.length;
+ for (int i = 0; i < length; i++) {
+ mJsonWriter.name(keys[i + 1]);
+ Object value = values[i];
+ if (value instanceof String) {
+ mJsonWriter.value((String) value);
+ } else if (value instanceof Number) {
+ mJsonWriter.value((Number) value);
+ } else if (value instanceof Boolean) {
+ mJsonWriter.value((Boolean) value);
+ } else if (value instanceof CompletionInfo[]) {
+ CompletionInfo[] ci = (CompletionInfo[]) value;
+ mJsonWriter.beginArray();
+ for (int j = 0; j < ci.length; j++) {
+ mJsonWriter.value(ci[j].toString());
+ }
+ mJsonWriter.endArray();
+ } else if (value instanceof SharedPreferences) {
+ SharedPreferences prefs = (SharedPreferences) value;
+ mJsonWriter.beginObject();
+ for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+ mJsonWriter.name(entry.getKey());
+ final Object innerValue = entry.getValue();
+ if (innerValue == null) {
+ mJsonWriter.nullValue();
+ } else if (innerValue instanceof Boolean) {
+ mJsonWriter.value((Boolean) innerValue);
+ } else if (innerValue instanceof Number) {
+ mJsonWriter.value((Number) innerValue);
+ } else {
+ mJsonWriter.value(innerValue.toString());
+ }
+ }
+ mJsonWriter.endObject();
+ } else if (value instanceof Key[]) {
+ Key[] keyboardKeys = (Key[]) value;
+ mJsonWriter.beginArray();
+ for (Key keyboardKey : keyboardKeys) {
+ mJsonWriter.beginObject();
+ mJsonWriter.name("code").value(keyboardKey.mCode);
+ mJsonWriter.name("altCode").value(keyboardKey.mAltCode);
+ mJsonWriter.name("x").value(keyboardKey.mX);
+ mJsonWriter.name("y").value(keyboardKey.mY);
+ mJsonWriter.name("w").value(keyboardKey.mWidth);
+ mJsonWriter.name("h").value(keyboardKey.mHeight);
+ mJsonWriter.endObject();
+ }
+ mJsonWriter.endArray();
+ } else if (value instanceof SuggestedWords) {
+ SuggestedWords words = (SuggestedWords) value;
+ mJsonWriter.beginObject();
+ mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+ mJsonWriter.name("hasAutoCorrectionCandidate")
+ .value(words.mHasAutoCorrectionCandidate);
+ mJsonWriter.name("isPunctuationSuggestions")
+ .value(words.mIsPunctuationSuggestions);
+ mJsonWriter.name("allowsToBeAutoCorrected")
+ .value(words.mAllowsToBeAutoCorrected);
+ mJsonWriter.name("isObsoleteSuggestions")
+ .value(words.mIsObsoleteSuggestions);
+ mJsonWriter.name("isPrediction")
+ .value(words.mIsPrediction);
+ mJsonWriter.name("words");
+ mJsonWriter.beginArray();
+ final int size = words.size();
+ for (int j = 0; j < size; j++) {
+ SuggestedWordInfo wordInfo = words.getWordInfo(j);
+ mJsonWriter.value(wordInfo.toString());
+ }
+ mJsonWriter.endArray();
+ mJsonWriter.endObject();
+ } else if (value == null) {
+ mJsonWriter.nullValue();
+ } else {
+ Log.w(TAG, "Unrecognized type to be logged: " +
+ (value == null ? "<null>" : value.getClass().getName()));
+ mJsonWriter.nullValue();
+ }
+ }
+ mJsonWriter.endObject();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.w(TAG, "Error in JsonWriter; disabling logging");
+ try {
+ mJsonWriter.close();
+ } catch (IllegalStateException e1) {
+ // assume that this is just the json not being terminated properly.
+ // ignore
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } finally {
+ mJsonWriter = NULL_JSON_WRITER;
+ }
+ }
+ }
+
+ private static class LogUnit implements Runnable {
+ private final List<String[]> mKeysList = new ArrayList<String[]>();
+ private final List<Object[]> mValuesList = new ArrayList<Object[]>();
+ private final List<Boolean> mIsPotentiallyPrivate = new ArrayList<Boolean>();
+ private boolean mRemovePotentiallyPrivateEvents = true;
+
+ private void addLogAtom(final String[] keys, final Object[] values,
+ final Boolean isPotentiallyPrivate) {
+ mKeysList.add(keys);
+ mValuesList.add(values);
+ mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+ }
+
+ void setRemovePotentiallyPrivateEvents(boolean removePotentiallyPrivateEvents) {
+ mRemovePotentiallyPrivateEvents = removePotentiallyPrivateEvents;
+ }
+
+ @Override
+ public void run() {
+ final int numAtoms = mKeysList.size();
+ for (int atomIndex = 0; atomIndex < numAtoms; atomIndex++) {
+ if (mRemovePotentiallyPrivateEvents && mIsPotentiallyPrivate.get(atomIndex)) {
+ continue;
+ }
+ final String[] keys = mKeysList.get(atomIndex);
+ final Object[] values = mValuesList.get(atomIndex);
+ ResearchLogger.getInstance().outputEvent(keys, values);
+ }
+ }
+ }
+
+ private static int scrubDigitFromCodePoint(int codePoint) {
+ return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
+ }
+
+ /* package for test */ static String scrubDigitsFromString(String s) {
+ StringBuilder sb = null;
+ final int length = s.length();
+ for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
+ final int codePoint = Character.codePointAt(s, i);
+ if (Character.isDigit(codePoint)) {
+ if (sb == null) {
+ sb = new StringBuilder(length);
+ sb.append(s.substring(0, i));
+ }
+ sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
+ } else {
+ if (sb != null) {
+ sb.appendCodePoint(codePoint);
+ }
+ }
+ }
+ if (sb == null) {
+ return s;
+ } else {
+ return sb.toString();
+ }
+ }
+
+ private String scrubWord(String word) {
+ if (mDictionary == null) {
+ return WORD_REPLACEMENT_STRING;
+ }
+ if (mDictionary.isValidWord(word)) {
+ return word;
+ }
+ return WORD_REPLACEMENT_STRING;
+ }
+
+ private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
+ "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
+ "pressure"
+ };
+ public static void latinKeyboardView_processMotionEvent(final MotionEvent me, final int action,
+ final long eventTime, final int index, final int id, final int x, final int y) {
+ if (me != null) {
+ final String actionString;
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
+ case MotionEvent.ACTION_UP: actionString = "UP"; break;
+ case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
+ case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
+ case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
+ case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
+ case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
+ default: actionString = "ACTION_" + action; break;
+ }
+ final float size = me.getSize(index);
+ final float pressure = me.getPressure(index);
+ final Object[] values = {
+ actionString, eventTime, id, x, y, size, pressure
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = {
+ "LatinIMEOnCodeInput", "code", "x", "y"
+ };
+ public static void latinIME_onCodeInput(final int code, final int x, final int y) {
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+ }
+
+ private static final String[] EVENTKEYS_CORRECTION = {
+ "LogCorrection", "subgroup", "before", "after", "position"
+ };
+ public static void logCorrection(final String subgroup, final String before, final String after,
+ final int position) {
+ final Object[] values = {
+ subgroup, scrubDigitsFromString(before), scrubDigitsFromString(after), position
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_CORRECTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION = {
+ "LatinIMECommitCurrentAutoCorrection", "typedWord", "autoCorrection"
+ };
+ public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
+ final String autoCorrection) {
+ final Object[] values = {
+ scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values);
+ researchLogger.flushQueue(researchLogger.isPrivacyThreat(autoCorrection));
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = {
+ "LatinIMECommitText", "typedWord"
+ };
+ public static void latinIME_commitText(final CharSequence typedWord) {
+ final String scrubbedWord = scrubDigitsFromString(typedWord.toString());
+ final Object[] values = {
+ scrubbedWord
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values);
+ researchLogger.flushQueue(researchLogger.isPrivacyThreat(scrubbedWord));
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = {
+ "LatinIMEDeleteSurroundingText", "length"
+ };
+ public static void latinIME_deleteSurroundingText(final int length) {
+ final Object[] values = {
+ length
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD = {
+ "LatinIMEDoubleSpaceAutoPeriod"
+ };
+ public static void latinIME_doubleSpaceAutoPeriod() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_DOUBLESPACEAUTOPERIOD, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
+ "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions"
+ };
+ public static void latinIME_onDisplayCompletions(
+ final CompletionInfo[] applicationSpecifiedCompletions) {
+ final Object[] values = {
+ applicationSpecifiedCompletions
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
+ values);
+ }
+
+ /* package */ static boolean getAndClearLatinIMEExpectingUpdateSelection() {
+ boolean returnValue = sLatinIMEExpectingUpdateSelection;
+ sLatinIMEExpectingUpdateSelection = false;
+ return returnValue;
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = {
+ "LatinIMEOnWindowHidden", "isTextTruncated", "text"
+ };
+ public static void latinIME_onWindowHidden(final int savedSelectionStart,
+ final int savedSelectionEnd, final InputConnection ic) {
+ if (ic != null) {
+ ic.beginBatchEdit();
+ ic.performContextMenuAction(android.R.id.selectAll);
+ CharSequence charSequence = ic.getSelectedText(0);
+ ic.setSelection(savedSelectionStart, savedSelectionEnd);
+ ic.endBatchEdit();
+ sLatinIMEExpectingUpdateSelection = true;
+ final Object[] values = new Object[2];
+ if (OUTPUT_ENTIRE_BUFFER) {
+ if (TextUtils.isEmpty(charSequence)) {
+ values[0] = false;
+ values[1] = "";
+ } else {
+ if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+ int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+ // do not cut in the middle of a supplementary character
+ final char c = charSequence.charAt(length - 1);
+ if (Character.isHighSurrogate(c)) {
+ length--;
+ }
+ final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+ length);
+ values[0] = true;
+ values[1] = truncatedCharSequence.toString();
+ } else {
+ values[0] = false;
+ values[1] = charSequence.toString();
+ }
+ }
+ } else {
+ values[0] = true;
+ values[1] = "";
+ }
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+ researchLogger.flushQueue(true); // Play it safe. Remove privacy-sensitive events.
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
+ "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
+ "fieldId", "display", "model", "prefs", "outputFormatVersion"
+ };
+ public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
+ final SharedPreferences prefs) {
+ if (editorInfo != null) {
+ final Object[] values = {
+ getUUID(prefs), editorInfo.packageName, Integer.toHexString(editorInfo.inputType),
+ Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY,
+ Build.MODEL, prefs, OUTPUT_FORMAT_VERSION
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
+ }
+ }
+
+ private static String getUUID(final SharedPreferences prefs) {
+ String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
+ if (null == uuidString) {
+ UUID uuid = UUID.randomUUID();
+ uuidString = uuid.toString();
+ Editor editor = prefs.edit();
+ editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
+ editor.apply();
+ }
+ return uuidString;
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = {
+ "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart",
+ "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd",
+ "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context"
+ };
+ public static void latinIME_onUpdateSelection(final int lastSelectionStart,
+ final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
+ final int newSelStart, final int newSelEnd, final int composingSpanStart,
+ final int composingSpanEnd, final boolean expectingUpdateSelection,
+ final boolean expectingUpdateSelectionFromLogger,
+ final RichInputConnection connection) {
+ String word = "";
+ if (connection != null) {
+ Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+ if (range != null) {
+ word = range.mWord;
+ }
+ }
+ final ResearchLogger researchLogger = getInstance();
+ final String scrubbedWord = researchLogger.scrubWord(word);
+ final Object[] values = {
+ lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
+ newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, scrubbedWord
+ };
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PERFORMEDITORACTION = {
+ "LatinIMEPerformEditorAction", "imeActionNext"
+ };
+ public static void latinIME_performEditorAction(final int imeActionNext) {
+ final Object[] values = {
+ imeActionNext
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_PERFORMEDITORACTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION = {
+ "LatinIMEPickApplicationSpecifiedCompletion", "index", "text", "x", "y"
+ };
+ public static void latinIME_pickApplicationSpecifiedCompletion(final int index,
+ final CharSequence cs, int x, int y) {
+ final Object[] values = {
+ index, cs, x, y
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION, values);
+ researchLogger.flushQueue(researchLogger.isPrivacyThreat(cs.toString()));
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
+ "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y"
+ };
+ public static void latinIME_pickSuggestionManually(final String replacedWord,
+ final int index, CharSequence suggestion, int x, int y) {
+ final Object[] values = {
+ scrubDigitsFromString(replacedWord), index, suggestion == null ? null :
+ scrubDigitsFromString(suggestion.toString()), x, y
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
+ values);
+ researchLogger.flushQueue(researchLogger.isPrivacyThreat(suggestion.toString()));
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
+ "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
+ };
+ public static void latinIME_punctuationSuggestion(final int index,
+ final CharSequence suggestion, int x, int y) {
+ final Object[] values = {
+ index, suggestion, x, y
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT = {
+ "LatinIMERevertDoubleSpaceWhileInBatchEdit"
+ };
+ public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT,
+ EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION = {
+ "LatinIMERevertSwapPunctuation"
+ };
+ public static void latinIME_revertSwapPunctuation() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_REVERTSWAPPUNCTUATION, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
+ "LatinIMESendKeyCodePoint", "code"
+ };
+ public static void latinIME_sendKeyCodePoint(final int code) {
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code))
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT = {
+ "LatinIMESwapSwapperAndSpaceWhileInBatchEdit"
+ };
+ public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT,
+ EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW = {
+ "LatinIMESwitchToKeyboardView"
+ };
+ public static void latinIME_switchToKeyboardView() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWITCHTOKEYBOARDVIEW, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS = {
+ "LatinKeyboardViewOnLongPress"
+ };
+ public static void latinKeyboardView_onLongPress() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD = {
+ "LatinKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width",
+ "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey",
+ "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled",
+ "isMultiLine", "tw", "th", "keys"
+ };
+ public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
+ if (keyboard != null) {
+ final KeyboardId kid = keyboard.mId;
+ final boolean isPasswordView = kid.passwordInput();
+ final Object[] values = {
+ KeyboardId.elementIdToName(kid.mElementId),
+ kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+ kid.mOrientation,
+ kid.mWidth,
+ KeyboardId.modeName(kid.mMode),
+ kid.imeAction(),
+ kid.navigateNext(),
+ kid.navigatePrevious(),
+ kid.mClobberSettingsKey,
+ isPasswordView,
+ kid.mShortcutKeyEnabled,
+ kid.mHasShortcutKey,
+ kid.mLanguageSwitchKeyEnabled,
+ kid.isMultiLine(),
+ keyboard.mOccupiedWidth,
+ keyboard.mOccupiedHeight,
+ keyboard.mKeys
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINKEYBOARDVIEW_SETKEYBOARD, values);
+ getInstance().setIsPasswordView(isPasswordView);
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = {
+ "LatinIMERevertCommit", "originallyTypedWord"
+ };
+ public static void latinIME_revertCommit(final String originallyTypedWord) {
+ final Object[] values = {
+ originallyTypedWord
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
+ "PointerTrackerCallListenerOnCancelInput"
+ };
+ public static void pointerTracker_callListenerOnCancelInput() {
+ getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
+ EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = {
+ "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y",
+ "ignoreModifierKey", "altersCode", "isEnabled"
+ };
+ public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
+ final int y, final boolean ignoreModifierKey, final boolean altersCode,
+ final int code) {
+ if (key != null) {
+ CharSequence outputText = key.mOutputText;
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
+ : scrubDigitsFromString(outputText.toString()),
+ x, y, ignoreModifierKey, altersCode, key.isEnabled()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = {
+ "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey",
+ "isEnabled"
+ };
+ public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
+ final boolean withSliding, final boolean ignoreModifierKey) {
+ if (key != null) {
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+ ignoreModifierKey, key.isEnabled()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = {
+ "PointerTrackerOnDownEvent", "deltaT", "distanceSquared"
+ };
+ public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
+ final Object[] values = {
+ deltaT, distanceSquared
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
+ "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY"
+ };
+ public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
+ final int lastY) {
+ final Object[] values = {
+ x, y, lastX, lastY
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
+ }
+
+ private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
+ "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
+ };
+ public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
+ if (me != null) {
+ final Object[] values = {
+ me.toString()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS = {
+ "SuggestionsViewSetSuggestions", "suggestedWords"
+ };
+ public static void suggestionsView_setSuggestions(final SuggestedWords suggestedWords) {
+ if (suggestedWords != null) {
+ final Object[] values = {
+ suggestedWords
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_SUGGESTIONSVIEW_SETSUGGESTIONS,
+ values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_USER_TIMESTAMP = {
+ "UserTimestamp"
+ };
+ public void userTimestamp() {
+ getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
new file mode 100644
index 000000000..0c19bed05
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -0,0 +1,429 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.util.regex.Pattern;
+
+/**
+ * Wrapper for InputConnection to simplify interaction
+ */
+public class RichInputConnection {
+ private static final String TAG = RichInputConnection.class.getSimpleName();
+ private static final boolean DBG = false;
+ // Provision for a long word pair and a separator
+ private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
+ private static final Pattern spaceRegex = Pattern.compile("\\s+");
+ private static final int INVALID_CURSOR_POSITION = -1;
+
+ InputConnection mIC;
+ int mNestLevel;
+ public RichInputConnection() {
+ mIC = null;
+ mNestLevel = 0;
+ }
+
+ public void beginBatchEdit(final InputConnection newInputConnection) {
+ if (++mNestLevel == 1) {
+ mIC = newInputConnection;
+ if (null != mIC) mIC.beginBatchEdit();
+ } else {
+ if (DBG) {
+ throw new RuntimeException("Nest level too deep");
+ } else {
+ Log.e(TAG, "Nest level too deep : " + mNestLevel);
+ }
+ }
+ }
+ public void endBatchEdit() {
+ if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
+ if (--mNestLevel == 0 && null != mIC) mIC.endBatchEdit();
+ }
+
+ private void checkBatchEdit() {
+ if (mNestLevel != 1) {
+ // TODO: exception instead
+ Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
+ Log.e(TAG, Utils.getStackTrace(4));
+ }
+ }
+
+ public void finishComposingText() {
+ checkBatchEdit();
+ if (null != mIC) mIC.finishComposingText();
+ }
+
+ public void commitText(final CharSequence text, final int i) {
+ checkBatchEdit();
+ if (null != mIC) mIC.commitText(text, i);
+ }
+
+ public int getCursorCapsMode(final int inputType) {
+ if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+ return mIC.getCursorCapsMode(inputType);
+ }
+
+ public CharSequence getTextBeforeCursor(final int i, final int j) {
+ if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+ return null;
+ }
+
+ public CharSequence getTextAfterCursor(final int i, final int j) {
+ if (null != mIC) return mIC.getTextAfterCursor(i, j);
+ return null;
+ }
+
+ public void deleteSurroundingText(final int i, final int j) {
+ checkBatchEdit();
+ if (null != mIC) mIC.deleteSurroundingText(i, j);
+ }
+
+ public void performEditorAction(final int actionId) {
+ if (null != mIC) mIC.performEditorAction(actionId);
+ }
+
+ public void sendKeyEvent(final KeyEvent keyEvent) {
+ checkBatchEdit();
+ if (null != mIC) mIC.sendKeyEvent(keyEvent);
+ }
+
+ public void setComposingText(final CharSequence text, final int i) {
+ checkBatchEdit();
+ if (null != mIC) mIC.setComposingText(text, i);
+ }
+
+ public void setSelection(final int from, final int to) {
+ checkBatchEdit();
+ if (null != mIC) mIC.setSelection(from, to);
+ }
+
+ public void commitCorrection(final CorrectionInfo correctionInfo) {
+ checkBatchEdit();
+ if (null != mIC) mIC.commitCorrection(correctionInfo);
+ }
+
+ public void commitCompletion(final CompletionInfo completionInfo) {
+ checkBatchEdit();
+ if (null != mIC) mIC.commitCompletion(completionInfo);
+ }
+
+ public CharSequence getPreviousWord(final String sentenceSeperators) {
+ //TODO: Should fix this. This could be slow!
+ if (null == mIC) return null;
+ CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ return getPreviousWord(prev, sentenceSeperators);
+ }
+
+ /**
+ * Represents a range of text, relative to the current cursor position.
+ */
+ public static class Range {
+ /** Characters before selection start */
+ public final int mCharsBefore;
+
+ /**
+ * Characters after selection start, including one trailing word
+ * separator.
+ */
+ public final int mCharsAfter;
+
+ /** The actual characters that make up a word */
+ public final String mWord;
+
+ public Range(int charsBefore, int charsAfter, String word) {
+ if (charsBefore < 0 || charsAfter < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ this.mCharsBefore = charsBefore;
+ this.mCharsAfter = charsAfter;
+ this.mWord = word;
+ }
+ }
+
+ private static boolean isSeparator(int code, String sep) {
+ return sep.indexOf(code) != -1;
+ }
+
+ // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
+ // Also, it won't return words that end in a separator.
+ // Example :
+ // "abc def|" -> abc
+ // "abc def |" -> abc
+ // "abc def. |" -> abc
+ // "abc def . |" -> def
+ // "abc|" -> null
+ // "abc |" -> null
+ // "abc. def|" -> null
+ public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
+ if (prev == null) return null;
+ String[] w = spaceRegex.split(prev);
+
+ // If we can't find two words, or we found an empty word, return null.
+ if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - 2];
+ }
+
+ public CharSequence getThisWord(String sentenceSeperators) {
+ if (null == mIC) return null;
+ final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ return getThisWord(prev, sentenceSeperators);
+ }
+
+ // Get the word immediately before the cursor, even if there is whitespace between it and
+ // the cursor - but not if there is punctuation.
+ // Example :
+ // "abc def|" -> def
+ // "abc def |" -> def
+ // "abc def. |" -> null
+ // "abc def . |" -> null
+ public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
+ if (prev == null) return null;
+ String[] w = spaceRegex.split(prev);
+
+ // No word : return null
+ if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - 1];
+ }
+
+ /**
+ * @param separators characters which may separate words
+ * @return the word that surrounds the cursor, including up to one trailing
+ * separator. For example, if the field contains "he|llo world", where |
+ * represents the cursor, then "hello " will be returned.
+ */
+ public String getWordAtCursor(String separators) {
+ // getWordRangeAtCursor returns null if the connection is null
+ Range r = getWordRangeAtCursor(separators, 0);
+ return (r == null) ? null : r.mWord;
+ }
+
+ private int getCursorPosition() {
+ if (null == mIC) return INVALID_CURSOR_POSITION;
+ final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
+ if (extracted == null) {
+ return INVALID_CURSOR_POSITION;
+ }
+ return extracted.startOffset + extracted.selectionStart;
+ }
+
+ /**
+ * Returns the text surrounding the cursor.
+ *
+ * @param sep a string of characters that split words.
+ * @param additionalPrecedingWordsCount the number of words before the current word that should
+ * be included in the returned range
+ * @return a range containing the text surrounding the cursor
+ */
+ public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
+ if (mIC == null || sep == null) {
+ return null;
+ }
+ CharSequence before = mIC.getTextBeforeCursor(1000, 0);
+ CharSequence after = mIC.getTextAfterCursor(1000, 0);
+ if (before == null || after == null) {
+ return null;
+ }
+
+ // Going backward, alternate skipping non-separators and separators until enough words
+ // have been read.
+ int start = before.length();
+ boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at
+ while (true) { // see comments below for why this is guaranteed to halt
+ while (start > 0) {
+ final int codePoint = Character.codePointBefore(before, start);
+ if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+ break; // inner loop
+ }
+ --start;
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ --start;
+ }
+ }
+ // isStoppingAtWhitespace is true every other time through the loop,
+ // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+ // guarantees outer loop termination
+ if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+ break; // outer loop
+ }
+ isStoppingAtWhitespace = !isStoppingAtWhitespace;
+ }
+
+ // Find last word separator after the cursor
+ int end = -1;
+ while (++end < after.length()) {
+ final int codePoint = Character.codePointAt(after, end);
+ if (isSeparator(codePoint, sep)) {
+ break;
+ }
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ ++end;
+ }
+ }
+
+ int cursor = getCursorPosition();
+ if (start >= 0 && cursor + end <= after.length() + before.length()) {
+ String word = before.toString().substring(start, before.length())
+ + after.toString().substring(0, end);
+ return new Range(before.length() - start, end, word);
+ }
+
+ return null;
+ }
+
+ public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+ CharSequence before = getTextBeforeCursor(1, 0);
+ CharSequence after = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+
+ public void removeTrailingSpace() {
+ checkBatchEdit();
+ final CharSequence lastOne = getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
+ deleteSurroundingText(1, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(1);
+ }
+ }
+ }
+
+ public boolean sameAsTextBeforeCursor(final CharSequence text) {
+ final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
+ return TextUtils.equals(text, beforeText);
+ }
+
+ /* (non-javadoc)
+ * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
+ */
+ public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
+ // Bail out if the cursor is not at the end of a word (cursor must be preceded by
+ // non-whitespace, non-separator, non-start-of-text)
+ // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(1, 0);
+ if (TextUtils.isEmpty(textBeforeCursor)
+ || settings.isWordSeparator(textBeforeCursor.charAt(0))) return null;
+
+ // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
+ // separator or end of line/text)
+ // Example: "test|"<EOL> "te|st" get rejected here
+ final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(textAfterCursor)
+ && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
+
+ // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
+ // Example: " -|" gets rejected here but "e-|" and "e|" are okay
+ CharSequence word = getWordAtCursor(settings.mWordSeparators);
+ // We don't suggest on leading single quotes, so we have to remove them from the word if
+ // it starts with single quotes.
+ while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
+ word = word.subSequence(1, word.length());
+ }
+ if (TextUtils.isEmpty(word)) return null;
+ final char firstChar = word.charAt(0); // we just tested that word is not empty
+ if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
+
+ // We only suggest on words that start with a letter or a symbol that is excluded from
+ // word separators (see #handleCharacterWhileInBatchEdit).
+ if (!(Character.isLetter(firstChar)
+ || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
+ return null;
+ }
+
+ return word;
+ }
+
+ public boolean revertDoubleSpace() {
+ checkBatchEdit();
+ // Here we test whether we indeed have a period and a space before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ if (!". ".equals(textBeforeCursor)) {
+ // Theoretically we should not be coming here if there isn't ". " before the
+ // cursor, but the application may be changing the text while we are typing, so
+ // anything goes. We should not crash.
+ Log.d(TAG, "Tried to revert double-space combo but we didn't find "
+ + "\". \" just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(2);
+ }
+ commitText(" ", 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
+ }
+ return true;
+ }
+
+ public boolean revertSwapPunctuation() {
+ checkBatchEdit();
+ // Here we test whether we indeed have a space and something else before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
+ // enter surrogate pairs this code will have been removed.
+ if (TextUtils.isEmpty(textBeforeCursor)
+ || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
+ // We may only come here if the application is changing the text while we are typing.
+ // This is quite a broken case, but not logically impossible, so we shouldn't crash,
+ // but some debugging log may be in order.
+ Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
+ + "find a space just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_deleteSurroundingText(2);
+ }
+ commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_revertSwapPunctuation();
+ }
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 6c515c845..4c67b4957 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -16,354 +16,126 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.compat.CompatUtils;
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
-import com.android.inputmethod.deprecated.VoiceProxy;
-import com.android.inputmethod.compat.VibratorCompatWrapper;
-
import android.app.AlertDialog;
-import android.app.Dialog;
import android.app.backup.BackupManager;
-import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
+import android.media.AudioManager;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
-import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
-import android.speech.SpeechRecognizer;
-import android.text.AutoText;
-import android.text.TextUtils;
-import android.text.method.LinkMovementMethod;
-import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;
-import java.util.Arrays;
-import java.util.Locale;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethodcommon.InputMethodSettingsFragment;
-public class Settings extends PreferenceActivity
- implements SharedPreferences.OnSharedPreferenceChangeListener,
- DialogInterface.OnDismissListener, OnPreferenceClickListener {
- private static final String TAG = "Settings";
+public class Settings extends InputMethodSettingsFragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+ public static final boolean ENABLE_EXPERIMENTAL_SETTINGS = false;
- public static final String PREF_GENERAL_SETTINGS_KEY = "general_settings";
+ // In the same order as xml/prefs.xml
+ public static final String PREF_GENERAL_SETTINGS = "general_settings";
+ public static final String PREF_AUTO_CAP = "auto_cap";
public static final String PREF_VIBRATE_ON = "vibrate_on";
public static final String PREF_SOUND_ON = "sound_on";
- public static final String PREF_KEY_PREVIEW_POPUP_ON = "popup_on";
- public static final String PREF_RECORRECTION_ENABLED = "recorrection_enabled";
- public static final String PREF_AUTO_CAP = "auto_cap";
- public static final String PREF_SETTINGS_KEY = "settings_key";
- public static final String PREF_VOICE_SETTINGS_KEY = "voice_mode";
- public static final String PREF_INPUT_LANGUAGE = "input_language";
- public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
- public static final String PREF_SUBTYPES = "subtype_settings";
-
+ public static final String PREF_POPUP_ON = "popup_on";
+ public static final String PREF_VOICE_MODE = "voice_mode";
+ public static final String PREF_CORRECTION_SETTINGS = "correction_settings";
public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
- public static final String PREF_CORRECTION_SETTINGS_KEY = "correction_settings";
- public static final String PREF_QUICK_FIXES = "quick_fixes";
- public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
- public static final String PREF_DEBUG_SETTINGS = "debug_settings";
-
- public static final String PREF_NGRAM_SETTINGS_KEY = "ngram_settings";
- public static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion";
- public static final String PREF_BIGRAM_PREDICTIONS = "bigram_prediction";
-
- public static final String PREF_MISC_SETTINGS_KEY = "misc_settings";
-
+ public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
+ public static final String PREF_MISC_SETTINGS = "misc_settings";
+ public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+ public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME =
+ "last_user_dictionary_write_time";
+ public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
+ public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
+ "pref_suppress_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";
public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
"pref_key_preview_popup_dismiss_delay";
- public static final String PREF_KEY_USE_CONTACTS_DICT =
- "pref_key_use_contacts_dict";
-
- public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
-
- // Dialog ids
- private static final int VOICE_INPUT_CONFIRM_DIALOG = 0;
-
- public static class Values {
- // From resources:
- public final boolean mSwipeDownDismissKeyboardEnabled;
- public final int mDelayBeforeFadeoutLanguageOnSpacebar;
- public final int mDelayUpdateSuggestions;
- public final int mDelayUpdateOldSuggestions;
- public final int mDelayUpdateShiftState;
- public final int mDurationOfFadeoutLanguageOnSpacebar;
- public final float mFinalFadeoutFactorOfLanguageOnSpacebar;
- public final long mDoubleSpacesTurnIntoPeriodTimeout;
- public final String mWordSeparators;
- public final String mMagicSpaceStrippers;
- public final String mMagicSpaceSwappers;
- public final String mSuggestPuncs;
- public final SuggestedWords mSuggestPuncList;
-
- // From preferences:
- public final boolean mSoundOn; // Sound setting private to Latin IME (see mSilentModeOn)
- public final boolean mVibrateOn;
- public final boolean mKeyPreviewPopupOn;
- public final int mKeyPreviewPopupDismissDelay;
- public final boolean mAutoCap;
- public final boolean mQuickFixes;
- public final boolean mAutoCorrectEnabled;
- public final double mAutoCorrectionThreshold;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- public final boolean mBigramSuggestionEnabled;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
- public final boolean mBigramPredictionEnabled;
- public final boolean mUseContactsDict;
-
- public Values(final SharedPreferences prefs, final Context context,
- final String localeStr) {
- final Resources res = context.getResources();
- final Locale savedLocale;
- if (null != localeStr) {
- final Locale keyboardLocale = Utils.constructLocaleFromString(localeStr);
- savedLocale = Utils.setSystemLocale(res, keyboardLocale);
- } else {
- savedLocale = null;
- }
-
- // Get the resources
- mSwipeDownDismissKeyboardEnabled = res.getBoolean(
- R.bool.config_swipe_down_dismiss_keyboard_enabled);
- mDelayBeforeFadeoutLanguageOnSpacebar = res.getInteger(
- R.integer.config_delay_before_fadeout_language_on_spacebar);
- mDelayUpdateSuggestions =
- res.getInteger(R.integer.config_delay_update_suggestions);
- mDelayUpdateOldSuggestions = res.getInteger(
- R.integer.config_delay_update_old_suggestions);
- mDelayUpdateShiftState =
- res.getInteger(R.integer.config_delay_update_shift_state);
- mDurationOfFadeoutLanguageOnSpacebar = res.getInteger(
- R.integer.config_duration_of_fadeout_language_on_spacebar);
- mFinalFadeoutFactorOfLanguageOnSpacebar = res.getInteger(
- R.integer.config_final_fadeout_percentage_of_language_on_spacebar) / 100.0f;
- mDoubleSpacesTurnIntoPeriodTimeout = res.getInteger(
- R.integer.config_double_spaces_turn_into_period_timeout);
- mMagicSpaceStrippers = res.getString(R.string.magic_space_stripping_symbols);
- mMagicSpaceSwappers = res.getString(R.string.magic_space_swapping_symbols);
- String wordSeparators = mMagicSpaceStrippers + mMagicSpaceSwappers
- + res.getString(R.string.magic_space_promoting_symbols);
- final String notWordSeparators = res.getString(R.string.non_word_separator_symbols);
- for (int i = notWordSeparators.length() - 1; i >= 0; --i) {
- wordSeparators = wordSeparators.replace(notWordSeparators.substring(i, i + 1), "");
- }
- mWordSeparators = wordSeparators;
- mSuggestPuncs = res.getString(R.string.suggested_punctuations);
- // TODO: it would be nice not to recreate this each time we change the configuration
- mSuggestPuncList = createSuggestPuncList(mSuggestPuncs);
-
- // Get the settings preferences
- final boolean hasVibrator = VibratorCompatWrapper.getInstance(context).hasVibrator();
- mVibrateOn = hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON, false);
- mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON,
- res.getBoolean(R.bool.config_default_sound_enabled));
-
- mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res);
- mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res);
- mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
- mQuickFixes = isQuickFixesEnabled(prefs, res);
-
- mAutoCorrectEnabled = isAutoCorrectEnabled(prefs, res);
- mBigramSuggestionEnabled = mAutoCorrectEnabled
- && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
- mBigramPredictionEnabled = mBigramSuggestionEnabled
- && isBigramPredictionEnabled(prefs, res);
-
- mAutoCorrectionThreshold = getAutoCorrectionThreshold(prefs, res);
-
- mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
-
- Utils.setSystemLocale(res, savedLocale);
- }
-
- public boolean isSuggestedPunctuation(int code) {
- return mSuggestPuncs.contains(String.valueOf((char)code));
- }
-
- public boolean isWordSeparator(int code) {
- return mWordSeparators.contains(String.valueOf((char)code));
- }
-
- public boolean isMagicSpaceStripper(int code) {
- return mMagicSpaceStrippers.contains(String.valueOf((char)code));
- }
+ public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+ public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
+ public static final String PREF_VIBRATION_DURATION_SETTINGS =
+ "pref_vibration_duration_settings";
+ public static final String PREF_KEYPRESS_SOUND_VOLUME =
+ "pref_keypress_sound_volume";
- public boolean isMagicSpaceSwapper(int code) {
- return mMagicSpaceSwappers.contains(String.valueOf((char)code));
- }
-
- // Helper methods
- private static boolean isQuickFixesEnabled(SharedPreferences sp, Resources resources) {
- final boolean showQuickFixesOption = resources.getBoolean(
- R.bool.config_enable_quick_fixes_option);
- if (!showQuickFixesOption) {
- return isAutoCorrectEnabled(sp, resources);
- }
- return sp.getBoolean(Settings.PREF_QUICK_FIXES, resources.getBoolean(
- R.bool.config_default_quick_fixes));
- }
-
- private static boolean isAutoCorrectEnabled(SharedPreferences sp, Resources resources) {
- final String currentAutoCorrectionSetting = sp.getString(
- Settings.PREF_AUTO_CORRECTION_THRESHOLD,
- resources.getString(R.string.auto_correction_threshold_mode_index_modest));
- final String autoCorrectionOff = resources.getString(
- R.string.auto_correction_threshold_mode_index_off);
- return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
- }
-
- // Public to access from KeyboardSwitcher. Should it have access to some
- // process-global instance instead?
- public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) {
- final boolean showPopupOption = resources.getBoolean(
- R.bool.config_enable_show_popup_on_keypress_option);
- if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview);
- return sp.getBoolean(Settings.PREF_KEY_PREVIEW_POPUP_ON,
- resources.getBoolean(R.bool.config_default_popup_preview));
- }
-
- // Likewise
- public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp,
- Resources resources) {
- return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
- Integer.toString(resources.getInteger(R.integer.config_delay_after_preview))));
- }
-
- private static boolean isBigramSuggestionEnabled(SharedPreferences sp, Resources resources,
- boolean autoCorrectEnabled) {
- final boolean showBigramSuggestionsOption = resources.getBoolean(
- R.bool.config_enable_bigram_suggestions_option);
- if (!showBigramSuggestionsOption) {
- return autoCorrectEnabled;
- }
- return sp.getBoolean(Settings.PREF_BIGRAM_SUGGESTIONS, resources.getBoolean(
- R.bool.config_default_bigram_suggestions));
- }
-
- private static boolean isBigramPredictionEnabled(SharedPreferences sp,
- Resources resources) {
- return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
- R.bool.config_default_bigram_prediction));
- }
-
- private static double getAutoCorrectionThreshold(SharedPreferences sp,
- Resources resources) {
- final String currentAutoCorrectionSetting = sp.getString(
- Settings.PREF_AUTO_CORRECTION_THRESHOLD,
- resources.getString(R.string.auto_correction_threshold_mode_index_modest));
- final String[] autoCorrectionThresholdValues = resources.getStringArray(
- R.array.auto_correction_threshold_values);
- // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
- double autoCorrectionThreshold = Double.MAX_VALUE;
- try {
- final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
- if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
- autoCorrectionThreshold = Double.parseDouble(
- autoCorrectionThresholdValues[arrayIndex]);
- }
- } catch (NumberFormatException e) {
- // Whenever the threshold settings are correct, never come here.
- autoCorrectionThreshold = Double.MAX_VALUE;
- Log.w(TAG, "Cannot load auto correction threshold setting."
- + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
- + ", autoCorrectionThresholdValues: "
- + Arrays.toString(autoCorrectionThresholdValues));
- }
- return autoCorrectionThreshold;
- }
-
- private static SuggestedWords createSuggestPuncList(final String puncs) {
- SuggestedWords.Builder builder = new SuggestedWords.Builder();
- if (puncs != null) {
- for (int i = 0; i < puncs.length(); i++) {
- builder.addWord(puncs.subSequence(i, i + 1));
- }
- }
- return builder.build();
- }
- }
+ public static final String PREF_INPUT_LANGUAGE = "input_language";
+ public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
+ public static final String PREF_DEBUG_SETTINGS = "debug_settings";
- private PreferenceScreen mInputLanguageSelection;
- private CheckBoxPreference mQuickFixes;
+ private PreferenceScreen mKeypressVibrationDurationSettingsPref;
+ private PreferenceScreen mKeypressSoundVolumeSettingsPref;
private ListPreference mVoicePreference;
- private ListPreference mSettingsKeyPreference;
private ListPreference mShowCorrectionSuggestionsPreference;
- private ListPreference mAutoCorrectionThreshold;
+ private ListPreference mAutoCorrectionThresholdPreference;
private ListPreference mKeyPreviewPopupDismissDelay;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- private CheckBoxPreference mBigramSuggestion;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
+ // Use bigrams to predict the next word when there is no input for it yet
private CheckBoxPreference mBigramPrediction;
private Preference mDebugSettingsPreference;
- private boolean mVoiceOn;
-
- private AlertDialog mDialog;
- private VoiceProxy.VoiceLoggerWrapper mVoiceLogger;
-
- private boolean mOkClicked = false;
- private String mVoiceModeOff;
+ private TextView mKeypressVibrationDurationSettingsTextView;
+ private TextView mKeypressSoundVolumeSettingsTextView;
private void ensureConsistencyOfAutoCorrectionSettings() {
final String autoCorrectionOff = getResources().getString(
R.string.auto_correction_threshold_mode_index_off);
- final String currentSetting = mAutoCorrectionThreshold.getValue();
- mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
- mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
+ final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
+ if (null != mBigramPrediction) {
+ mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
+ }
}
@Override
- protected void onCreate(Bundle icicle) {
+ public void onCreate(Bundle icicle) {
super.onCreate(icicle);
+ setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
+ setSubtypeEnablerTitle(R.string.select_language);
+ addPreferencesFromResource(R.xml.prefs);
+
final Resources res = getResources();
+ final Context context = getActivity();
- addPreferencesFromResource(R.xml.prefs);
- mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES);
- mInputLanguageSelection.setOnPreferenceClickListener(this);
- mQuickFixes = (CheckBoxPreference) findPreference(PREF_QUICK_FIXES);
- mVoicePreference = (ListPreference) findPreference(PREF_VOICE_SETTINGS_KEY);
- mSettingsKeyPreference = (ListPreference) findPreference(PREF_SETTINGS_KEY);
+ mVoicePreference = (ListPreference) findPreference(PREF_VOICE_MODE);
mShowCorrectionSuggestionsPreference =
(ListPreference) findPreference(PREF_SHOW_SUGGESTIONS_SETTING);
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
- mVoiceModeOff = getString(R.string.voice_mode_off);
- mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
- .equals(mVoiceModeOff));
- mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(this);
-
- mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
- mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS);
+ mAutoCorrectionThresholdPreference =
+ (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
if (mDebugSettingsPreference != null) {
final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
- debugSettingsIntent.setClassName(getPackageName(), DebugSettings.class.getName());
+ debugSettingsIntent.setClassName(
+ context.getPackageName(), DebugSettings.class.getName());
mDebugSettingsPreference.setIntent(debugSettingsIntent);
}
ensureConsistencyOfAutoCorrectionSettings();
final PreferenceGroup generalSettings =
- (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS_KEY);
+ (PreferenceGroup) findPreference(PREF_GENERAL_SETTINGS);
final PreferenceGroup textCorrectionGroup =
- (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY);
-
- final boolean showSettingsKeyOption = res.getBoolean(
- R.bool.config_enable_show_settings_key_option);
- if (!showSettingsKeyOption) {
- generalSettings.removePreference(mSettingsKeyPreference);
- }
+ (PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS);
+ final PreferenceGroup miscSettings =
+ (PreferenceGroup) findPreference(PREF_MISC_SETTINGS);
final boolean showVoiceKeyOption = res.getBoolean(
R.bool.config_enable_show_voice_key_option);
@@ -371,44 +143,25 @@ public class Settings extends PreferenceActivity
generalSettings.removePreference(mVoicePreference);
}
- if (!VibratorCompatWrapper.getInstance(this).hasVibrator()) {
+ if (!VibratorUtils.getInstance(context).hasVibrator()) {
+ final PreferenceGroup advancedSettings =
+ (PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
- }
-
- if (InputMethodServiceCompatWrapper.CAN_HANDLE_ON_CURRENT_INPUT_METHOD_SUBTYPE_CHANGED) {
- generalSettings.removePreference(findPreference(PREF_SUBTYPES));
+ if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+ advancedSettings.removePreference(findPreference(PREF_VIBRATION_DURATION_SETTINGS));
+ }
}
final boolean showPopupOption = res.getBoolean(
R.bool.config_enable_show_popup_on_keypress_option);
if (!showPopupOption) {
- generalSettings.removePreference(findPreference(PREF_KEY_PREVIEW_POPUP_ON));
- }
-
- final boolean showRecorrectionOption = res.getBoolean(
- R.bool.config_enable_show_recorrection_option);
- if (!showRecorrectionOption) {
- generalSettings.removePreference(findPreference(PREF_RECORRECTION_ENABLED));
+ generalSettings.removePreference(findPreference(PREF_POPUP_ON));
}
- final boolean showQuickFixesOption = res.getBoolean(
- R.bool.config_enable_quick_fixes_option);
- if (!showQuickFixesOption) {
- textCorrectionGroup.removePreference(findPreference(PREF_QUICK_FIXES));
- }
-
- final boolean showBigramSuggestionsOption = res.getBoolean(
- R.bool.config_enable_bigram_suggestions_option);
- if (!showBigramSuggestionsOption) {
- textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_SUGGESTIONS));
- textCorrectionGroup.removePreference(findPreference(PREF_BIGRAM_PREDICTIONS));
- }
-
- final boolean showUsabilityModeStudyOption = res.getBoolean(
- R.bool.config_enable_usability_study_mode_option);
- if (!showUsabilityModeStudyOption) {
- getPreferenceScreen().removePreference(findPreference(PREF_USABILITY_STUDY_MODE));
- }
+ final CheckBoxPreference includeOtherImesInLanguageSwitchList =
+ (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
+ includeOtherImesInLanguageSwitchList.setEnabled(
+ !SettingsValues.isLanguageSwitchKeySupressed(prefs));
mKeyPreviewPopupDismissDelay =
(ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
@@ -417,7 +170,7 @@ public class Settings extends PreferenceActivity
res.getString(R.string.key_preview_popup_dismiss_default_delay),
};
final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
- R.integer.config_delay_after_preview));
+ R.integer.config_key_preview_linger_timeout));
mKeyPreviewPopupDismissDelay.setEntries(entries);
mKeyPreviewPopupDismissDelay.setEntryValues(
new String[] { "0", popupDismissDelayDefaultValue });
@@ -425,39 +178,80 @@ public class Settings extends PreferenceActivity
mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
}
mKeyPreviewPopupDismissDelay.setEnabled(
- Settings.Values.isKeyPreviewPopupEnabled(prefs, res));
+ SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
final PreferenceScreen dictionaryLink =
(PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
final Intent intent = dictionaryLink.getIntent();
- final int number = getPackageManager().queryIntentActivities(intent, 0).size();
+ final int number = context.getPackageManager().queryIntentActivities(intent, 0).size();
if (0 >= number) {
textCorrectionGroup.removePreference(dictionaryLink);
}
+
+ final boolean showUsabilityStudyModeOption =
+ res.getBoolean(R.bool.config_enable_usability_study_mode_option)
+ || ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS;
+ final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE);
+ if (!showUsabilityStudyModeOption) {
+ if (usabilityStudyPref != null) {
+ miscSettings.removePreference(usabilityStudyPref);
+ }
+ }
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ if (usabilityStudyPref instanceof CheckBoxPreference) {
+ CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
+ checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, true));
+ checkbox.setSummary(R.string.settings_warning_researcher_mode);
+ }
+ }
+
+ mKeypressVibrationDurationSettingsPref =
+ (PreferenceScreen) findPreference(PREF_VIBRATION_DURATION_SETTINGS);
+ if (mKeypressVibrationDurationSettingsPref != null) {
+ mKeypressVibrationDurationSettingsPref.setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference arg0) {
+ showKeypressVibrationDurationSettingsDialog();
+ return true;
+ }
+ });
+ updateKeypressVibrationDurationSettingsSummary(prefs, res);
+ }
+
+ mKeypressSoundVolumeSettingsPref =
+ (PreferenceScreen) findPreference(PREF_KEYPRESS_SOUND_VOLUME);
+ if (mKeypressSoundVolumeSettingsPref != null) {
+ mKeypressSoundVolumeSettingsPref.setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference arg0) {
+ showKeypressSoundVolumeSettingDialog();
+ return true;
+ }
+ });
+ updateKeypressSoundVolumeSummary(prefs, res);
+ }
+ refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, res);
}
@Override
- protected void onResume() {
+ public void onResume() {
super.onResume();
- int autoTextSize = AutoText.getSize(getListView());
- if (autoTextSize < 1) {
- ((PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY))
- .removePreference(mQuickFixes);
- }
- if (!VoiceProxy.VOICE_INSTALLED
- || !SpeechRecognizer.isRecognitionAvailable(this)) {
- getPreferenceScreen().removePreference(mVoicePreference);
- } else {
+ final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
+ if (isShortcutImeEnabled) {
updateVoiceModeSummary();
+ } else {
+ getPreferenceScreen().removePreference(mVoicePreference);
}
- updateSettingsKeySummary();
updateShowCorrectionSuggestionsSummary();
updateKeyPreviewPopupDelaySummary();
+ updateCustomInputStylesSummary();
}
@Override
- protected void onDestroy() {
+ public void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
this);
super.onDestroy();
@@ -465,38 +259,25 @@ public class Settings extends PreferenceActivity
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- (new BackupManager(this)).dataChanged();
- // If turning on voice input, show dialog
- if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) {
- if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
- .equals(mVoiceModeOff)) {
- showVoiceConfirmation();
- }
- } else if (key.equals(PREF_KEY_PREVIEW_POPUP_ON)) {
+ (new BackupManager(getActivity())).dataChanged();
+ if (key.equals(PREF_POPUP_ON)) {
final ListPreference popupDismissDelay =
(ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
if (null != popupDismissDelay) {
- popupDismissDelay.setEnabled(prefs.getBoolean(PREF_KEY_PREVIEW_POPUP_ON, true));
+ popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true));
}
+ } else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
+ final CheckBoxPreference includeOtherImesInLanguageSwicthList =
+ (CheckBoxPreference)findPreference(
+ PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
+ includeOtherImesInLanguageSwicthList.setEnabled(
+ !SettingsValues.isLanguageSwitchKeySupressed(prefs));
}
ensureConsistencyOfAutoCorrectionSettings();
- mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
- .equals(mVoiceModeOff));
updateVoiceModeSummary();
- updateSettingsKeySummary();
updateShowCorrectionSuggestionsSummary();
updateKeyPreviewPopupDelaySummary();
- }
-
- @Override
- public boolean onPreferenceClick(Preference pref) {
- if (pref == mInputLanguageSelection) {
- startActivity(CompatUtils.getInputLanguageSelectionIntent(
- Utils.getInputMethodId(InputMethodManagerCompatWrapper.getInstance(this),
- getApplicationInfo().packageName), 0));
- return true;
- }
- return false;
+ refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources());
}
private void updateShowCorrectionSuggestionsSummary() {
@@ -506,10 +287,20 @@ public class Settings extends PreferenceActivity
mShowCorrectionSuggestionsPreference.getValue())]);
}
- private void updateSettingsKeySummary() {
- mSettingsKeyPreference.setSummary(
- getResources().getStringArray(R.array.settings_key_modes)
- [mSettingsKeyPreference.findIndexOfValue(mSettingsKeyPreference.getValue())]);
+ private void updateCustomInputStylesSummary() {
+ final PreferenceScreen customInputStyles =
+ (PreferenceScreen)findPreference(PREF_CUSTOM_INPUT_STYLES);
+ final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
+ final Resources res = getResources();
+ final String prefSubtype = SettingsValues.getPrefAdditionalSubtypes(prefs, res);
+ final InputMethodSubtype[] subtypes =
+ AdditionalSubtype.createAdditionalSubtypesArray(prefSubtype);
+ final StringBuilder styles = new StringBuilder();
+ for (final InputMethodSubtype subtype : subtypes) {
+ if (styles.length() > 0) styles.append(", ");
+ styles.append(SubtypeLocale.getSubtypeDisplayName(subtype, res));
+ }
+ customInputStyles.setSummary(styles);
}
private void updateKeyPreviewPopupDelaySummary() {
@@ -517,87 +308,144 @@ public class Settings extends PreferenceActivity
lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]);
}
- private void showVoiceConfirmation() {
- mOkClicked = false;
- showDialog(VOICE_INPUT_CONFIRM_DIALOG);
- // Make URL in the dialog message clickable
- if (mDialog != null) {
- TextView textView = (TextView) mDialog.findViewById(android.R.id.message);
- if (textView != null) {
- textView.setMovementMethod(LinkMovementMethod.getInstance());
- }
- }
- }
-
private void updateVoiceModeSummary() {
mVoicePreference.setSummary(
getResources().getStringArray(R.array.voice_input_modes_summary)
[mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
}
- @Override
- protected Dialog onCreateDialog(int id) {
- switch (id) {
- case VOICE_INPUT_CONFIRM_DIALOG:
- DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int whichButton) {
- if (whichButton == DialogInterface.BUTTON_NEGATIVE) {
- mVoicePreference.setValue(mVoiceModeOff);
- mVoiceLogger.settingsWarningDialogCancel();
- } else if (whichButton == DialogInterface.BUTTON_POSITIVE) {
- mOkClicked = true;
- mVoiceLogger.settingsWarningDialogOk();
- }
- updateVoicePreference();
- }
- };
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.voice_warning_title)
- .setPositiveButton(android.R.string.ok, listener)
- .setNegativeButton(android.R.string.cancel, listener);
-
- // Get the current list of supported locales and check the current locale against
- // that list, to decide whether to put a warning that voice input will not work in
- // the current language as part of the pop-up confirmation dialog.
- boolean localeSupported = SubtypeSwitcher.getInstance().isVoiceSupported(
- Locale.getDefault().toString());
-
- final CharSequence message;
- if (localeSupported) {
- message = TextUtils.concat(
- getText(R.string.voice_warning_may_not_understand), "\n\n",
- getText(R.string.voice_hint_dialog_message));
- } else {
- message = TextUtils.concat(
- getText(R.string.voice_warning_locale_not_supported), "\n\n",
- getText(R.string.voice_warning_may_not_understand), "\n\n",
- getText(R.string.voice_hint_dialog_message));
- }
- builder.setMessage(message);
- AlertDialog dialog = builder.create();
- mDialog = dialog;
- dialog.setOnDismissListener(this);
- mVoiceLogger.settingsWarningDialogShown();
- return dialog;
- default:
- Log.e(TAG, "unknown dialog " + id);
- return null;
+ private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
+ SharedPreferences sp, Resources res) {
+ if (mKeypressVibrationDurationSettingsPref != null) {
+ final boolean hasVibrator = VibratorUtils.getInstance(getActivity()).hasVibrator();
+ final boolean vibrateOn = hasVibrator && sp.getBoolean(Settings.PREF_VIBRATE_ON,
+ res.getBoolean(R.bool.config_default_vibration_enabled));
+ mKeypressVibrationDurationSettingsPref.setEnabled(vibrateOn);
+ }
+
+ if (mKeypressSoundVolumeSettingsPref != null) {
+ final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
+ res.getBoolean(R.bool.config_default_sound_enabled));
+ mKeypressSoundVolumeSettingsPref.setEnabled(soundOn);
}
}
- @Override
- public void onDismiss(DialogInterface dialog) {
- mVoiceLogger.settingsWarningDialogDismissed();
- if (!mOkClicked) {
- // This assumes that onPreferenceClick gets called first, and this if the user
- // agreed after the warning, we set the mOkClicked value to true.
- mVoicePreference.setValue(mVoiceModeOff);
+ private void updateKeypressVibrationDurationSettingsSummary(
+ SharedPreferences sp, Resources res) {
+ if (mKeypressVibrationDurationSettingsPref != null) {
+ mKeypressVibrationDurationSettingsPref.setSummary(
+ SettingsValues.getCurrentVibrationDuration(sp, res)
+ + res.getString(R.string.settings_ms));
}
}
- private void updateVoicePreference() {
- boolean isChecked = !mVoicePreference.getValue().equals(mVoiceModeOff);
- mVoiceLogger.voiceInputSettingEnabled(isChecked);
+ private void showKeypressVibrationDurationSettingsDialog() {
+ final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
+ final Context context = getActivity();
+ final Resources res = context.getResources();
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.prefs_keypress_vibration_duration_settings);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ final int ms = Integer.valueOf(
+ mKeypressVibrationDurationSettingsTextView.getText().toString());
+ sp.edit().putInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, ms).apply();
+ updateKeypressVibrationDurationSettingsSummary(sp, res);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+ final View v = LayoutInflater.from(context).inflate(
+ R.layout.vibration_settings_dialog, null);
+ final int currentMs = SettingsValues.getCurrentVibrationDuration(
+ getPreferenceManager().getSharedPreferences(), getResources());
+ mKeypressVibrationDurationSettingsTextView = (TextView)v.findViewById(R.id.vibration_value);
+ final SeekBar sb = (SeekBar)v.findViewById(R.id.vibration_settings);
+ sb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
+ final int tempMs = arg1;
+ mKeypressVibrationDurationSettingsTextView.setText(String.valueOf(tempMs));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ final int tempMs = arg0.getProgress();
+ VibratorUtils.getInstance(context).vibrate(tempMs);
+ }
+ });
+ sb.setProgress(currentMs);
+ mKeypressVibrationDurationSettingsTextView.setText(String.valueOf(currentMs));
+ builder.setView(v);
+ builder.create().show();
+ }
+
+ private void updateKeypressSoundVolumeSummary(SharedPreferences sp, Resources res) {
+ if (mKeypressSoundVolumeSettingsPref != null) {
+ mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf(
+ (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100)));
+ }
+ }
+
+ private void showKeypressSoundVolumeSettingDialog() {
+ final Context context = getActivity();
+ final AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
+ final Resources res = context.getResources();
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.prefs_keypress_sound_volume_settings);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ final float volume =
+ ((float)Integer.valueOf(
+ mKeypressSoundVolumeSettingsTextView.getText().toString())) / 100;
+ sp.edit().putFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, volume).apply();
+ updateKeypressSoundVolumeSummary(sp, res);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.dismiss();
+ }
+ });
+ final View v = LayoutInflater.from(context).inflate(
+ R.layout.sound_effect_volume_dialog, null);
+ final int currentVolumeInt =
+ (int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100);
+ mKeypressSoundVolumeSettingsTextView =
+ (TextView)v.findViewById(R.id.sound_effect_volume_value);
+ final SeekBar sb = (SeekBar)v.findViewById(R.id.sound_effect_volume_bar);
+ sb.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
+ final int tempVolume = arg1;
+ mKeypressSoundVolumeSettingsTextView.setText(String.valueOf(tempVolume));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ final float tempVolume = ((float)arg0.getProgress()) / 100;
+ am.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD, tempVolume);
+ }
+ });
+ sb.setProgress(currentVolumeInt);
+ mKeypressSoundVolumeSettingsTextView.setText(String.valueOf(currentVolumeInt));
+ builder.setView(v);
+ builder.create().show();
}
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java
new file mode 100644
index 000000000..68f8582fc
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import android.content.Intent;
+import android.preference.PreferenceActivity;
+
+public class SettingsActivity extends PreferenceActivity {
+ private static final String DEFAULT_FRAGMENT = Settings.class.getName();
+
+ @Override
+ public Intent getIntent() {
+ final Intent intent = super.getIntent();
+ if (!intent.hasExtra(EXTRA_SHOW_FRAGMENT)) {
+ intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+ }
+ intent.putExtra(EXTRA_NO_HEADERS, true);
+ return intent;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
new file mode 100644
index 000000000..ef423f19b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * When you call the constructor of this class, you may want to change the current system locale by
+ * using {@link LocaleUtils.RunInLocale}.
+ */
+public class SettingsValues {
+ private static final String TAG = SettingsValues.class.getSimpleName();
+
+ private static final int SUGGESTION_VISIBILITY_SHOW_VALUE
+ = R.string.prefs_suggestion_visibility_show_value;
+ private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+ private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
+ = R.string.prefs_suggestion_visibility_hide_value;
+
+ private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+ SUGGESTION_VISIBILITY_SHOW_VALUE,
+ SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
+ SUGGESTION_VISIBILITY_HIDE_VALUE
+ };
+
+ // From resources:
+ public final int mDelayUpdateOldSuggestions;
+ public final String mWeakSpaceStrippers;
+ public final String mWeakSpaceSwappers;
+ private final String mPhantomSpacePromotingSymbols;
+ public final SuggestedWords mSuggestPuncList;
+ private final String mSymbolsExcludedFromWordSeparators;
+ public final String mWordSeparators;
+ public final CharSequence mHintToSaveText;
+
+ // From preferences, in the same order as xml/prefs.xml:
+ public final boolean mAutoCap;
+ public final boolean mVibrateOn;
+ public final boolean mSoundOn;
+ public final boolean mKeyPreviewPopupOn;
+ private final String mVoiceMode;
+ private final String mAutoCorrectionThresholdRawValue;
+ public final String mShowSuggestionsSetting;
+ @SuppressWarnings("unused") // TODO: Use this
+ private final boolean mUsabilityStudyMode;
+ public final boolean mIncludesOtherImesInLanguageSwitchList;
+ public final boolean mIsLanguageSwitchKeySuppressed;
+ @SuppressWarnings("unused") // TODO: Use this
+ private final String mKeyPreviewPopupDismissDelayRawValue;
+ public final boolean mUseContactsDict;
+ // Use bigrams to predict the next word when there is no input for it yet
+ public final boolean mBigramPredictionEnabled;
+ @SuppressWarnings("unused") // TODO: Use this
+ private final int mVibrationDurationSettingsRawValue;
+ @SuppressWarnings("unused") // TODO: Use this
+ private final float mKeypressSoundVolumeRawValue;
+ private final InputMethodSubtype[] mAdditionalSubtypes;
+
+ // From the input box
+ private final InputAttributes mInputAttributes;
+
+ // Deduced settings
+ public final int mKeypressVibrationDuration;
+ public final float mFxVolume;
+ public final int mKeyPreviewPopupDismissDelay;
+ private final boolean mAutoCorrectEnabled;
+ public final float mAutoCorrectionThreshold;
+ public final boolean mCorrectionEnabled;
+ public final int mSuggestionVisibility;
+ private final boolean mVoiceKeyEnabled;
+ private final boolean mVoiceKeyOnMain;
+
+ public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
+ final Context context) {
+ final Resources res = context.getResources();
+
+ // Get the resources
+ mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions);
+ mWeakSpaceStrippers = res.getString(R.string.weak_space_stripping_symbols);
+ mWeakSpaceSwappers = res.getString(R.string.weak_space_swapping_symbols);
+ mPhantomSpacePromotingSymbols = res.getString(R.string.phantom_space_promoting_symbols);
+ if (LatinImeLogger.sDBG) {
+ final int length = mWeakSpaceStrippers.length();
+ for (int i = 0; i < length; i = mWeakSpaceStrippers.offsetByCodePoints(i, 1)) {
+ if (isWeakSpaceSwapper(mWeakSpaceStrippers.codePointAt(i))) {
+ throw new RuntimeException("Char code " + mWeakSpaceStrippers.codePointAt(i)
+ + " is both a weak space swapper and stripper.");
+ }
+ }
+ }
+ final String[] suggestPuncsSpec = KeySpecParser.parseCsvString(
+ res.getString(R.string.suggested_punctuations), null);
+ mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
+ mSymbolsExcludedFromWordSeparators =
+ res.getString(R.string.symbols_excluded_from_word_separators);
+ mWordSeparators = createWordSeparators(mWeakSpaceStrippers, mWeakSpaceSwappers,
+ mSymbolsExcludedFromWordSeparators, res);
+ mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
+
+ // Store the input attributes
+ if (null == inputAttributes) {
+ mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+ } else {
+ mInputAttributes = inputAttributes;
+ }
+
+ // Get the settings preferences
+ mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
+ mVibrateOn = isVibrateOn(context, prefs, res);
+ mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON,
+ res.getBoolean(R.bool.config_default_sound_enabled));
+ mKeyPreviewPopupOn = isKeyPreviewPopupEnabled(prefs, res);
+ final String voiceModeMain = res.getString(R.string.voice_mode_main);
+ final String voiceModeOff = res.getString(R.string.voice_mode_off);
+ mVoiceMode = prefs.getString(Settings.PREF_VOICE_MODE, voiceModeMain);
+ mAutoCorrectionThresholdRawValue = prefs.getString(Settings.PREF_AUTO_CORRECTION_THRESHOLD,
+ res.getString(R.string.auto_correction_threshold_mode_index_modest));
+ mShowSuggestionsSetting = prefs.getString(Settings.PREF_SHOW_SUGGESTIONS_SETTING,
+ res.getString(R.string.prefs_suggestion_visibility_default_value));
+ mUsabilityStudyMode = getUsabilityStudyMode(prefs);
+ mIncludesOtherImesInLanguageSwitchList = prefs.getBoolean(
+ Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false);
+ mIsLanguageSwitchKeySuppressed = isLanguageSwitchKeySupressed(prefs);
+ mKeyPreviewPopupDismissDelayRawValue = prefs.getString(
+ Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
+ mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
+ mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
+ mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
+ mVibrationDurationSettingsRawValue =
+ prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
+ mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
+
+ // Compute other readable settings
+ mKeypressVibrationDuration = getCurrentVibrationDuration(prefs, res);
+ mFxVolume = getCurrentKeypressSoundVolume(prefs, res);
+ mKeyPreviewPopupDismissDelay = getKeyPreviewPopupDismissDelay(prefs, res);
+ mAutoCorrectionThreshold = getAutoCorrectionThreshold(res,
+ mAutoCorrectionThresholdRawValue);
+ mVoiceKeyEnabled = mVoiceMode != null && !mVoiceMode.equals(voiceModeOff);
+ mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
+ mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
+ getPrefAdditionalSubtypes(prefs, res));
+ mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+ mSuggestionVisibility = createSuggestionVisibility(res);
+ }
+
+ // Helper functions to create member values.
+ private static SuggestedWords createSuggestPuncList(final String[] puncs) {
+ final ArrayList<SuggestedWordInfo> puncList = new ArrayList<SuggestedWordInfo>();
+ if (puncs != null) {
+ for (final String puncSpec : puncs) {
+ puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED));
+ }
+ }
+ return new SuggestedWords(puncList,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* allowsToBeAutoCorrected */,
+ true /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ }
+
+ private static String createWordSeparators(final String weakSpaceStrippers,
+ final String weakSpaceSwappers, final String symbolsExcludedFromWordSeparators,
+ final Resources res) {
+ String wordSeparators = weakSpaceStrippers + weakSpaceSwappers
+ + res.getString(R.string.phantom_space_promoting_symbols);
+ for (int i = symbolsExcludedFromWordSeparators.length() - 1; i >= 0; --i) {
+ wordSeparators = wordSeparators.replace(
+ symbolsExcludedFromWordSeparators.substring(i, i + 1), "");
+ }
+ return wordSeparators;
+ }
+
+ private int createSuggestionVisibility(final Resources res) {
+ final String suggestionVisiblityStr = mShowSuggestionsSetting;
+ for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+ if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+ return visibility;
+ }
+ }
+ throw new RuntimeException("Bug: visibility string is not configured correctly");
+ }
+
+ private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
+ final Resources res) {
+ final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
+ return hasVibrator && prefs.getBoolean(Settings.PREF_VIBRATE_ON,
+ res.getBoolean(R.bool.config_default_vibration_enabled));
+ }
+
+ public boolean isApplicationSpecifiedCompletionsOn() {
+ return mInputAttributes.mApplicationSpecifiedCompletionOn;
+ }
+
+ public boolean isEditorActionNext() {
+ return mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT;
+ }
+
+ public boolean isSuggestionsRequested(final int displayOrientation) {
+ return mInputAttributes.mIsSettingsSuggestionStripOn
+ && (mCorrectionEnabled
+ || isSuggestionStripVisibleInOrientation(displayOrientation));
+ }
+
+ public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+ return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
+ || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ && orientation == Configuration.ORIENTATION_PORTRAIT);
+ }
+
+ public boolean isWordSeparator(int code) {
+ return mWordSeparators.contains(String.valueOf((char)code));
+ }
+
+ public boolean isSymbolExcludedFromWordSeparators(int code) {
+ return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code));
+ }
+
+ public boolean isWeakSpaceStripper(int code) {
+ // TODO: this does not work if the code does not fit in a char
+ return mWeakSpaceStrippers.contains(String.valueOf((char)code));
+ }
+
+ public boolean isWeakSpaceSwapper(int code) {
+ // TODO: this does not work if the code does not fit in a char
+ return mWeakSpaceSwappers.contains(String.valueOf((char)code));
+ }
+
+ public boolean isPhantomSpacePromotingSymbol(int code) {
+ // TODO: this does not work if the code does not fit in a char
+ return mPhantomSpacePromotingSymbols.contains(String.valueOf((char)code));
+ }
+
+ private static boolean isAutoCorrectEnabled(final Resources resources,
+ final String currentAutoCorrectionSetting) {
+ final String autoCorrectionOff = resources.getString(
+ R.string.auto_correction_threshold_mode_index_off);
+ return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
+ }
+
+ // Public to access from KeyboardSwitcher. Should it have access to some
+ // process-global instance instead?
+ public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) {
+ final boolean showPopupOption = resources.getBoolean(
+ R.bool.config_enable_show_popup_on_keypress_option);
+ if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview);
+ return sp.getBoolean(Settings.PREF_POPUP_ON,
+ resources.getBoolean(R.bool.config_default_popup_preview));
+ }
+
+ // Likewise
+ public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp,
+ Resources resources) {
+ // TODO: use mKeyPreviewPopupDismissDelayRawValue instead of reading it again here.
+ return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Integer.toString(resources.getInteger(
+ R.integer.config_key_preview_linger_timeout))));
+ }
+
+ private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
+ final Resources resources) {
+ return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
+ R.bool.config_default_next_word_prediction));
+ }
+
+ private static float getAutoCorrectionThreshold(final Resources resources,
+ final String currentAutoCorrectionSetting) {
+ final String[] autoCorrectionThresholdValues = resources.getStringArray(
+ R.array.auto_correction_threshold_values);
+ // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
+ float autoCorrectionThreshold = Float.MAX_VALUE;
+ try {
+ final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
+ if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
+ autoCorrectionThreshold = Float.parseFloat(
+ autoCorrectionThresholdValues[arrayIndex]);
+ }
+ } catch (NumberFormatException e) {
+ // Whenever the threshold settings are correct, never come here.
+ autoCorrectionThreshold = Float.MAX_VALUE;
+ Log.w(TAG, "Cannot load auto correction threshold setting."
+ + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
+ + ", autoCorrectionThresholdValues: "
+ + Arrays.toString(autoCorrectionThresholdValues));
+ }
+ return autoCorrectionThreshold;
+ }
+
+ public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) {
+ final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
+ final int inputType = (editorInfo != null) ? editorInfo.inputType : 0;
+ return shortcutImeEnabled && mVoiceKeyEnabled
+ && !InputTypeUtils.isPasswordInputType(inputType);
+ }
+
+ public boolean isVoiceKeyOnMain() {
+ return mVoiceKeyOnMain;
+ }
+
+ public static boolean isLanguageSwitchKeySupressed(SharedPreferences sp) {
+ return sp.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+ }
+
+ public boolean isLanguageSwitchKeyEnabled(Context context) {
+ if (mIsLanguageSwitchKeySuppressed) {
+ return false;
+ }
+ if (mIncludesOtherImesInLanguageSwitchList) {
+ return ImfUtils.hasMultipleEnabledIMEsOrSubtypes(
+ context, /* include aux subtypes */false);
+ } else {
+ return ImfUtils.hasMultipleEnabledSubtypesInThisIme(
+ context, /* include aux subtypes */false);
+ }
+ }
+
+ public boolean isFullscreenModeAllowed(Resources res) {
+ return res.getBoolean(R.bool.config_use_fullscreen_mode);
+ }
+
+ public InputMethodSubtype[] getAdditionalSubtypes() {
+ return mAdditionalSubtypes;
+ }
+
+ public static String getPrefAdditionalSubtypes(final SharedPreferences prefs,
+ final Resources res) {
+ final String prefSubtypes = res.getString(R.string.predefined_subtypes, "");
+ return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes);
+ }
+
+ // Accessed from the settings interface, hence public
+ public static float getCurrentKeypressSoundVolume(final SharedPreferences sp,
+ final Resources res) {
+ // TODO: use mVibrationDurationSettingsRawValue instead of reading it again here
+ final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
+ if (volume >= 0) {
+ return volume;
+ }
+
+ return Float.parseFloat(
+ Utils.getDeviceOverrideValue(res, R.array.keypress_volumes, "-1.0f"));
+ }
+
+ // Likewise
+ public static int getCurrentVibrationDuration(final SharedPreferences sp,
+ final Resources res) {
+ // TODO: use mKeypressVibrationDuration instead of reading it again here
+ final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
+ if (ms >= 0) {
+ return ms;
+ }
+
+ return Integer.parseInt(
+ Utils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations, "-1"));
+ }
+
+ // Likewise
+ public static boolean getUsabilityStudyMode(final SharedPreferences prefs) {
+ // TODO: use mUsabilityStudyMode instead of reading it again here
+ return prefs.getBoolean(Settings.PREF_USABILITY_STUDY_MODE, true);
+ }
+
+ public static long getLastUserHistoryWriteTime(
+ final SharedPreferences prefs, final String locale) {
+ final String str = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
+ final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(str);
+ if (map.containsKey(locale)) {
+ return map.get(locale);
+ }
+ return 0;
+ }
+
+ public static void setLastUserHistoryWriteTime(
+ final SharedPreferences prefs, final String locale) {
+ final String oldStr = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
+ final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(oldStr);
+ map.put(locale, System.currentTimeMillis());
+ final String newStr = Utils.localeAndTimeHashMapToStr(map);
+ prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
+ }
+
+ // For debug.
+ public String getInputAttributesDebugString() {
+ return mInputAttributes.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SharedPreferencesCompat.java b/java/src/com/android/inputmethod/latin/SharedPreferencesCompat.java
deleted file mode 100644
index 1d36c0b98..000000000
--- a/java/src/com/android/inputmethod/latin/SharedPreferencesCompat.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.SharedPreferences;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-/**
- * Reflection utils to call SharedPreferences$Editor.apply when possible,
- * falling back to commit when apply isn't available.
- */
-public class SharedPreferencesCompat {
- private static final Method sApplyMethod = findApplyMethod();
-
- private static Method findApplyMethod() {
- try {
- return SharedPreferences.Editor.class.getMethod("apply");
- } catch (NoSuchMethodException unused) {
- // fall through
- }
- return null;
- }
-
- public static void apply(SharedPreferences.Editor editor) {
- if (sApplyMethod != null) {
- try {
- sApplyMethod.invoke(editor);
- return;
- } catch (InvocationTargetException unused) {
- // fall through
- } catch (IllegalAccessException unused) {
- // fall through
- }
- }
- editor.commit();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
new file mode 100644
index 000000000..a43b90525
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -0,0 +1,197 @@
+/*
+ * 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;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class StringUtils {
+ private StringUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static int codePointCount(String text) {
+ if (TextUtils.isEmpty(text)) return 0;
+ return text.codePointCount(0, text.length());
+ }
+
+ public static boolean containsInArray(String key, String[] array) {
+ for (final String element : array) {
+ if (key.equals(element)) return true;
+ }
+ return false;
+ }
+
+ public static boolean containsInCsv(String key, String csv) {
+ if (TextUtils.isEmpty(csv)) return false;
+ return containsInArray(key, csv.split(","));
+ }
+
+ public static String appendToCsvIfNotExists(String key, String csv) {
+ if (TextUtils.isEmpty(csv)) return key;
+ if (containsInCsv(key, csv)) return csv;
+ return csv + "," + key;
+ }
+
+ public static String removeFromCsvIfExists(String key, String csv) {
+ if (TextUtils.isEmpty(csv)) return "";
+ final String[] elements = csv.split(",");
+ if (!containsInArray(key, elements)) return csv;
+ final ArrayList<String> result = new ArrayList<String>(elements.length - 1);
+ for (final String element : elements) {
+ if (!key.equals(element)) result.add(element);
+ }
+ return TextUtils.join(",", result);
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the character.
+ * @param a first character to check
+ * @param b second character to check
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ */
+ public static boolean equalsIgnoreCase(char a, char b) {
+ // Some language, such as Turkish, need testing both cases.
+ return a == b
+ || Character.toLowerCase(a) == Character.toLowerCase(b)
+ || Character.toUpperCase(a) == Character.toUpperCase(b);
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the characters, including if they are
+ * both null.
+ * @param a first CharSequence to check
+ * @param b second CharSequence to check
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ */
+ public static boolean equalsIgnoreCase(CharSequence a, CharSequence b) {
+ if (a == b)
+ return true; // including both a and b are null.
+ if (a == null || b == null)
+ return false;
+ final int length = a.length();
+ if (length != b.length())
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreCase(a.charAt(i), b.charAt(i)))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if a and b are equal ignoring the case of the characters, including if a is null
+ * and b is zero length.
+ * @param a CharSequence to check
+ * @param b character array to check
+ * @param offset start offset of array b
+ * @param length length of characters in array b
+ * @return {@code true} if a and b are equal, {@code false} otherwise.
+ * @throws IndexOutOfBoundsException
+ * if {@code offset < 0 || length < 0 || offset + length > data.length}.
+ * @throws NullPointerException if {@code b == null}.
+ */
+ public static boolean equalsIgnoreCase(CharSequence a, char[] b, int offset, int length) {
+ if (offset < 0 || length < 0 || length > b.length - offset)
+ throw new IndexOutOfBoundsException("array.length=" + b.length + " offset=" + offset
+ + " length=" + length);
+ if (a == null)
+ return length == 0; // including a is null and b is zero length.
+ if (a.length() != length)
+ return false;
+ for (int i = 0; i < length; i++) {
+ if (!equalsIgnoreCase(a.charAt(i), b[offset + i]))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if cs contains any upper case characters.
+ *
+ * @param cs the CharSequence to check
+ * @return {@code true} if cs contains any upper case characters, {@code false} otherwise.
+ */
+ public static boolean hasUpperCase(final CharSequence cs) {
+ final int length = cs.length();
+ for (int i = 0, cp = 0; i < length; i += Character.charCount(cp)) {
+ cp = Character.codePointAt(cs, i);
+ if (Character.isUpperCase(cp)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove duplicates from an array of strings.
+ *
+ * This method will always keep the first occurrence of all strings at their position
+ * in the array, removing the subsequent ones.
+ */
+ public static void removeDupes(final ArrayList<CharSequence> suggestions) {
+ if (suggestions.size() < 2) return;
+ int i = 1;
+ // Don't cache suggestions.size(), since we may be removing items
+ while (i < suggestions.size()) {
+ final CharSequence cur = suggestions.get(i);
+ // Compare each suggestion with each previous suggestion
+ for (int j = 0; j < i; j++) {
+ CharSequence previous = suggestions.get(j);
+ if (TextUtils.equals(cur, previous)) {
+ suggestions.remove(i);
+ i--;
+ break;
+ }
+ }
+ i++;
+ }
+ }
+
+ public static String toTitleCase(String s, Locale locale) {
+ if (s.length() <= 1) {
+ // TODO: is this really correct? Shouldn't this be s.toUpperCase()?
+ return s;
+ }
+ // TODO: fix the bugs below
+ // - This does not work for Greek, because it returns upper case instead of title case.
+ // - It does not work for Serbian, because it fails to account for the "lj" character,
+ // which should be "Lj" in title case and "LJ" in upper case.
+ // - It does not work for Dutch, because it fails to account for the "ij" digraph, which
+ // are two different characters but both should be capitalized as "IJ" as if they were
+ // a single letter.
+ // - It also does not work with unicode surrogate code points.
+ return s.toUpperCase(locale).charAt(0) + s.substring(1);
+ }
+
+ public static int[] toCodePointArray(final String string) {
+ final char[] characters = string.toCharArray();
+ final int length = characters.length;
+ final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
+ int codePoint = Character.codePointAt(characters, 0);
+ int dsti = 0;
+ for (int srci = Character.charCount(codePoint);
+ srci < length; srci += Character.charCount(codePoint), ++dsti) {
+ codePoints[dsti] = codePoint;
+ codePoint = Character.codePointAt(characters, srci);
+ }
+ codePoints[dsti] = codePoint;
+ return codePoints;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 917521c40..ca293060a 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -16,14 +16,50 @@
package com.android.inputmethod.latin;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
import android.content.Context;
import android.content.res.Resources;
+import android.os.Build;
+import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import java.util.HashMap;
import java.util.Locale;
public class SubtypeLocale {
- private static String[] sExceptionKeys;
- private static String[] sExceptionValues;
+ static final String TAG = SubtypeLocale.class.getSimpleName();
+ // This class must be located in the same package as LatinIME.java.
+ private static final String RESOURCE_PACKAGE_NAME =
+ DictionaryFactory.class.getPackage().getName();
+
+ // Special language code to represent "no language".
+ public static final String NO_LANGUAGE = "zz";
+ public static final String QWERTY = "qwerty";
+ public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
+
+ private static String[] sPredefinedKeyboardLayoutSet;
+ // Keyboard layout to its display name map.
+ private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
+ new HashMap<String, String>();
+ // Keyboard layout to subtype name resource id map.
+ private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
+ new HashMap<String, Integer>();
+ // Exceptional locale to subtype name resource id map.
+ private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
+ new HashMap<String, Integer>();
+ private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
+ "string/subtype_generic_";
+ private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
+ "string/subtype_with_layout_";
+ private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX =
+ "string/subtype_no_language_";
+ // Exceptional locales to display name map.
+ private static final HashMap<String, String> sExceptionalDisplayNamesMap =
+ new HashMap<String, String>();
private SubtypeLocale() {
// Intentional empty constructor for utility class.
@@ -31,16 +67,140 @@ public class SubtypeLocale {
public static void init(Context context) {
final Resources res = context.getResources();
- sExceptionKeys = res.getStringArray(R.array.subtype_locale_exception_keys);
- sExceptionValues = res.getStringArray(R.array.subtype_locale_exception_values);
+
+ final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts);
+ sPredefinedKeyboardLayoutSet = predefinedLayoutSet;
+ final String[] layoutDisplayNames = res.getStringArray(
+ R.array.predefined_layout_display_names);
+ for (int i = 0; i < predefinedLayoutSet.length; i++) {
+ final String layoutName = predefinedLayoutSet[i];
+ sKeyboardLayoutToDisplayNameMap.put(layoutName, layoutDisplayNames[i]);
+ final String resourceName = SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX + layoutName;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sKeyboardLayoutToNameIdsMap.put(layoutName, resId);
+ // Register subtype name resource id of "No language" with key "zz_<layout>"
+ final String noLanguageResName = SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX + layoutName;
+ final int noLanguageResId = res.getIdentifier(
+ noLanguageResName, null, RESOURCE_PACKAGE_NAME);
+ final String key = getNoLanguageLayoutKey(layoutName);
+ sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
+ }
+
+ final String[] exceptionalLocales = res.getStringArray(
+ R.array.subtype_locale_exception_keys);
+ final String[] exceptionalDisplayNames = res.getStringArray(
+ R.array.subtype_locale_exception_values);
+ for (int i = 0; i < exceptionalLocales.length; i++) {
+ final String localeString = exceptionalLocales[i];
+ sExceptionalDisplayNamesMap.put(localeString, exceptionalDisplayNames[i]);
+ final String resourceName = SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX + localeString;
+ final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME);
+ sExceptionalLocaleToWithLayoutNameIdsMap.put(localeString, resId);
+ }
+ }
+
+ public static String[] getPredefinedKeyboardLayoutSet() {
+ return sPredefinedKeyboardLayoutSet;
+ }
+
+ public static boolean isExceptionalLocale(String localeString) {
+ return sExceptionalLocaleToWithLayoutNameIdsMap.containsKey(localeString);
+ }
+
+ private static final String getNoLanguageLayoutKey(String keyboardLayoutName) {
+ return NO_LANGUAGE + "_" + keyboardLayoutName;
+ }
+
+ public static int getSubtypeNameId(String localeString, String keyboardLayoutName) {
+ if (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15 && isExceptionalLocale(localeString)) {
+ return sExceptionalLocaleToWithLayoutNameIdsMap.get(localeString);
+ }
+ final String key = localeString.equals(NO_LANGUAGE)
+ ? getNoLanguageLayoutKey(keyboardLayoutName)
+ : keyboardLayoutName;
+ final Integer nameId = sKeyboardLayoutToNameIdsMap.get(key);
+ return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
+ }
+
+ public static String getSubtypeLocaleDisplayName(String localeString) {
+ final String exceptionalValue = sExceptionalDisplayNamesMap.get(localeString);
+ if (exceptionalValue != null) {
+ return exceptionalValue;
+ }
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
+ return StringUtils.toTitleCase(locale.getDisplayName(locale), locale);
+ }
+
+ // InputMethodSubtype's display name in its locale.
+ // isAdditionalSubtype (T=true, F=false)
+ // locale layout | display name
+ // ------ ------ - ----------------------
+ // en_US qwerty F English (US) exception
+ // en_GB qwerty F English (UK) exception
+ // fr azerty F Français
+ // fr_CA qwerty F Français (Canada)
+ // de qwertz F Deutsch
+ // zz qwerty F No language (QWERTY) in system locale
+ // fr qwertz T Français (QWERTZ)
+ // de qwerty T Deutsch (QWERTY)
+ // en_US azerty T English (US) (AZERTY)
+ // zz azerty T No language (AZERTY) in system locale
+
+ public static String getSubtypeDisplayName(final InputMethodSubtype subtype, Resources res) {
+ final String replacementString = (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15
+ && subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME))
+ ? subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)
+ : getSubtypeLocaleDisplayName(subtype.getLocale());
+ final int nameResId = subtype.getNameResId();
+ final RunInLocale<String> getSubtypeName = new RunInLocale<String>() {
+ @Override
+ protected String job(Resources res) {
+ try {
+ return res.getString(nameResId, replacementString);
+ } catch (Resources.NotFoundException e) {
+ // TODO: Remove this catch when InputMethodManager.getCurrentInputMethodSubtype
+ // is fixed.
+ Log.w(TAG, "Unknown subtype: mode=" + subtype.getMode()
+ + " locale=" + subtype.getLocale()
+ + " extra=" + subtype.getExtraValue()
+ + "\n" + Utils.getStackTrace());
+ return "";
+ }
+ }
+ };
+ final Locale locale = isNoLanguage(subtype)
+ ? res.getConfiguration().locale : getSubtypeLocale(subtype);
+ return getSubtypeName.runInLocale(res, locale);
+ }
+
+ public static boolean isNoLanguage(InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ return localeString.equals(NO_LANGUAGE);
+ }
+
+ public static Locale getSubtypeLocale(InputMethodSubtype subtype) {
+ final String localeString = subtype.getLocale();
+ return LocaleUtils.constructLocaleFromString(localeString);
+ }
+
+ public static String getKeyboardLayoutSetDisplayName(InputMethodSubtype subtype) {
+ final String layoutName = getKeyboardLayoutSetName(subtype);
+ return getKeyboardLayoutSetDisplayName(layoutName);
+ }
+
+ public static String getKeyboardLayoutSetDisplayName(String layoutName) {
+ return sKeyboardLayoutToDisplayNameMap.get(layoutName);
}
- public static String getFullDisplayName(Locale locale) {
- String localeCode = locale.toString();
- for (int index = 0; index < sExceptionKeys.length; index++) {
- if (sExceptionKeys[index].equals(localeCode))
- return sExceptionValues[index];
+ public static String getKeyboardLayoutSetName(InputMethodSubtype subtype) {
+ final String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
+ // TODO: Remove this null check when InputMethodManager.getCurrentInputMethodSubtype is
+ // fixed.
+ if (keyboardLayoutSet == null) {
+ android.util.Log.w(TAG, "KeyboardLayoutSet not found, use QWERTY: " +
+ "locale=" + subtype.getLocale() + " extraValue=" + subtype.getExtraValue());
+ return QWERTY;
}
- return locale.getDisplayName(locale);
+ return keyboardLayoutSet;
}
}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 6ca12c0c5..664de6774 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -16,29 +16,23 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
-import com.android.inputmethod.deprecated.VoiceProxy;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.keyboard.LatinKeyboard;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.IBinder;
-import android.text.TextUtils;
import android.util.Log;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -47,53 +41,49 @@ public class SubtypeSwitcher {
private static boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = SubtypeSwitcher.class.getSimpleName();
- private static final char LOCALE_SEPARATER = '_';
- private static final String KEYBOARD_MODE = "keyboard";
- private static final String VOICE_MODE = "voice";
- private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY =
- "requireNetworkConnectivity";
- public static final String USE_SPACEBAR_LANGUAGE_SWITCH_KEY = "use_spacebar_language_switch";
-
- private final TextUtils.SimpleStringSplitter mLocaleSplitter =
- new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER);
-
private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
private /* final */ LatinIME mService;
- private /* final */ InputMethodManagerCompatWrapper mImm;
+ private /* final */ InputMethodManager mImm;
private /* final */ Resources mResources;
private /* final */ ConnectivityManager mConnectivityManager;
- private /* final */ boolean mConfigUseSpacebarLanguageSwitcher;
- private /* final */ SharedPreferences mPrefs;
- private final ArrayList<InputMethodSubtypeCompatWrapper>
- mEnabledKeyboardSubtypesOfCurrentInputMethod =
- new ArrayList<InputMethodSubtypeCompatWrapper>();
- private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>();
- private final LanguageBarInfo mLanguageBarInfo = new LanguageBarInfo();
/*-----------------------------------------------------------*/
// Variants which should be changed only by reload functions.
- private boolean mNeedsToDisplayLanguage;
- private boolean mIsSystemLanguageSameAsInputLanguage;
- private InputMethodInfoCompatWrapper mShortcutInputMethodInfo;
- private InputMethodSubtypeCompatWrapper mShortcutSubtype;
- private List<InputMethodSubtypeCompatWrapper> mAllEnabledSubtypesOfCurrentInputMethod;
- private InputMethodSubtypeCompatWrapper mCurrentSubtype;
- private Locale mSystemLocale;
- private Locale mInputLocale;
- private String mInputLocaleStr;
- private String mInputMethodId;
- private VoiceProxy.VoiceInputWrapper mVoiceInputWrapper;
+ private NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
+ private InputMethodInfo mShortcutInputMethodInfo;
+ private InputMethodSubtype mShortcutSubtype;
+ private InputMethodSubtype mNoLanguageSubtype;
+ // Note: This variable is always non-null after {@link #initialize(LatinIME)}.
+ private InputMethodSubtype mCurrentSubtype;
+ private Locale mCurrentSystemLocale;
/*-----------------------------------------------------------*/
private boolean mIsNetworkConnected;
+ static class NeedsToDisplayLanguage {
+ private int mEnabledSubtypeCount;
+ private boolean mIsSystemLanguageSameAsInputLanguage;
+
+ public boolean getValue() {
+ return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage;
+ }
+
+ public void updateEnabledSubtypeCount(int count) {
+ mEnabledSubtypeCount = count;
+ }
+
+ public void updateIsSystemLanguageSameAsInputLanguage(boolean isSame) {
+ mIsSystemLanguageSameAsInputLanguage = isSame;
+ }
+ }
+
public static SubtypeSwitcher getInstance() {
return sInstance;
}
- public static void init(LatinIME service, SharedPreferences prefs) {
+ public static void init(LatinIME service) {
SubtypeLocale.init(service);
- sInstance.initialize(service, prefs);
+ sInstance.initialize(service);
sInstance.updateAllParameters();
}
@@ -101,31 +91,28 @@ public class SubtypeSwitcher {
// Intentional empty constructor for singleton.
}
- private void initialize(LatinIME service, SharedPreferences prefs) {
+ private void initialize(LatinIME service) {
mService = service;
mResources = service.getResources();
- mImm = InputMethodManagerCompatWrapper.getInstance(service);
+ mImm = ImfUtils.getInputMethodManager(service);
mConnectivityManager = (ConnectivityManager) service.getSystemService(
Context.CONNECTIVITY_SERVICE);
- mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
- mEnabledLanguagesOfCurrentInputMethod.clear();
- mSystemLocale = null;
- mInputLocale = null;
- mInputLocaleStr = null;
- mCurrentSubtype = null;
- mAllEnabledSubtypesOfCurrentInputMethod = null;
- mVoiceInputWrapper = null;
- mPrefs = prefs;
+ mCurrentSystemLocale = mResources.getConfiguration().locale;
+ mCurrentSubtype = mImm.getCurrentInputMethodSubtype();
+ mNoLanguageSubtype = ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
+ service, SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
+ if (mNoLanguageSubtype == null) {
+ throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
+ }
final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
mIsNetworkConnected = (info != null && info.isConnected());
- mInputMethodId = Utils.getInputMethodId(mImm, service.getPackageName());
}
// Update all parameters stored in SubtypeSwitcher.
// Only configuration changed event is allowed to call this because this is heavy.
private void updateAllParameters() {
- mSystemLocale = mResources.getConfiguration().locale;
+ mCurrentSystemLocale = mResources.getConfiguration().locale;
updateSubtype(mImm.getCurrentInputMethodSubtype());
updateParametersOnStartInputView();
}
@@ -133,47 +120,29 @@ public class SubtypeSwitcher {
// Update parameters which are changed outside LatinIME. This parameters affect UI so they
// should be updated every time onStartInputview.
public void updateParametersOnStartInputView() {
- mConfigUseSpacebarLanguageSwitcher = mPrefs.getBoolean(USE_SPACEBAR_LANGUAGE_SWITCH_KEY,
- mService.getResources().getBoolean(
- R.bool.config_use_spacebar_language_switcher));
updateEnabledSubtypes();
updateShortcutIME();
}
// Reload enabledSubtypes from the framework.
private void updateEnabledSubtypes() {
- final String currentMode = getCurrentSubtypeMode();
+ final InputMethodSubtype currentSubtype = mCurrentSubtype;
boolean foundCurrentSubtypeBecameDisabled = true;
- mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList(
- null, true);
- mEnabledLanguagesOfCurrentInputMethod.clear();
- mEnabledKeyboardSubtypesOfCurrentInputMethod.clear();
- for (InputMethodSubtypeCompatWrapper ims : mAllEnabledSubtypesOfCurrentInputMethod) {
- final String locale = ims.getLocale();
- final String mode = ims.getMode();
- mLocaleSplitter.setString(locale);
- if (mLocaleSplitter.hasNext()) {
- mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next());
- }
- if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) {
+ final List<InputMethodSubtype> enabledSubtypesOfThisIme =
+ mImm.getEnabledInputMethodSubtypeList(null, true);
+ for (InputMethodSubtype ims : enabledSubtypesOfThisIme) {
+ if (ims.equals(currentSubtype)) {
foundCurrentSubtypeBecameDisabled = false;
}
- if (KEYBOARD_MODE.equals(ims.getMode())) {
- mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims);
- }
}
- mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
- && mIsSystemLanguageSameAsInputLanguage);
+ mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
if (foundCurrentSubtypeBecameDisabled) {
if (DBG) {
- Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode);
+ Log.w(TAG, "Last subtype: "
+ + currentSubtype.getLocale() + "/" + currentSubtype.getExtraValue());
Log.w(TAG, "Last subtype was disabled. Update to the current one.");
}
updateSubtype(mImm.getCurrentInputMethodSubtype());
- } else {
- // mLanguageBarInfo.update() will be called in updateSubtype so there is no need
- // to call this in the if-clause above.
- mLanguageBarInfo.update();
}
}
@@ -182,14 +151,16 @@ public class SubtypeSwitcher {
Log.d(TAG, "Update shortcut IME from : "
+ (mShortcutInputMethodInfo == null
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
- + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
- + ", " + mShortcutSubtype.getMode())));
+ + (mShortcutSubtype == null ? "<null>" : (
+ mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
}
// TODO: Update an icon for shortcut IME
- final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts =
+ final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
mImm.getShortcutInputMethodsAndSubtypes();
- for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) {
- List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi);
+ mShortcutInputMethodInfo = null;
+ mShortcutSubtype = null;
+ for (InputMethodInfo imi : shortcuts.keySet()) {
+ List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
@@ -202,97 +173,28 @@ public class SubtypeSwitcher {
Log.d(TAG, "Update shortcut IME to : "
+ (mShortcutInputMethodInfo == null
? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
- + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale()
- + ", " + mShortcutSubtype.getMode())));
+ + (mShortcutSubtype == null ? "<null>" : (
+ mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
}
}
// Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
- public void updateSubtype(InputMethodSubtypeCompatWrapper newSubtype) {
- final String newLocale;
- final String newMode;
- final String oldMode = getCurrentSubtypeMode();
- if (newSubtype == null) {
- // Normally, newSubtype shouldn't be null. But just in case newSubtype was null,
- // fallback to the default locale.
- Log.w(TAG, "Couldn't get the current subtype.");
- newLocale = "en_US";
- newMode = KEYBOARD_MODE;
- } else {
- newLocale = newSubtype.getLocale();
- newMode = newSubtype.getMode();
- }
+ public void updateSubtype(InputMethodSubtype newSubtype) {
if (DBG) {
- Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode
- + ", from: " + mInputLocaleStr + ", " + oldMode);
- }
- boolean languageChanged = false;
- if (!newLocale.equals(mInputLocaleStr)) {
- if (mInputLocaleStr != null) {
- languageChanged = true;
- }
- updateInputLocale(newLocale);
- }
- boolean modeChanged = false;
- if (!newMode.equals(oldMode)) {
- if (oldMode != null) {
- modeChanged = true;
- }
+ Log.w(TAG, "onCurrentInputMethodSubtypeChanged: to: "
+ + newSubtype.getLocale() + "/" + newSubtype.getExtraValue() + ", from: "
+ + mCurrentSubtype.getLocale() + "/" + mCurrentSubtype.getExtraValue());
}
- mCurrentSubtype = newSubtype;
- // If the old mode is voice input, we need to reset or cancel its status.
- // We cancel its status when we change mode, while we reset otherwise.
- if (isKeyboardMode()) {
- if (modeChanged) {
- if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) {
- mVoiceInputWrapper.cancel();
- }
- }
- if (modeChanged || languageChanged) {
- updateShortcutIME();
- mService.onRefreshKeyboard();
- }
- } else if (isVoiceMode() && mVoiceInputWrapper != null) {
- if (VOICE_MODE.equals(oldMode)) {
- mVoiceInputWrapper.reset();
- }
- // If needsToShowWarningDialog is true, voice input need to show warning before
- // show recognition view.
- if (languageChanged || modeChanged
- || VoiceProxy.getInstance().needsToShowWarningDialog()) {
- triggerVoiceIME();
- }
- } else {
- Log.w(TAG, "Unknown subtype mode: " + newMode);
- if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) {
- // We need to reset the voice input to release the resources and to reset its status
- // as it is not the current input mode.
- mVoiceInputWrapper.reset();
- }
- }
- mLanguageBarInfo.update();
- }
+ final Locale newLocale = SubtypeLocale.getSubtypeLocale(newSubtype);
+ mNeedsToDisplayLanguage.updateIsSystemLanguageSameAsInputLanguage(
+ mCurrentSystemLocale.equals(newLocale));
- // Update the current input locale from Locale string.
- private void updateInputLocale(String inputLocaleStr) {
- // example: inputLocaleStr = "en_US" "en" ""
- // "en_US" --> language: en & country: US
- // "en" --> language: en
- // "" --> the system locale
- if (!TextUtils.isEmpty(inputLocaleStr)) {
- mInputLocale = Utils.constructLocaleFromString(inputLocaleStr);
- mInputLocaleStr = inputLocaleStr;
- } else {
- mInputLocale = mSystemLocale;
- String country = mSystemLocale.getCountry();
- mInputLocaleStr = mSystemLocale.getLanguage()
- + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage());
- }
- mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase(
- getInputLocale().getLanguage());
- mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1
- && mIsSystemLanguageSameAsInputLanguage);
+ if (newSubtype.equals(mCurrentSubtype)) return;
+
+ mCurrentSubtype = newSubtype;
+ updateShortcutIME();
+ mService.onRefreshKeyboard();
}
////////////////////////////
@@ -305,87 +207,34 @@ public class SubtypeSwitcher {
}
final String imiId = mShortcutInputMethodInfo.getId();
- final InputMethodSubtypeCompatWrapper subtype = mShortcutSubtype;
- switchToTargetIME(imiId, subtype);
+ switchToTargetIME(imiId, mShortcutSubtype);
}
- private void switchToTargetIME(
- final String imiId, final InputMethodSubtypeCompatWrapper subtype) {
+ private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype) {
final IBinder token = mService.getWindow().getWindow().getAttributes().token;
if (token == null) {
return;
}
+ final InputMethodManager imm = mImm;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
- mImm.setInputMethodAndSubtype(token, imiId, subtype);
+ imm.setInputMethodAndSubtype(token, imiId, subtype);
return null;
}
-
- @Override
- protected void onPostExecute(Void result) {
- // Calls in this method need to be done in the same thread as the thread which
- // called switchToShortcutIME().
-
- // Notify an event that the current subtype was changed. This event will be
- // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented
- // when the API level is 10 or previous.
- mService.notifyOnCurrentInputMethodSubtypeChanged(subtype);
- }
- }.execute();
- }
-
- public Drawable getShortcutIcon() {
- return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype);
- }
-
- private Drawable getSubtypeIcon(
- InputMethodInfoCompatWrapper imi, InputMethodSubtypeCompatWrapper subtype) {
- final PackageManager pm = mService.getPackageManager();
- if (imi != null) {
- final String imiPackageName = imi.getPackageName();
- if (DBG) {
- Log.d(TAG, "Update icons of IME: " + imiPackageName + ","
- + subtype.getLocale() + "," + subtype.getMode());
- }
- if (subtype != null) {
- return pm.getDrawable(imiPackageName, subtype.getIconResId(),
- imi.getServiceInfo().applicationInfo);
- } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) {
- return pm.getDrawable(imiPackageName,
- imi.getSubtypeAt(0).getIconResId(),
- imi.getServiceInfo().applicationInfo);
- } else {
- try {
- return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm);
- } catch (PackageManager.NameNotFoundException e) {
- Log.w(TAG, "IME can't be found: " + imiPackageName);
- }
- }
- }
- return null;
- }
-
- private static boolean contains(String[] hay, String needle) {
- for (String element : hay) {
- if (element.equals(needle))
- return true;
- }
- return false;
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public boolean isShortcutImeEnabled() {
- if (mShortcutInputMethodInfo == null)
+ if (mShortcutInputMethodInfo == null) {
return false;
- if (mShortcutSubtype == null)
+ }
+ if (mShortcutSubtype == null) {
return true;
- // For compatibility, if the shortcut subtype is dummy, we assume the shortcut IME
- // (built-in voice dummy subtype) is available.
- if (!mShortcutSubtype.hasOriginalObject()) return true;
+ }
final boolean allowsImplicitlySelectedSubtypes = true;
- for (final InputMethodSubtypeCompatWrapper enabledSubtype :
- mImm.getEnabledInputMethodSubtypeList(
- mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) {
+ for (final InputMethodSubtype enabledSubtype : mImm.getEnabledInputMethodSubtypeList(
+ mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) {
if (enabledSubtype.equals(mShortcutSubtype)) {
return true;
}
@@ -398,8 +247,7 @@ public class SubtypeSwitcher {
return false;
if (mShortcutSubtype == null)
return true;
- if (contains(mShortcutSubtype.getExtraValue().split(","),
- SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) {
+ if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
return mIsNetworkConnected;
}
return true;
@@ -410,265 +258,40 @@ public class SubtypeSwitcher {
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
mIsNetworkConnected = !noConnection;
- final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance();
- final LatinKeyboard keyboard = switcher.getLatinKeyboard();
- if (keyboard != null) {
- keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView());
- }
+ KeyboardSwitcher.getInstance().onNetworkStateChanged();
}
//////////////////////////////////
- // Language Switching functions //
+ // Subtype Switching functions //
//////////////////////////////////
- public int getEnabledKeyboardLocaleCount() {
- return mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
- }
-
- public boolean useSpacebarLanguageSwitcher() {
- return mConfigUseSpacebarLanguageSwitcher;
- }
-
- public boolean needsToDisplayLanguage() {
- return mNeedsToDisplayLanguage;
- }
-
- public Locale getInputLocale() {
- return mInputLocale;
- }
-
- public String getInputLocaleStr() {
- return mInputLocaleStr;
- }
-
- public String[] getEnabledLanguages() {
- int enabledLanguageCount = mEnabledLanguagesOfCurrentInputMethod.size();
- // Workaround for explicitly specifying the voice language
- if (enabledLanguageCount == 1) {
- mEnabledLanguagesOfCurrentInputMethod.add(mEnabledLanguagesOfCurrentInputMethod
- .get(0));
- ++enabledLanguageCount;
+ public boolean needsToDisplayLanguage(Locale keyboardLocale) {
+ if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) {
+ return true;
}
- return mEnabledLanguagesOfCurrentInputMethod.toArray(new String[enabledLanguageCount]);
- }
-
- public Locale getSystemLocale() {
- return mSystemLocale;
+ if (!keyboardLocale.equals(getCurrentSubtypeLocale())) {
+ return false;
+ }
+ return mNeedsToDisplayLanguage.getValue();
}
- public boolean isSystemLanguageSameAsInputLanguage() {
- return mIsSystemLanguageSameAsInputLanguage;
+ public Locale getCurrentSubtypeLocale() {
+ return SubtypeLocale.getSubtypeLocale(mCurrentSubtype);
}
public void onConfigurationChanged(Configuration conf) {
final Locale systemLocale = conf.locale;
// If system configuration was changed, update all parameters.
- if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) {
+ if (!systemLocale.equals(mCurrentSystemLocale)) {
updateAllParameters();
}
}
- public boolean isKeyboardMode() {
- return KEYBOARD_MODE.equals(getCurrentSubtypeMode());
+ public InputMethodSubtype getCurrentSubtype() {
+ return mCurrentSubtype;
}
-
- ///////////////////////////
- // Voice Input functions //
- ///////////////////////////
-
- public boolean setVoiceInputWrapper(VoiceProxy.VoiceInputWrapper vi) {
- if (mVoiceInputWrapper == null && vi != null) {
- mVoiceInputWrapper = vi;
- if (isVoiceMode()) {
- if (DBG) {
- Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr());
- }
- triggerVoiceIME();
- return true;
- }
- }
- return false;
- }
-
- public boolean isVoiceMode() {
- return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode());
- }
-
- public boolean isDummyVoiceMode() {
- return mCurrentSubtype != null && mCurrentSubtype.getOriginalObject() == null
- && VOICE_MODE.equals(getCurrentSubtypeMode());
- }
-
- private void triggerVoiceIME() {
- if (!mService.isInputViewShown()) return;
- VoiceProxy.getInstance().startListening(false,
- KeyboardSwitcher.getInstance().getKeyboardView().getWindowToken());
- }
-
- //////////////////////////////////////
- // Spacebar Language Switch support //
- //////////////////////////////////////
-
- private class LanguageBarInfo {
- private int mCurrentKeyboardSubtypeIndex;
- private InputMethodSubtypeCompatWrapper mNextKeyboardSubtype;
- private InputMethodSubtypeCompatWrapper mPreviousKeyboardSubtype;
- private String mNextLanguage;
- private String mPreviousLanguage;
- public LanguageBarInfo() {
- update();
- }
-
- private String getNextLanguage() {
- return mNextLanguage;
- }
-
- private String getPreviousLanguage() {
- return mPreviousLanguage;
- }
-
- public InputMethodSubtypeCompatWrapper getNextKeyboardSubtype() {
- return mNextKeyboardSubtype;
- }
-
- public InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtype() {
- return mPreviousKeyboardSubtype;
- }
-
- public void update() {
- if (!mConfigUseSpacebarLanguageSwitcher
- || mEnabledKeyboardSubtypesOfCurrentInputMethod == null
- || mEnabledKeyboardSubtypesOfCurrentInputMethod.size() == 0) return;
- mCurrentKeyboardSubtypeIndex = getCurrentIndex();
- mNextKeyboardSubtype = getNextKeyboardSubtypeInternal(mCurrentKeyboardSubtypeIndex);
- Locale locale = Utils.constructLocaleFromString(mNextKeyboardSubtype.getLocale());
- mNextLanguage = getFullDisplayName(locale, true);
- mPreviousKeyboardSubtype = getPreviousKeyboardSubtypeInternal(
- mCurrentKeyboardSubtypeIndex);
- locale = Utils.constructLocaleFromString(mPreviousKeyboardSubtype.getLocale());
- mPreviousLanguage = getFullDisplayName(locale, true);
- }
-
- private int normalize(int index) {
- final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
- final int ret = index % N;
- return ret < 0 ? ret + N : ret;
- }
-
- private int getCurrentIndex() {
- final int N = mEnabledKeyboardSubtypesOfCurrentInputMethod.size();
- for (int i = 0; i < N; ++i) {
- if (mEnabledKeyboardSubtypesOfCurrentInputMethod.get(i).equals(mCurrentSubtype)) {
- return i;
- }
- }
- return 0;
- }
-
- private InputMethodSubtypeCompatWrapper getNextKeyboardSubtypeInternal(int index) {
- return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index + 1));
- }
-
- private InputMethodSubtypeCompatWrapper getPreviousKeyboardSubtypeInternal(int index) {
- return mEnabledKeyboardSubtypesOfCurrentInputMethod.get(normalize(index - 1));
- }
- }
-
- public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) {
- if (returnsNameInThisLocale) {
- return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
- } else {
- return toTitleCase(locale.getDisplayName(), locale);
- }
- }
-
- public static String getDisplayLanguage(Locale locale) {
- return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
- }
-
- public static String getMiddleDisplayLanguage(Locale locale) {
- return toTitleCase((Utils.constructLocaleFromString(
- locale.getLanguage()).getDisplayLanguage(locale)), locale);
- }
-
- public static String getShortDisplayLanguage(Locale locale) {
- return toTitleCase(locale.getLanguage(), locale);
- }
-
- private static String toTitleCase(String s, Locale locale) {
- if (s.length() == 0) {
- return s;
- }
- return s.toUpperCase(locale).charAt(0) + s.substring(1);
- }
-
- public String getInputLanguageName() {
- return getDisplayLanguage(getInputLocale());
- }
-
- public String getNextInputLanguageName() {
- return mLanguageBarInfo.getNextLanguage();
- }
-
- public String getPreviousInputLanguageName() {
- return mLanguageBarInfo.getPreviousLanguage();
- }
-
- /////////////////////////////
- // Other utility functions //
- /////////////////////////////
-
- public String getCurrentSubtypeExtraValue() {
- // If null, return what an empty ExtraValue would return : the empty string.
- return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : "";
- }
-
- public boolean currentSubtypeContainsExtraValueKey(String key) {
- // If null, return what an empty ExtraValue would return : false.
- return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false;
- }
-
- public String getCurrentSubtypeExtraValueOf(String key) {
- // If null, return what an empty ExtraValue would return : null.
- return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null;
- }
-
- public String getCurrentSubtypeMode() {
- return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE;
- }
-
-
- public boolean isVoiceSupported(String locale) {
- // Get the current list of supported locales and check the current locale against that
- // list. We cache this value so as not to check it every time the user starts a voice
- // input. Because this method is called by onStartInputView, this should mean that as
- // long as the locale doesn't change while the user is keeping the IME open, the
- // value should never be stale.
- String supportedLocalesString = VoiceProxy.getSupportedLocalesString(
- mService.getContentResolver());
- List<String> voiceInputSupportedLocales = Arrays.asList(
- supportedLocalesString.split("\\s+"));
- return voiceInputSupportedLocales.contains(locale);
- }
-
- private void changeToNextSubtype() {
- final InputMethodSubtypeCompatWrapper subtype =
- mLanguageBarInfo.getNextKeyboardSubtype();
- switchToTargetIME(mInputMethodId, subtype);
- }
-
- private void changeToPreviousSubtype() {
- final InputMethodSubtypeCompatWrapper subtype =
- mLanguageBarInfo.getPreviousKeyboardSubtype();
- switchToTargetIME(mInputMethodId, subtype);
- }
-
- public void toggleLanguage(boolean next) {
- if (next) {
- changeToNextSubtype();
- } else {
- changeToPreviousSubtype();
- }
+ public InputMethodSubtype getNoLanguageSubtype() {
+ return mNoLanguageSubtype;
}
}
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index eb5ed5a65..892245402 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -17,117 +17,110 @@
package com.android.inputmethod.latin;
import android.content.Context;
-import android.text.AutoText;
import android.text.TextUtils;
import android.util.Log;
-import android.view.View;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.io.File;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* This class loads a dictionary and provides a list of suggestions for a given sequence of
* characters. This includes corrections and completions.
*/
public class Suggest implements Dictionary.WordCallback {
-
public static final String TAG = Suggest.class.getSimpleName();
public static final int APPROX_MAX_WORD_LENGTH = 32;
+ // TODO: rename this to CORRECTION_OFF
public static final int CORRECTION_NONE = 0;
- public static final int CORRECTION_BASIC = 1;
- public static final int CORRECTION_FULL = 2;
- public static final int CORRECTION_FULL_BIGRAM = 3;
-
- /**
- * Words that appear in both bigram and unigram data gets multiplier ranging from
- * BIGRAM_MULTIPLIER_MIN to BIGRAM_MULTIPLIER_MAX depending on the score from
- * bigram data.
- */
- public static final double BIGRAM_MULTIPLIER_MIN = 1.2;
- public static final double BIGRAM_MULTIPLIER_MAX = 1.5;
-
- /**
- * Maximum possible bigram frequency. Will depend on how many bits are being used in data
- * structure. Maximum bigram frequency will get the BIGRAM_MULTIPLIER_MAX as the multiplier.
- */
- public static final int MAXIMUM_BIGRAM_FREQUENCY = 127;
+ // TODO: rename this to CORRECTION_ON
+ public static final int CORRECTION_FULL = 1;
+ // It seems the following values are only used for logging.
public static final int DIC_USER_TYPED = 0;
public static final int DIC_MAIN = 1;
public static final int DIC_USER = 2;
- public static final int DIC_AUTO = 3;
+ public static final int DIC_USER_HISTORY = 3;
public static final int DIC_CONTACTS = 4;
+ public static final int DIC_WHITELIST = 6;
// If you add a type of dictionary, increment DIC_TYPE_LAST_ID
- public static final int DIC_TYPE_LAST_ID = 4;
-
+ // TODO: this value seems unused. Remove it?
+ public static final int DIC_TYPE_LAST_ID = 6;
public static final String DICT_KEY_MAIN = "main";
public static final String DICT_KEY_CONTACTS = "contacts";
- public static final String DICT_KEY_AUTO = "auto";
+ // User dictionary, the system-managed one.
public static final String DICT_KEY_USER = "user";
- public static final String DICT_KEY_USER_BIGRAM = "user_bigram";
+ // User history dictionary for the unigram map, internal to LatinIME
+ public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram";
+ // User history dictionary for the bigram map, internal to LatinIME
+ public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram";
public static final String DICT_KEY_WHITELIST ="whitelist";
private static final boolean DBG = LatinImeLogger.sDBG;
- private AutoCorrection mAutoCorrection;
-
- private Dictionary mMainDict;
+ private Dictionary mMainDictionary;
+ private ContactsBinaryDictionary mContactsDict;
private WhitelistDictionary mWhiteListDictionary;
- private final Map<String, Dictionary> mUnigramDictionaries = new HashMap<String, Dictionary>();
- private final Map<String, Dictionary> mBigramDictionaries = new HashMap<String, Dictionary>();
+ private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
+ new ConcurrentHashMap<String, Dictionary>();
+ private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries =
+ new ConcurrentHashMap<String, Dictionary>();
- private int mPrefMaxSuggestions = 18;
+ public static final int MAX_SUGGESTIONS = 18;
private static final int PREF_MAX_BIGRAMS = 60;
- private boolean mQuickFixesEnabled;
-
- private double mAutoCorrectionThreshold;
- private int[] mScores = new int[mPrefMaxSuggestions];
- private int[] mBigramScores = new int[PREF_MAX_BIGRAMS];
+ private float mAutoCorrectionThreshold;
- private ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
- ArrayList<CharSequence> mBigramSuggestions = new ArrayList<CharSequence>();
- private ArrayList<CharSequence> mStringPool = new ArrayList<CharSequence>();
- private CharSequence mTypedWord;
+ private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>();
+ private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>();
+ private CharSequence mConsideredWord;
// TODO: Remove these member variables by passing more context to addWord() callback method
private boolean mIsFirstCharCapitalized;
private boolean mIsAllUpperCase;
+ private int mTrailingSingleQuotesCount;
- private int mCorrectionMode = CORRECTION_BASIC;
+ private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
- public Suggest(Context context, int dictionaryResId, Locale locale) {
- init(context, DictionaryFactory.createDictionaryFromManager(context, locale,
- dictionaryResId));
+ public Suggest(final Context context, final Locale locale) {
+ initAsynchronously(context, locale);
}
- /* package for test */ Suggest(Context context, File dictionary, long startOffset, long length,
- Flag[] flagArray) {
- init(null, DictionaryFactory.createDictionaryForTest(context, dictionary, startOffset,
- length, flagArray));
- }
-
- private void init(Context context, Dictionary mainDict) {
- mMainDict = mainDict;
+ /* package for test */ Suggest(final Context context, final File dictionary,
+ final long startOffset, final long length, final Locale locale) {
+ final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
+ startOffset, length /* useFullEditDistance */, false, locale);
+ mMainDictionary = mainDict;
addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
- mWhiteListDictionary = WhitelistDictionary.init(context);
+ initWhitelistAndAutocorrectAndPool(context, locale);
+ }
+
+ private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
+ mWhiteListDictionary = new WhitelistDictionary(context, locale);
addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
- mAutoCorrection = new AutoCorrection();
- initPool();
}
- private void addOrReplaceDictionary(Map<String, Dictionary> dictionaries, String key,
- Dictionary dict) {
+ private void initAsynchronously(final Context context, final Locale locale) {
+ resetMainDict(context, locale);
+
+ // TODO: read the whitelist and init the pool asynchronously too.
+ // initPool should be done asynchronously now that the pool is thread-safe.
+ initWhitelistAndAutocorrectAndPool(context, locale);
+ }
+
+ private static void addOrReplaceDictionary(
+ final ConcurrentHashMap<String, Dictionary> dictionaries,
+ final String key, final Dictionary dict) {
final Dictionary oldDict = (dict == null)
? dictionaries.remove(key)
: dictionaries.put(key, dict);
@@ -136,50 +129,47 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- public void resetMainDict(Context context, int dictionaryResId, Locale locale) {
- final Dictionary newMainDict = DictionaryFactory.createDictionaryFromManager(
- context, locale, dictionaryResId);
- mMainDict = newMainDict;
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
- }
-
- private void initPool() {
- for (int i = 0; i < mPrefMaxSuggestions; i++) {
- StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- mStringPool.add(sb);
- }
- }
-
- public void setQuickFixesEnabled(boolean enabled) {
- mQuickFixesEnabled = enabled;
+ public void resetMainDict(final Context context, final Locale locale) {
+ mMainDictionary = null;
+ new Thread("InitializeBinaryDictionary") {
+ @Override
+ public void run() {
+ final DictionaryCollection newMainDict =
+ DictionaryFactory.createMainDictionaryFromManager(context, locale);
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ mMainDictionary = newMainDict;
+ }
+ }.start();
}
- public int getCorrectionMode() {
- return mCorrectionMode;
+ // The main dictionary could have been loaded asynchronously. Don't cache the return value
+ // of this method.
+ public boolean hasMainDictionary() {
+ return null != mMainDictionary && mMainDictionary.isInitialized();
}
- public void setCorrectionMode(int mode) {
- mCorrectionMode = mode;
+ public Dictionary getMainDictionary() {
+ return mMainDictionary;
}
- public boolean hasMainDictionary() {
- return mMainDict != null;
+ public ContactsBinaryDictionary getContactsDictionary() {
+ return mContactsDict;
}
- public Map<String, Dictionary> getUnigramDictionaries() {
+ public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
return mUnigramDictionaries;
}
- public int getApproxMaxWordLength() {
+ public static int getApproxMaxWordLength() {
return APPROX_MAX_WORD_LENGTH;
}
/**
* Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
- * before the main dictionary, if set.
+ * before the main dictionary, if set. This refers to the system-managed user dictionary.
*/
- public void setUserDictionary(Dictionary userDictionary) {
+ public void setUserDictionary(UserBinaryDictionary userDictionary) {
addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
}
@@ -188,68 +178,28 @@ public class Suggest implements Dictionary.WordCallback {
* the contacts dictionary by passing null to this method. In this case no contacts dictionary
* won't be used.
*/
- public void setContactsDictionary(Dictionary contactsDictionary) {
+ public void setContactsDictionary(ContactsBinaryDictionary contactsDictionary) {
+ mContactsDict = contactsDictionary;
addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
}
- public void setAutoDictionary(Dictionary autoDictionary) {
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_AUTO, autoDictionary);
- }
-
- public void setUserBigramDictionary(Dictionary userBigramDictionary) {
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_BIGRAM, userBigramDictionary);
+ public void setUserHistoryDictionary(UserHistoryDictionary userHistoryDictionary) {
+ addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
+ userHistoryDictionary);
+ addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
+ userHistoryDictionary);
}
- public void setAutoCorrectionThreshold(double threshold) {
+ public void setAutoCorrectionThreshold(float threshold) {
mAutoCorrectionThreshold = threshold;
}
- public boolean isAggressiveAutoCorrectionMode() {
- return (mAutoCorrectionThreshold == 0);
- }
-
- /**
- * Number of suggestions to generate from the input key sequence. This has
- * to be a number between 1 and 100 (inclusive).
- * @param maxSuggestions
- * @throws IllegalArgumentException if the number is out of range
- */
- public void setMaxSuggestions(int maxSuggestions) {
- if (maxSuggestions < 1 || maxSuggestions > 100) {
- throw new IllegalArgumentException("maxSuggestions must be between 1 and 100");
- }
- mPrefMaxSuggestions = maxSuggestions;
- mScores = new int[mPrefMaxSuggestions];
- mBigramScores = new int[PREF_MAX_BIGRAMS];
- collectGarbage(mSuggestions, mPrefMaxSuggestions);
- while (mStringPool.size() < mPrefMaxSuggestions) {
- StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- mStringPool.add(sb);
- }
- }
-
- /**
- * Returns a object which represents suggested words that match the list of character codes
- * passed in. This object contents will be overwritten the next time this function is called.
- * @param view a view for retrieving the context for AutoText
- * @param wordComposer contains what is currently being typed
- * @param prevWordForBigram previous word (used only for bigram)
- * @return suggested words object.
- */
- public SuggestedWords getSuggestions(View view, WordComposer wordComposer,
- CharSequence prevWordForBigram) {
- return getSuggestedWordBuilder(view, wordComposer, prevWordForBigram).build();
- }
-
- private CharSequence capitalizeWord(boolean all, boolean first, CharSequence word) {
+ private static CharSequence capitalizeWord(final boolean all, final boolean first,
+ final CharSequence word) {
if (TextUtils.isEmpty(word) || !(all || first)) return word;
final int wordLength = word.length();
- final int poolSize = mStringPool.size();
- final StringBuilder sb =
- poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
- : new StringBuilder(getApproxMaxWordLength());
- sb.setLength(0);
+ final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
// TODO: Must pay attention to locale when changing case.
if (all) {
sb.append(word.toString().toUpperCase());
@@ -262,249 +212,271 @@ public class Suggest implements Dictionary.WordCallback {
return sb;
}
- protected void addBigramToSuggestions(CharSequence bigram) {
- final int poolSize = mStringPool.size();
- final StringBuilder sb = poolSize > 0 ?
- (StringBuilder) mStringPool.remove(poolSize - 1)
- : new StringBuilder(getApproxMaxWordLength());
- sb.setLength(0);
- sb.append(bigram);
- mSuggestions.add(sb);
+ protected void addBigramToSuggestions(SuggestedWordInfo bigram) {
+ mSuggestions.add(bigram);
+ }
+
+ private static final WordComposer sEmptyWordComposer = new WordComposer();
+ public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) {
+ LatinImeLogger.onStartSuggestion(prevWordForBigram);
+ mIsFirstCharCapitalized = false;
+ mIsAllUpperCase = false;
+ mTrailingSingleQuotesCount = 0;
+ mSuggestions = new ArrayList<SuggestedWordInfo>(MAX_SUGGESTIONS);
+
+ // Treating USER_TYPED as UNIGRAM suggestion for logging now.
+ LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
+ mConsideredWord = "";
+
+ mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
+
+ getAllBigrams(prevWordForBigram, sEmptyWordComposer);
+
+ // Nothing entered: return all bigrams for the previous word
+ int insertCount = Math.min(mBigramSuggestions.size(), MAX_SUGGESTIONS);
+ for (int i = 0; i < insertCount; ++i) {
+ addBigramToSuggestions(mBigramSuggestions.get(i));
+ }
+
+ SuggestedWordInfo.removeDups(mSuggestions);
+
+ return new SuggestedWords(mSuggestions,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* allowsToBeAutoCorrected */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ true /* isPrediction */);
}
// TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
- public SuggestedWords.Builder getSuggestedWordBuilder(View view, WordComposer wordComposer,
- CharSequence prevWordForBigram) {
+ public SuggestedWords getSuggestedWords(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mAutoCorrection.init();
mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
mIsAllUpperCase = wordComposer.isAllUpperCase();
- collectGarbage(mSuggestions, mPrefMaxSuggestions);
- Arrays.fill(mScores, 0);
-
- // Save a lowercase version of the original word
- CharSequence typedWord = wordComposer.getTypedWord();
- if (typedWord != null) {
- final String typedWordString = typedWord.toString();
- typedWord = typedWordString;
- // Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord(typedWordString, Suggest.DIC_USER_TYPED,
- Dictionary.DataType.UNIGRAM);
- }
- mTypedWord = typedWord;
-
- if (wordComposer.size() <= 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM
- || mCorrectionMode == CORRECTION_BASIC)) {
+ mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
+ mSuggestions = new ArrayList<SuggestedWordInfo>(MAX_SUGGESTIONS);
+
+ final String typedWord = wordComposer.getTypedWord();
+ final String consideredWord = mTrailingSingleQuotesCount > 0
+ ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount)
+ : typedWord;
+ // Treating USER_TYPED as UNIGRAM suggestion for logging now.
+ LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
+ mConsideredWord = consideredWord;
+
+ if (wordComposer.size() <= 1 && isCorrectionEnabled) {
// At first character typed, search only the bigrams
- Arrays.fill(mBigramScores, 0);
- collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS);
+ mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
if (!TextUtils.isEmpty(prevWordForBigram)) {
- CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase();
- if (mMainDict != null && mMainDict.isValidWord(lowerPrevWord)) {
- prevWordForBigram = lowerPrevWord;
- }
- for (final Dictionary dictionary : mBigramDictionaries.values()) {
- dictionary.getBigrams(wordComposer, prevWordForBigram, this);
- }
- if (TextUtils.isEmpty(typedWord)) {
+ getAllBigrams(prevWordForBigram, wordComposer);
+ if (TextUtils.isEmpty(consideredWord)) {
// Nothing entered: return all bigrams for the previous word
- int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
+ int insertCount = Math.min(mBigramSuggestions.size(), MAX_SUGGESTIONS);
for (int i = 0; i < insertCount; ++i) {
addBigramToSuggestions(mBigramSuggestions.get(i));
}
} else {
// Word entered: return only bigrams that match the first char of the typed word
- final char currentChar = typedWord.charAt(0);
+ final char currentChar = consideredWord.charAt(0);
// TODO: Must pay attention to locale when changing case.
+ // TODO: Use codepoint instead of char
final char currentCharUpper = Character.toUpperCase(currentChar);
int count = 0;
final int bigramSuggestionSize = mBigramSuggestions.size();
for (int i = 0; i < bigramSuggestionSize; i++) {
- final CharSequence bigramSuggestion = mBigramSuggestions.get(i);
- final char bigramSuggestionFirstChar = bigramSuggestion.charAt(0);
+ final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i);
+ final char bigramSuggestionFirstChar =
+ (char)bigramSuggestion.codePointAt(0);
if (bigramSuggestionFirstChar == currentChar
|| bigramSuggestionFirstChar == currentCharUpper) {
addBigramToSuggestions(bigramSuggestion);
- if (++count > mPrefMaxSuggestions) break;
+ if (++count > MAX_SUGGESTIONS) break;
}
}
}
}
} else if (wordComposer.size() > 1) {
+ final WordComposer wordComposerForLookup;
+ if (mTrailingSingleQuotesCount > 0) {
+ wordComposerForLookup = new WordComposer(wordComposer);
+ for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+ wordComposerForLookup.deleteLast();
+ }
+ } else {
+ wordComposerForLookup = wordComposer;
+ }
// At second character typed, search the unigrams (scores being affected by bigrams)
for (final String key : mUnigramDictionaries.keySet()) {
- // Skip AutoDictionary and WhitelistDictionary to lookup
- if (key.equals(DICT_KEY_AUTO) || key.equals(DICT_KEY_WHITELIST))
+ // Skip UserUnigramDictionary and WhitelistDictionary to lookup
+ if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
continue;
final Dictionary dictionary = mUnigramDictionaries.get(key);
- dictionary.getWords(wordComposer, this);
- }
- }
- CharSequence autoText = null;
- final String typedWordString = typedWord == null ? null : typedWord.toString();
- if (typedWord != null) {
- // Apply quick fix only for the typed word.
- if (mQuickFixesEnabled) {
- final String lowerCaseTypedWord = typedWordString.toLowerCase();
- CharSequence tempAutoText = capitalizeWord(
- mIsAllUpperCase, mIsFirstCharCapitalized, AutoText.get(
- lowerCaseTypedWord, 0, lowerCaseTypedWord.length(), view));
- // TODO: cleanup canAdd
- // Is there an AutoText (also known as Quick Fixes) correction?
- // Capitalize as needed
- boolean canAdd = tempAutoText != null;
- // Is that correction already the current prediction (or original word)?
- canAdd &= !TextUtils.equals(tempAutoText, typedWord);
- // Is that correction already the next predicted word?
- if (canAdd && mSuggestions.size() > 0 && mCorrectionMode != CORRECTION_BASIC) {
- canAdd &= !TextUtils.equals(tempAutoText, mSuggestions.get(0));
- }
- if (canAdd) {
- if (DBG) {
- Log.d(TAG, "Auto corrected by AUTOTEXT.");
- }
- autoText = tempAutoText;
- }
+ dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo);
}
}
- CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, mIsFirstCharCapitalized,
- mWhiteListDictionary.getWhiteListedWord(typedWordString));
+ final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase,
+ mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
- mAutoCorrection.updateAutoCorrectionStatus(mUnigramDictionaries, wordComposer,
- mSuggestions, mScores, typedWord, mAutoCorrectionThreshold, mCorrectionMode,
- autoText, whitelistedWord);
-
- if (autoText != null) {
- mSuggestions.add(0, autoText);
+ final boolean hasAutoCorrection;
+ if (isCorrectionEnabled) {
+ final CharSequence autoCorrection =
+ AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
+ mSuggestions, consideredWord, mAutoCorrectionThreshold,
+ whitelistedWord);
+ hasAutoCorrection = (null != autoCorrection);
+ } else {
+ hasAutoCorrection = false;
}
if (whitelistedWord != null) {
- mSuggestions.add(0, whitelistedWord);
+ if (mTrailingSingleQuotesCount > 0) {
+ final StringBuilder sb = new StringBuilder(whitelistedWord);
+ for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+ sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
+ }
+ mSuggestions.add(0, new SuggestedWordInfo(sb.toString(),
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST));
+ } else {
+ mSuggestions.add(0, new SuggestedWordInfo(whitelistedWord,
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_WHITELIST));
+ }
}
- if (typedWord != null) {
- mSuggestions.add(0, typedWordString);
- }
- removeDupes();
+ mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_TYPED));
+ SuggestedWordInfo.removeDups(mSuggestions);
+ final ArrayList<SuggestedWordInfo> suggestionsList;
if (DBG) {
- double normalizedScore = mAutoCorrection.getNormalizedScore();
- ArrayList<SuggestedWords.SuggestedWordInfo> scoreInfoList =
- new ArrayList<SuggestedWords.SuggestedWordInfo>();
- scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("+", false));
- for (int i = 0; i < mScores.length; ++i) {
- if (normalizedScore > 0) {
- final String scoreThreshold = String.format("%d (%4.2f)", mScores[i],
- normalizedScore);
- scoreInfoList.add(
- new SuggestedWords.SuggestedWordInfo(scoreThreshold, false));
- normalizedScore = 0.0;
- } else {
- final String score = Integer.toString(mScores[i]);
- scoreInfoList.add(new SuggestedWords.SuggestedWordInfo(score, false));
- }
- }
- for (int i = mScores.length; i < mSuggestions.size(); ++i) {
- scoreInfoList.add(new SuggestedWords.SuggestedWordInfo("--", false));
- }
- return new SuggestedWords.Builder().addWords(mSuggestions, scoreInfoList);
+ suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions);
+ } else {
+ suggestionsList = mSuggestions;
}
- return new SuggestedWords.Builder().addWords(mSuggestions, null);
- }
- private void removeDupes() {
- final ArrayList<CharSequence> suggestions = mSuggestions;
- if (suggestions.size() < 2) return;
- int i = 1;
- // Don't cache suggestions.size(), since we may be removing items
- while (i < suggestions.size()) {
- final CharSequence cur = suggestions.get(i);
- // Compare each candidate with each previous candidate
- for (int j = 0; j < i; j++) {
- CharSequence previous = suggestions.get(j);
- if (TextUtils.equals(cur, previous)) {
- removeFromSuggestions(i);
- i--;
- break;
- }
- }
- i++;
+ // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
+ // but still autocorrected from - in the case the whitelist only capitalizes the word.
+ // The whitelist should be case-insensitive, so it's not possible to be consistent with
+ // a boolean flag. Right now this is handled with a slight hack in
+ // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
+ final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
+ getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized())
+ // If we don't have a main dictionary, we never want to auto-correct. The reason for this
+ // is, the user may have a contact whose name happens to match a valid word in their
+ // language, and it will unexpectedly auto-correct. For example, if the user types in
+ // English with no dictionary and has a "Will" in their contact list, "will" would
+ // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
+ && hasMainDictionary();
+
+ boolean autoCorrectionAvailable = hasAutoCorrection;
+ if (isCorrectionEnabled) {
+ autoCorrectionAvailable |= !allowsToBeAutoCorrected;
}
+ // Don't auto-correct words with multiple capital letter
+ autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
+ autoCorrectionAvailable &= !wordComposer.isResumed();
+ if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0
+ && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord,
+ suggestionsList.get(1).mWord)) {
+ autoCorrectionAvailable = false;
+ }
+ return new SuggestedWords(suggestionsList,
+ !allowsToBeAutoCorrected /* typedWordValid */,
+ autoCorrectionAvailable /* hasAutoCorrectionCandidate */,
+ allowsToBeAutoCorrected /* allowsToBeAutoCorrected */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
}
- private void removeFromSuggestions(int index) {
- CharSequence garbage = mSuggestions.remove(index);
- if (garbage != null && garbage instanceof StringBuilder) {
- mStringPool.add(garbage);
+ /**
+ * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if
+ * it contains any upper case characters.
+ */
+ private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) {
+ if (StringUtils.hasUpperCase(prevWord)) {
+ // TODO: Must pay attention to locale when changing case.
+ final CharSequence lowerPrevWord = prevWord.toString().toLowerCase();
+ for (final Dictionary dictionary : mBigramDictionaries.values()) {
+ dictionary.getBigrams(wordComposer, lowerPrevWord, this);
+ }
+ }
+ for (final Dictionary dictionary : mBigramDictionaries.values()) {
+ dictionary.getBigrams(wordComposer, prevWord, this);
}
}
- public boolean hasAutoCorrection() {
- return mAutoCorrection.hasAutoCorrection();
+ private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
+ final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
+ final SuggestedWordInfo typedWordInfo = suggestions.get(0);
+ typedWordInfo.setDebugString("+");
+ final int suggestionsSize = suggestions.size();
+ final ArrayList<SuggestedWordInfo> suggestionsList =
+ new ArrayList<SuggestedWordInfo>(suggestionsSize);
+ suggestionsList.add(typedWordInfo);
+ // Note: i here is the index in mScores[], but the index in mSuggestions is one more
+ // than i because we added the typed word to mSuggestions without touching mScores.
+ for (int i = 0; i < suggestionsSize - 1; ++i) {
+ final SuggestedWordInfo cur = suggestions.get(i + 1);
+ final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+ typedWord, cur.toString(), cur.mScore);
+ final String scoreInfoString;
+ if (normalizedScore > 0) {
+ scoreInfoString = String.format("%d (%4.2f)", cur.mScore, normalizedScore);
+ } else {
+ scoreInfoString = Integer.toString(cur.mScore);
+ }
+ cur.setDebugString(scoreInfoString);
+ suggestionsList.add(cur);
+ }
+ return suggestionsList;
}
+ // TODO: Use codepoint instead of char
@Override
public boolean addWord(final char[] word, final int offset, final int length, int score,
- final int dicTypeId, final Dictionary.DataType dataType) {
- Dictionary.DataType dataTypeForLog = dataType;
- final ArrayList<CharSequence> suggestions;
- final int[] sortedScores;
+ final int dicTypeId, final int dataType) {
+ int dataTypeForLog = dataType;
+ final ArrayList<SuggestedWordInfo> suggestions;
final int prefMaxSuggestions;
- if(dataType == Dictionary.DataType.BIGRAM) {
+ if (dataType == Dictionary.BIGRAM) {
suggestions = mBigramSuggestions;
- sortedScores = mBigramScores;
prefMaxSuggestions = PREF_MAX_BIGRAMS;
} else {
suggestions = mSuggestions;
- sortedScores = mScores;
- prefMaxSuggestions = mPrefMaxSuggestions;
+ prefMaxSuggestions = MAX_SUGGESTIONS;
}
int pos = 0;
// Check if it's the same word, only caps are different
- if (Utils.equalsIgnoreCase(mTypedWord, word, offset, length)) {
+ if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
// TODO: remove this surrounding if clause and move this logic to
// getSuggestedWordBuilder.
if (suggestions.size() > 0) {
- final String currentHighestWord = suggestions.get(0).toString();
+ final SuggestedWordInfo currentHighestWord = suggestions.get(0);
// If the current highest word is also equal to typed word, we need to compare
// frequency to determine the insertion position. This does not ensure strictly
// correct ordering, but ensures the top score is on top which is enough for
// removing duplicates correctly.
- if (Utils.equalsIgnoreCase(currentHighestWord, word, offset, length)
- && score <= sortedScores[0]) {
+ if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length)
+ && score <= currentHighestWord.mScore) {
pos = 1;
}
}
} else {
- if (dataType == Dictionary.DataType.UNIGRAM) {
- // Check if the word was already added before (by bigram data)
- int bigramSuggestion = searchBigramSuggestion(word,offset,length);
- if(bigramSuggestion >= 0) {
- dataTypeForLog = Dictionary.DataType.BIGRAM;
- // turn freq from bigram into multiplier specified above
- double multiplier = (((double) mBigramScores[bigramSuggestion])
- / MAXIMUM_BIGRAM_FREQUENCY)
- * (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN)
- + BIGRAM_MULTIPLIER_MIN;
- /* Log.d(TAG,"bigram num: " + bigramSuggestion
- + " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString()
- + " currentScore: " + score + " bigramScore: "
- + mBigramScores[bigramSuggestion]
- + " multiplier: " + multiplier); */
- score = (int)Math.round((score * multiplier));
- }
- }
-
// Check the last one's score and bail
- if (sortedScores[prefMaxSuggestions - 1] >= score) return true;
- while (pos < prefMaxSuggestions) {
- if (sortedScores[pos] < score
- || (sortedScores[pos] == score && length < suggestions.get(pos).length())) {
+ if (suggestions.size() >= prefMaxSuggestions
+ && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
+ while (pos < suggestions.size()) {
+ final int curScore = suggestions.get(pos).mScore;
+ if (curScore < score
+ || (curScore == score && length < suggestions.get(pos).codePointCount())) {
break;
}
pos++;
@@ -514,12 +486,7 @@ public class Suggest implements Dictionary.WordCallback {
return true;
}
- System.arraycopy(sortedScores, pos, sortedScores, pos + 1, prefMaxSuggestions - pos - 1);
- sortedScores[pos] = score;
- int poolSize = mStringPool.size();
- StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1)
- : new StringBuilder(getApproxMaxWordLength());
- sb.setLength(0);
+ final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
// TODO: Must pay attention to locale when changing case.
if (mIsAllUpperCase) {
sb.append(new String(word, offset, length).toUpperCase());
@@ -531,62 +498,59 @@ public class Suggest implements Dictionary.WordCallback {
} else {
sb.append(word, offset, length);
}
- suggestions.add(pos, sb);
+ for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+ sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
+ }
+ // TODO: figure out what type of suggestion this is
+ suggestions.add(pos, new SuggestedWordInfo(sb, score, SuggestedWordInfo.KIND_CORRECTION));
if (suggestions.size() > prefMaxSuggestions) {
- CharSequence garbage = suggestions.remove(prefMaxSuggestions);
- if (garbage instanceof StringBuilder) {
- mStringPool.add(garbage);
- }
+ suggestions.remove(prefMaxSuggestions);
} else {
LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
}
return true;
}
- private int searchBigramSuggestion(final char[] word, final int offset, final int length) {
- // TODO This is almost O(n^2). Might need fix.
- // search whether the word appeared in bigram data
- int bigramSuggestSize = mBigramSuggestions.size();
- for(int i = 0; i < bigramSuggestSize; i++) {
- if(mBigramSuggestions.get(i).length() == length) {
- boolean chk = true;
- for(int j = 0; j < length; j++) {
- if(mBigramSuggestions.get(i).charAt(j) != word[offset+j]) {
- chk = false;
- break;
- }
- }
- if(chk) return i;
- }
- }
-
- return -1;
- }
-
- private void collectGarbage(ArrayList<CharSequence> suggestions, int prefMaxSuggestions) {
- int poolSize = mStringPool.size();
- int garbageSize = suggestions.size();
- while (poolSize < prefMaxSuggestions && garbageSize > 0) {
- CharSequence garbage = suggestions.get(garbageSize - 1);
- if (garbage != null && garbage instanceof StringBuilder) {
- mStringPool.add(garbage);
- poolSize++;
- }
- garbageSize--;
- }
- if (poolSize == prefMaxSuggestions + 1) {
- Log.w("Suggest", "String pool got too big: " + poolSize);
- }
- suggestions.clear();
- }
-
public void close() {
- final Set<Dictionary> dictionaries = new HashSet<Dictionary>();
+ final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
dictionaries.addAll(mUnigramDictionaries.values());
dictionaries.addAll(mBigramDictionaries.values());
for (final Dictionary dictionary : dictionaries) {
dictionary.close();
}
- mMainDict = null;
+ mMainDictionary = null;
+ }
+
+ // TODO: Resolve the inconsistencies between the native auto correction algorithms and
+ // this safety net
+ public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
+ final CharSequence suggestion) {
+ // Safety net for auto correction.
+ // Actually if we hit this safety net, it's a bug.
+ // If user selected aggressive auto correction mode, there is no need to use the safety
+ // net.
+ // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+ // we should not use net because relatively edit distance can be big.
+ final int typedWordLength = typedWord.length();
+ if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) {
+ return false;
+ }
+ final int maxEditDistanceOfNativeDictionary =
+ (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
+ final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
+ if (DBG) {
+ Log.d(TAG, "Autocorrected edit distance = " + distance
+ + ", " + maxEditDistanceOfNativeDictionary);
+ }
+ if (distance > maxEditDistanceOfNativeDictionary) {
+ if (DBG) {
+ Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
+ Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
+ + "Turning off auto-correction.");
+ }
+ return true;
+ } else {
+ return false;
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index a8cdfc02e..45ac9ff53 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -16,169 +16,180 @@
package com.android.inputmethod.latin;
+import android.text.TextUtils;
import android.view.inputmethod.CompletionInfo;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Arrays;
import java.util.HashSet;
-import java.util.List;
public class SuggestedWords {
- public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, null);
+ public static final SuggestedWords EMPTY = new SuggestedWords(
+ new ArrayList<SuggestedWordInfo>(0), false, false, false, false, false, false);
- public final List<CharSequence> mWords;
public final boolean mTypedWordValid;
- public final boolean mHasMinimalSuggestion;
- public final List<SuggestedWordInfo> mSuggestedWordInfoList;
-
- private SuggestedWords(List<CharSequence> words, boolean typedWordValid,
- boolean hasMinimalSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) {
- if (words != null) {
- mWords = words;
- } else {
- mWords = Collections.emptyList();
- }
- mTypedWordValid = typedWordValid;
- mHasMinimalSuggestion = hasMinimalSuggestion;
+ public final boolean mHasAutoCorrectionCandidate;
+ public final boolean mIsPunctuationSuggestions;
+ public final boolean mAllowsToBeAutoCorrected;
+ public final boolean mIsObsoleteSuggestions;
+ public final boolean mIsPrediction;
+ private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
+
+ public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
+ final boolean typedWordValid,
+ final boolean hasAutoCorrectionCandidate,
+ final boolean allowsToBeAutoCorrected,
+ final boolean isPunctuationSuggestions,
+ final boolean isObsoleteSuggestions,
+ final boolean isPrediction) {
mSuggestedWordInfoList = suggestedWordInfoList;
+ mTypedWordValid = typedWordValid;
+ mHasAutoCorrectionCandidate = hasAutoCorrectionCandidate;
+ mAllowsToBeAutoCorrected = allowsToBeAutoCorrected;
+ mIsPunctuationSuggestions = isPunctuationSuggestions;
+ mIsObsoleteSuggestions = isObsoleteSuggestions;
+ mIsPrediction = isPrediction;
}
public int size() {
- return mWords.size();
+ return mSuggestedWordInfoList.size();
}
public CharSequence getWord(int pos) {
- return mWords.get(pos);
+ return mSuggestedWordInfoList.get(pos).mWord;
}
- public boolean hasAutoCorrectionWord() {
- return mHasMinimalSuggestion && size() > 1 && !mTypedWordValid;
+ public SuggestedWordInfo getWordInfo(int pos) {
+ return mSuggestedWordInfoList.get(pos);
}
- public boolean hasWordAboveAutoCorrectionScoreThreshold() {
- return mHasMinimalSuggestion && ((size() > 1 && !mTypedWordValid) || mTypedWordValid);
+ public SuggestedWordInfo getInfo(int pos) {
+ return mSuggestedWordInfoList.get(pos);
}
- public static class Builder {
- private List<CharSequence> mWords = new ArrayList<CharSequence>();
- private boolean mTypedWordValid;
- private boolean mHasMinimalSuggestion;
- private List<SuggestedWordInfo> mSuggestedWordInfoList =
- new ArrayList<SuggestedWordInfo>();
-
- public Builder() {
- // Nothing to do here.
- }
-
- public Builder addWords(List<CharSequence> words,
- List<SuggestedWordInfo> suggestedWordInfoList) {
- final int N = words.size();
- for (int i = 0; i < N; ++i) {
- SuggestedWordInfo suggestedWordInfo = null;
- if (suggestedWordInfoList != null) {
- suggestedWordInfo = suggestedWordInfoList.get(i);
- }
- if (suggestedWordInfo == null) {
- suggestedWordInfo = new SuggestedWordInfo();
- }
- addWord(words.get(i), suggestedWordInfo);
- }
- return this;
- }
-
- public Builder addWord(CharSequence word) {
- return addWord(word, null, false);
- }
-
- public Builder addWord(CharSequence word, CharSequence debugString,
- boolean isPreviousSuggestedWord) {
- SuggestedWordInfo info = new SuggestedWordInfo(debugString, isPreviousSuggestedWord);
- return addWord(word, info);
- }
-
- private Builder addWord(CharSequence word, SuggestedWordInfo suggestedWordInfo) {
- mWords.add(word);
- mSuggestedWordInfoList.add(suggestedWordInfo);
- return this;
- }
-
- public Builder setApplicationSpecifiedCompletions(CompletionInfo[] infos) {
- for (CompletionInfo info : infos)
- addWord(info.getText());
- return this;
- }
+ public boolean hasAutoCorrectionWord() {
+ return mHasAutoCorrectionCandidate && size() > 1 && !mTypedWordValid;
+ }
- public Builder setTypedWordValid(boolean typedWordValid) {
- mTypedWordValid = typedWordValid;
- return this;
- }
+ public boolean willAutoCorrect() {
+ return !mTypedWordValid && mHasAutoCorrectionCandidate;
+ }
- public Builder setHasMinimalSuggestion(boolean hasMinimalSuggestion) {
- mHasMinimalSuggestion = hasMinimalSuggestion;
- return this;
- }
+ @Override
+ public String toString() {
+ // Pretty-print method to help debug
+ return "SuggestedWords:"
+ + " mTypedWordValid=" + mTypedWordValid
+ + " mHasAutoCorrectionCandidate=" + mHasAutoCorrectionCandidate
+ + " mAllowsToBeAutoCorrected=" + mAllowsToBeAutoCorrected
+ + " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions
+ + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
+ }
- // Should get rid of the first one (what the user typed previously) from suggestions
- // and replace it with what the user currently typed.
- public Builder addTypedWordAndPreviousSuggestions(CharSequence typedWord,
- SuggestedWords previousSuggestions) {
- mWords.clear();
- mSuggestedWordInfoList.clear();
- final HashSet<String> alreadySeen = new HashSet<String>();
- addWord(typedWord, null, false);
- alreadySeen.add(typedWord.toString());
- final int previousSize = previousSuggestions.size();
- for (int pos = 1; pos < previousSize; pos++) {
- final String prevWord = previousSuggestions.getWord(pos).toString();
- // Filter out duplicate suggestion.
- if (!alreadySeen.contains(prevWord)) {
- addWord(prevWord, null, true);
- alreadySeen.add(prevWord);
- }
+ public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
+ final CompletionInfo[] infos) {
+ final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>();
+ for (CompletionInfo info : infos) {
+ if (null != info && info.getText() != null) {
+ result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_APP_DEFINED));
}
- mTypedWordValid = false;
- mHasMinimalSuggestion = false;
- return this;
}
+ return result;
+ }
- public SuggestedWords build() {
- return new SuggestedWords(mWords, mTypedWordValid, mHasMinimalSuggestion,
- mSuggestedWordInfoList);
+ // Should get rid of the first one (what the user typed previously) from suggestions
+ // and replace it with what the user currently typed.
+ public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
+ final CharSequence typedWord, final SuggestedWords previousSuggestions) {
+ final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>();
+ final HashSet<String> alreadySeen = new HashSet<String>();
+ suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_TYPED));
+ alreadySeen.add(typedWord.toString());
+ final int previousSize = previousSuggestions.size();
+ for (int pos = 1; pos < previousSize; pos++) {
+ final SuggestedWordInfo prevWordInfo = previousSuggestions.getWordInfo(pos);
+ final String prevWord = prevWordInfo.mWord.toString();
+ // Filter out duplicate suggestion.
+ if (!alreadySeen.contains(prevWord)) {
+ suggestionsList.add(prevWordInfo);
+ alreadySeen.add(prevWord);
+ }
}
+ return suggestionsList;
+ }
- public int size() {
- return mWords.size();
+ public static class SuggestedWordInfo {
+ public static final int MAX_SCORE = Integer.MAX_VALUE;
+ public static final int KIND_TYPED = 0; // What user typed
+ public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
+ public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+ public static final int KIND_WHITELIST = 3; // Whitelisted word
+ public static final int KIND_BLACKLIST = 4; // Blacklisted word
+ public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+ public static final int KIND_APP_DEFINED = 6; // Suggested by the application
+ public static final int KIND_SHORTCUT = 7; // A shortcut
+ private final String mWordStr;
+ public final CharSequence mWord;
+ public final int mScore;
+ public final int mKind; // one of the KIND_* constants above
+ public final int mCodePointCount;
+ private String mDebugString = "";
+
+ public SuggestedWordInfo(final CharSequence word, final int score, final int kind) {
+ mWordStr = word.toString();
+ mWord = word;
+ mScore = score;
+ mKind = kind;
+ mCodePointCount = mWordStr.codePointCount(0, mWordStr.length());
+ }
+
+
+ public void setDebugString(String str) {
+ if (null == str) throw new NullPointerException("Debug info is null");
+ mDebugString = str;
}
- public CharSequence getWord(int pos) {
- return mWords.get(pos);
+ public String getDebugString() {
+ return mDebugString;
}
- }
-
- public static class SuggestedWordInfo {
- private final CharSequence mDebugString;
- private final boolean mPreviousSuggestedWord;
- public SuggestedWordInfo() {
- mDebugString = "";
- mPreviousSuggestedWord = false;
+ public int codePointCount() {
+ return mCodePointCount;
}
- public SuggestedWordInfo(CharSequence debugString, boolean previousSuggestedWord) {
- mDebugString = debugString;
- mPreviousSuggestedWord = previousSuggestedWord;
+ public int codePointAt(int i) {
+ return mWordStr.codePointAt(i);
}
- public String getDebugString() {
- if (mDebugString == null) {
- return "";
+ @Override
+ public String toString() {
+ if (TextUtils.isEmpty(mDebugString)) {
+ return mWordStr;
} else {
- return mDebugString.toString();
+ return mWordStr + " (" + mDebugString.toString() + ")";
}
}
- public boolean isPreviousSuggestedWord () {
- return mPreviousSuggestedWord;
+ // TODO: Consolidate this method and StringUtils.removeDupes() in the future.
+ public static void removeDups(ArrayList<SuggestedWordInfo> candidates) {
+ if (candidates.size() <= 1) {
+ return;
+ }
+ int i = 1;
+ while(i < candidates.size()) {
+ final SuggestedWordInfo cur = candidates.get(i);
+ for (int j = 0; j < i; ++j) {
+ final SuggestedWordInfo previous = candidates.get(j);
+ if (TextUtils.equals(cur.mWord, previous.mWord)) {
+ candidates.remove(cur.mScore < previous.mScore ? i : j);
+ --i;
+ break;
+ }
+ }
+ ++i;
+ }
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
new file mode 100644
index 000000000..673b54500
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import android.content.Context;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+import java.util.Locale;
+
+public class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
+ private boolean mClosed;
+
+ public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) {
+ super(context, Suggest.DIC_CONTACTS, locale);
+ }
+
+ @Override
+ public synchronized void getWords(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final WordCallback callback,
+ final ProximityInfo proximityInfo) {
+ syncReloadDictionaryIfRequired();
+ getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ }
+
+ @Override
+ public synchronized boolean isValidWord(CharSequence word) {
+ syncReloadDictionaryIfRequired();
+ return isValidWordInner(word);
+ }
+
+ // Protect against multiple closing
+ @Override
+ public synchronized void close() {
+ // Actually with the current implementation of ContactsDictionary it's safe to close
+ // several times, so the following protection is really only for foolproofing
+ if (mClosed) return;
+ mClosed = true;
+ super.close();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
new file mode 100644
index 000000000..1606a34e0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import android.content.Context;
+
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
+
+ public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale) {
+ this(context, locale, false);
+ }
+
+ public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale,
+ final boolean alsoUseMoreRestrictiveLocales) {
+ super(context, locale, alsoUseMoreRestrictiveLocales);
+ }
+
+ @Override
+ public synchronized void getWords(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final WordCallback callback,
+ final ProximityInfo proximityInfo) {
+ syncReloadDictionaryIfRequired();
+ getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ }
+
+ @Override
+ public synchronized boolean isValidWord(CharSequence word) {
+ syncReloadDictionaryIfRequired();
+ return isValidWordInner(word);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java b/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java
new file mode 100644
index 000000000..4265309e5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.util.LruCache;
+
+public class TargetApplicationGetter extends AsyncTask<String, Void, ApplicationInfo> {
+
+ private static final int MAX_CACHE_ENTRIES = 64; // arbitrary
+ private static LruCache<String, ApplicationInfo> sCache =
+ new LruCache<String, ApplicationInfo>(MAX_CACHE_ENTRIES);
+
+ public static ApplicationInfo getCachedApplicationInfo(final String packageName) {
+ if (null == packageName) return null;
+ return sCache.get(packageName);
+ }
+ public static void removeApplicationInfoCache(final String packageName) {
+ sCache.remove(packageName);
+ }
+
+ public interface OnTargetApplicationKnownListener {
+ public void onTargetApplicationKnown(final ApplicationInfo info);
+ }
+
+ private Context mContext;
+ private final OnTargetApplicationKnownListener mListener;
+
+ public TargetApplicationGetter(final Context context,
+ final OnTargetApplicationKnownListener listener) {
+ mContext = context;
+ mListener = listener;
+ }
+
+ @Override
+ protected ApplicationInfo doInBackground(final String... packageName) {
+ final PackageManager pm = mContext.getPackageManager();
+ mContext = null; // Bazooka-powered anti-leak device
+ try {
+ final ApplicationInfo targetAppInfo =
+ pm.getApplicationInfo(packageName[0], 0 /* flags */);
+ sCache.put(packageName[0], targetAppInfo);
+ return targetAppInfo;
+ } catch (android.content.pm.PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final ApplicationInfo info) {
+ mListener.onTargetApplicationKnown(info);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java
deleted file mode 100644
index de13f3ae4..000000000
--- a/java/src/com/android/inputmethod/latin/TextEntryState.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import com.android.inputmethod.latin.Utils.RingCharBuffer;
-
-import android.util.Log;
-
-public class TextEntryState {
- private static final String TAG = TextEntryState.class.getSimpleName();
- private static final boolean DEBUG = false;
-
- private static final int UNKNOWN = 0;
- private static final int START = 1;
- private static final int IN_WORD = 2;
- private static final int ACCEPTED_DEFAULT = 3;
- private static final int PICKED_SUGGESTION = 4;
- private static final int PUNCTUATION_AFTER_WORD = 5;
- private static final int PUNCTUATION_AFTER_ACCEPTED = 6;
- private static final int SPACE_AFTER_ACCEPTED = 7;
- private static final int SPACE_AFTER_PICKED = 8;
- private static final int UNDO_COMMIT = 9;
- private static final int RECORRECTING = 10;
- private static final int PICKED_RECORRECTION = 11;
-
- private static int sState = UNKNOWN;
- private static int sPreviousState = UNKNOWN;
-
- private static void setState(final int newState) {
- sPreviousState = sState;
- sState = newState;
- }
-
- public static void acceptedDefault(CharSequence typedWord, CharSequence actualWord,
- int separatorCode) {
- if (typedWord == null) return;
- setState(ACCEPTED_DEFAULT);
- LatinImeLogger.logOnAutoCorrection(
- typedWord.toString(), actualWord.toString(), separatorCode);
- if (DEBUG)
- displayState("acceptedDefault", "typedWord", typedWord, "actualWord", actualWord);
- }
-
- // State.ACCEPTED_DEFAULT will be changed to other sub-states
- // (see "case ACCEPTED_DEFAULT" in typedCharacter() below),
- // and should be restored back to State.ACCEPTED_DEFAULT after processing for each sub-state.
- public static void backToAcceptedDefault(CharSequence typedWord) {
- if (typedWord == null) return;
- switch (sState) {
- case SPACE_AFTER_ACCEPTED:
- case PUNCTUATION_AFTER_ACCEPTED:
- case IN_WORD:
- setState(ACCEPTED_DEFAULT);
- break;
- default:
- break;
- }
- if (DEBUG) displayState("backToAcceptedDefault", "typedWord", typedWord);
- }
-
- public static void acceptedTyped(CharSequence typedWord) {
- setState(PICKED_SUGGESTION);
- if (DEBUG) displayState("acceptedTyped", "typedWord", typedWord);
- }
-
- public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) {
- if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
- setState(PICKED_RECORRECTION);
- } else {
- setState(PICKED_SUGGESTION);
- }
- if (DEBUG)
- displayState("acceptedSuggestion", "typedWord", typedWord, "actualWord", actualWord);
- }
-
- public static void selectedForRecorrection() {
- setState(RECORRECTING);
- if (DEBUG) displayState("selectedForRecorrection");
- }
-
- public static void onAbortRecorrection() {
- if (sState == RECORRECTING || sState == PICKED_RECORRECTION) {
- setState(START);
- }
- if (DEBUG) displayState("onAbortRecorrection");
- }
-
- public static void typedCharacter(char c, boolean isSeparator, int x, int y) {
- final boolean isSpace = (c == ' ');
- switch (sState) {
- case IN_WORD:
- if (isSpace || isSeparator) {
- setState(START);
- } else {
- // State hasn't changed.
- }
- break;
- case ACCEPTED_DEFAULT:
- case SPACE_AFTER_PICKED:
- case PUNCTUATION_AFTER_ACCEPTED:
- if (isSpace) {
- setState(SPACE_AFTER_ACCEPTED);
- } else if (isSeparator) {
- // Swap
- setState(PUNCTUATION_AFTER_ACCEPTED);
- } else {
- setState(IN_WORD);
- }
- break;
- case PICKED_SUGGESTION:
- case PICKED_RECORRECTION:
- if (isSpace) {
- setState(SPACE_AFTER_PICKED);
- } else if (isSeparator) {
- // Swap
- setState(PUNCTUATION_AFTER_ACCEPTED);
- } else {
- setState(IN_WORD);
- }
- break;
- case START:
- case UNKNOWN:
- case SPACE_AFTER_ACCEPTED:
- case PUNCTUATION_AFTER_WORD:
- if (!isSpace && !isSeparator) {
- setState(IN_WORD);
- } else {
- setState(START);
- }
- break;
- case UNDO_COMMIT:
- if (isSpace || isSeparator) {
- setState(ACCEPTED_DEFAULT);
- } else {
- setState(IN_WORD);
- }
- break;
- case RECORRECTING:
- setState(START);
- break;
- }
- RingCharBuffer.getInstance().push(c, x, y);
- if (isSeparator) {
- LatinImeLogger.logOnInputSeparator();
- } else {
- LatinImeLogger.logOnInputChar();
- }
- if (DEBUG) displayState("typedCharacter", "char", c, "isSeparator", isSeparator);
- }
-
- public static void backspace() {
- if (sState == ACCEPTED_DEFAULT) {
- setState(UNDO_COMMIT);
- LatinImeLogger.logOnAutoCorrectionCancelled();
- } else if (sState == UNDO_COMMIT) {
- setState(IN_WORD);
- }
- if (DEBUG) displayState("backspace");
- }
-
- public static void reset() {
- setState(START);
- if (DEBUG) displayState("reset");
- }
-
- public static boolean isAcceptedDefault() {
- return sState == ACCEPTED_DEFAULT;
- }
-
- public static boolean isSpaceAfterPicked() {
- return sState == SPACE_AFTER_PICKED;
- }
-
- public static boolean isUndoCommit() {
- return sState == UNDO_COMMIT;
- }
-
- public static boolean isPunctuationAfterAccepted() {
- return sState == PUNCTUATION_AFTER_ACCEPTED;
- }
-
- public static boolean isRecorrecting() {
- return sState == RECORRECTING || sState == PICKED_RECORRECTION;
- }
-
- public static String getState() {
- return stateName(sState);
- }
-
- private static String stateName(int state) {
- switch (state) {
- case START: return "START";
- case IN_WORD: return "IN_WORD";
- case ACCEPTED_DEFAULT: return "ACCEPTED_DEFAULT";
- case PICKED_SUGGESTION: return "PICKED_SUGGESTION";
- case PUNCTUATION_AFTER_WORD: return "PUNCTUATION_AFTER_WORD";
- case PUNCTUATION_AFTER_ACCEPTED: return "PUNCTUATION_AFTER_ACCEPTED";
- case SPACE_AFTER_ACCEPTED: return "SPACE_AFTER_ACCEPTED";
- case SPACE_AFTER_PICKED: return "SPACE_AFTER_PICKED";
- case UNDO_COMMIT: return "UNDO_COMMIT";
- case RECORRECTING: return "RECORRECTING";
- case PICKED_RECORRECTION: return "PICKED_RECORRECTION";
- default: return "UNKNOWN";
- }
- }
-
- private static void displayState(String title, Object ... args) {
- final StringBuilder sb = new StringBuilder(title);
- sb.append(':');
- for (int i = 0; i < args.length; i += 2) {
- sb.append(' ');
- sb.append(args[i]);
- sb.append('=');
- sb.append(args[i+1].toString());
- }
- sb.append(" state=");
- sb.append(stateName(sState));
- sb.append(" previous=");
- sb.append(stateName(sPreviousState));
- Log.d(TAG, sb.toString());
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
deleted file mode 100644
index 5b615ca29..000000000
--- a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java
+++ /dev/null
@@ -1,399 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.os.AsyncTask;
-import android.provider.BaseColumns;
-import android.util.Log;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-
-/**
- * Stores all the pairs user types in databases. Prune the database if the size
- * gets too big. Unlike AutoDictionary, it even stores the pairs that are already
- * in the dictionary.
- */
-public class UserBigramDictionary extends ExpandableDictionary {
- private static final String TAG = "UserBigramDictionary";
-
- /** Any pair being typed or picked */
- private static final int FREQUENCY_FOR_TYPED = 2;
-
- /** Maximum frequency for all pairs */
- private static final int FREQUENCY_MAX = 127;
-
- /** Maximum number of pairs. Pruning will start when databases goes above this number. */
- private static int sMaxUserBigrams = 10000;
-
- /**
- * When it hits maximum bigram pair, it will delete until you are left with
- * only (sMaxUserBigrams - sDeleteUserBigrams) pairs.
- * Do not keep this number small to avoid deleting too often.
- */
- private static int sDeleteUserBigrams = 1000;
-
- /**
- * Database version should increase if the database structure changes
- */
- private static final int DATABASE_VERSION = 1;
-
- private static final String DATABASE_NAME = "userbigram_dict.db";
-
- /** Name of the words table in the database */
- private static final String MAIN_TABLE_NAME = "main";
- // TODO: Consume less space by using a unique id for locale instead of the whole
- // 2-5 character string. (Same TODO from AutoDictionary)
- private static final String MAIN_COLUMN_ID = BaseColumns._ID;
- private static final String MAIN_COLUMN_WORD1 = "word1";
- private static final String MAIN_COLUMN_WORD2 = "word2";
- private static final String MAIN_COLUMN_LOCALE = "locale";
-
- /** Name of the frequency table in the database */
- private static final String FREQ_TABLE_NAME = "frequency";
- private static final String FREQ_COLUMN_ID = BaseColumns._ID;
- private static final String FREQ_COLUMN_PAIR_ID = "pair_id";
- private static final String FREQ_COLUMN_FREQUENCY = "freq";
-
- private final LatinIME mIme;
-
- /** Locale for which this auto dictionary is storing words */
- private String mLocale;
-
- private HashSet<Bigram> mPendingWrites = new HashSet<Bigram>();
- private final Object mPendingWritesLock = new Object();
- private static volatile boolean sUpdatingDB = false;
-
- private final static HashMap<String, String> sDictProjectionMap;
-
- static {
- sDictProjectionMap = new HashMap<String, String>();
- sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
- sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
- sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
- sDictProjectionMap.put(MAIN_COLUMN_LOCALE, MAIN_COLUMN_LOCALE);
-
- sDictProjectionMap.put(FREQ_COLUMN_ID, FREQ_COLUMN_ID);
- sDictProjectionMap.put(FREQ_COLUMN_PAIR_ID, FREQ_COLUMN_PAIR_ID);
- sDictProjectionMap.put(FREQ_COLUMN_FREQUENCY, FREQ_COLUMN_FREQUENCY);
- }
-
- private static DatabaseHelper sOpenHelper = null;
-
- private static class Bigram {
- public final String mWord1;
- public final String mWord2;
- public final int frequency;
-
- Bigram(String word1, String word2, int frequency) {
- this.mWord1 = word1;
- this.mWord2 = word2;
- this.frequency = frequency;
- }
-
- @Override
- public boolean equals(Object bigram) {
- Bigram bigram2 = (Bigram) bigram;
- return (mWord1.equals(bigram2.mWord1) && mWord2.equals(bigram2.mWord2));
- }
-
- @Override
- public int hashCode() {
- return (mWord1 + " " + mWord2).hashCode();
- }
- }
-
- public void setDatabaseMax(int maxUserBigram) {
- sMaxUserBigrams = maxUserBigram;
- }
-
- public void setDatabaseDelete(int deleteUserBigram) {
- sDeleteUserBigrams = deleteUserBigram;
- }
-
- public UserBigramDictionary(Context context, LatinIME ime, String locale, int dicTypeId) {
- super(context, dicTypeId);
- mIme = ime;
- mLocale = locale;
- if (sOpenHelper == null) {
- sOpenHelper = new DatabaseHelper(getContext());
- }
- if (mLocale != null && mLocale.length() > 1) {
- loadDictionary();
- }
- }
-
- @Override
- public void close() {
- flushPendingWrites();
- // Don't close the database as locale changes will require it to be reopened anyway
- // Also, the database is written to somewhat frequently, so it needs to be kept alive
- // throughout the life of the process.
- // mOpenHelper.close();
- super.close();
- }
-
- /**
- * Pair will be added to the userbigram database.
- */
- public int addBigrams(String word1, String word2) {
- // remove caps if second word is autocapitalized
- if (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) {
- word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1);
- }
- // Do not insert a word as a bigram of itself
- if (word1.equals(word2)) {
- return 0;
- }
-
- int freq = super.addBigram(word1, word2, FREQUENCY_FOR_TYPED);
- if (freq > FREQUENCY_MAX) freq = FREQUENCY_MAX;
- synchronized (mPendingWritesLock) {
- if (freq == FREQUENCY_FOR_TYPED || mPendingWrites.isEmpty()) {
- mPendingWrites.add(new Bigram(word1, word2, freq));
- } else {
- Bigram bi = new Bigram(word1, word2, freq);
- mPendingWrites.remove(bi);
- mPendingWrites.add(bi);
- }
- }
-
- return freq;
- }
-
- /**
- * Schedules a background thread to write any pending words to the database.
- */
- public void flushPendingWrites() {
- synchronized (mPendingWritesLock) {
- // Nothing pending? Return
- if (mPendingWrites.isEmpty()) return;
- // Create a background thread to write the pending entries
- new UpdateDbTask(getContext(), sOpenHelper, mPendingWrites, mLocale).execute();
- // Create a new map for writing new entries into while the old one is written to db
- mPendingWrites = new HashSet<Bigram>();
- }
- }
-
- /** Used for testing purpose **/
- void waitUntilUpdateDBDone() {
- synchronized (mPendingWritesLock) {
- while (sUpdatingDB) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- }
- }
- return;
- }
- }
-
- @Override
- public void loadDictionaryAsync() {
- // Load the words that correspond to the current input locale
- Cursor cursor = query(MAIN_COLUMN_LOCALE + "=?", new String[] { mLocale });
- try {
- if (cursor.moveToFirst()) {
- int word1Index = cursor.getColumnIndex(MAIN_COLUMN_WORD1);
- int word2Index = cursor.getColumnIndex(MAIN_COLUMN_WORD2);
- int frequencyIndex = cursor.getColumnIndex(FREQ_COLUMN_FREQUENCY);
- while (!cursor.isAfterLast()) {
- String word1 = cursor.getString(word1Index);
- String word2 = cursor.getString(word2Index);
- int frequency = cursor.getInt(frequencyIndex);
- // Safeguard against adding really long words. Stack may overflow due
- // to recursive lookup
- if (word1.length() < MAX_WORD_LENGTH && word2.length() < MAX_WORD_LENGTH) {
- super.setBigram(word1, word2, frequency);
- }
- cursor.moveToNext();
- }
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Query the database
- */
- private Cursor query(String selection, String[] selectionArgs) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
- // main INNER JOIN frequency ON (main._id=freq.pair_id)
- qb.setTables(MAIN_TABLE_NAME + " INNER JOIN " + FREQ_TABLE_NAME + " ON ("
- + MAIN_TABLE_NAME + "." + MAIN_COLUMN_ID + "=" + FREQ_TABLE_NAME + "."
- + FREQ_COLUMN_PAIR_ID +")");
-
- qb.setProjectionMap(sDictProjectionMap);
-
- // Get the database and run the query
- SQLiteDatabase db = sOpenHelper.getReadableDatabase();
- Cursor c = qb.query(db,
- new String[] { MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD2, FREQ_COLUMN_FREQUENCY },
- selection, selectionArgs, null, null, null);
- return c;
- }
-
- /**
- * This class helps open, create, and upgrade the database file.
- */
- private static class DatabaseHelper extends SQLiteOpenHelper {
-
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("PRAGMA foreign_keys = ON;");
- db.execSQL("CREATE TABLE " + MAIN_TABLE_NAME + " ("
- + MAIN_COLUMN_ID + " INTEGER PRIMARY KEY,"
- + MAIN_COLUMN_WORD1 + " TEXT,"
- + MAIN_COLUMN_WORD2 + " TEXT,"
- + MAIN_COLUMN_LOCALE + " TEXT"
- + ");");
- db.execSQL("CREATE TABLE " + FREQ_TABLE_NAME + " ("
- + FREQ_COLUMN_ID + " INTEGER PRIMARY KEY,"
- + FREQ_COLUMN_PAIR_ID + " INTEGER,"
- + FREQ_COLUMN_FREQUENCY + " INTEGER,"
- + "FOREIGN KEY(" + FREQ_COLUMN_PAIR_ID + ") REFERENCES " + MAIN_TABLE_NAME
- + "(" + MAIN_COLUMN_ID + ")" + " ON DELETE CASCADE"
- + ");");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS " + MAIN_TABLE_NAME);
- db.execSQL("DROP TABLE IF EXISTS " + FREQ_TABLE_NAME);
- onCreate(db);
- }
- }
-
- /**
- * Async task to write pending words to the database so that it stays in sync with
- * the in-memory trie.
- */
- private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
- private final HashSet<Bigram> mMap;
- private final DatabaseHelper mDbHelper;
- private final String mLocale;
-
- public UpdateDbTask(Context context, DatabaseHelper openHelper,
- HashSet<Bigram> pendingWrites, String locale) {
- mMap = pendingWrites;
- mLocale = locale;
- mDbHelper = openHelper;
- }
-
- /** Prune any old data if the database is getting too big. */
- private void checkPruneData(SQLiteDatabase db) {
- db.execSQL("PRAGMA foreign_keys = ON;");
- Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID },
- null, null, null, null, null);
- try {
- int totalRowCount = c.getCount();
- // prune out old data if we have too much data
- if (totalRowCount > sMaxUserBigrams) {
- int numDeleteRows = (totalRowCount - sMaxUserBigrams) + sDeleteUserBigrams;
- int pairIdColumnId = c.getColumnIndex(FREQ_COLUMN_PAIR_ID);
- c.moveToFirst();
- int count = 0;
- while (count < numDeleteRows && !c.isAfterLast()) {
- String pairId = c.getString(pairIdColumnId);
- // Deleting from MAIN table will delete the frequencies
- // due to FOREIGN KEY .. ON DELETE CASCADE
- db.delete(MAIN_TABLE_NAME, MAIN_COLUMN_ID + "=?",
- new String[] { pairId });
- c.moveToNext();
- count++;
- }
- }
- } finally {
- c.close();
- }
- }
-
- @Override
- protected void onPreExecute() {
- sUpdatingDB = true;
- }
-
- @Override
- protected Void doInBackground(Void... v) {
- SQLiteDatabase db = mDbHelper.getWritableDatabase();
- db.execSQL("PRAGMA foreign_keys = ON;");
- // Write all the entries to the db
- Iterator<Bigram> iterator = mMap.iterator();
- while (iterator.hasNext()) {
- Bigram bi = iterator.next();
-
- // find pair id
- Cursor c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
- MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND "
- + MAIN_COLUMN_LOCALE + "=?",
- new String[] { bi.mWord1, bi.mWord2, mLocale }, null, null, null);
-
- int pairId;
- if (c.moveToFirst()) {
- // existing pair
- pairId = c.getInt(c.getColumnIndex(MAIN_COLUMN_ID));
- db.delete(FREQ_TABLE_NAME, FREQ_COLUMN_PAIR_ID + "=?",
- new String[] { Integer.toString(pairId) });
- } else {
- // new pair
- Long pairIdLong = db.insert(MAIN_TABLE_NAME, null,
- getContentValues(bi.mWord1, bi.mWord2, mLocale));
- pairId = pairIdLong.intValue();
- }
- c.close();
-
- // insert new frequency
- db.insert(FREQ_TABLE_NAME, null, getFrequencyContentValues(pairId, bi.frequency));
- }
- checkPruneData(db);
- sUpdatingDB = false;
-
- return null;
- }
-
- private ContentValues getContentValues(String word1, String word2, String locale) {
- ContentValues values = new ContentValues(3);
- values.put(MAIN_COLUMN_WORD1, word1);
- values.put(MAIN_COLUMN_WORD2, word2);
- values.put(MAIN_COLUMN_LOCALE, locale);
- return values;
- }
-
- private ContentValues getFrequencyContentValues(int pairId, int frequency) {
- ContentValues values = new ContentValues(2);
- values.put(FREQ_COLUMN_PAIR_ID, pairId);
- values.put(FREQ_COLUMN_FREQUENCY, frequency);
- return values;
- }
- }
-
-}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
new file mode 100644
index 000000000..5bcdb57b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -0,0 +1,224 @@
+/*
+ * 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;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.provider.UserDictionary.Words;
+import android.text.TextUtils;
+
+import java.util.Arrays;
+
+/**
+ * An expandable dictionary that stores the words in the user unigram dictionary.
+ *
+ * Largely a copy of UserDictionary, will replace that class in the future.
+ */
+public class UserBinaryDictionary extends ExpandableBinaryDictionary {
+
+ // TODO: use Words.SHORTCUT when it's public in the SDK
+ final static String SHORTCUT = "shortcut";
+ private static final String[] PROJECTION_QUERY;
+ static {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ SHORTCUT,
+ Words.FREQUENCY,
+ };
+ } else {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ Words.FREQUENCY,
+ };
+ }
+ }
+
+ private static final String NAME = "userunigram";
+
+ // This is not exported by the framework so we pretty much have to write it here verbatim
+ private static final String ACTION_USER_DICTIONARY_INSERT =
+ "com.android.settings.USER_DICTIONARY_INSERT";
+
+ private ContentObserver mObserver;
+ final private String mLocale;
+ final private boolean mAlsoUseMoreRestrictiveLocales;
+
+ public UserBinaryDictionary(final Context context, final String locale) {
+ this(context, locale, false);
+ }
+
+ public UserBinaryDictionary(final Context context, final String locale,
+ final boolean alsoUseMoreRestrictiveLocales) {
+ super(context, getFilenameWithLocale(NAME, locale), Suggest.DIC_USER);
+ if (null == locale) throw new NullPointerException(); // Catch the error earlier
+ mLocale = locale;
+ mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
+ // Perform a managed query. The Activity will handle closing and re-querying the cursor
+ // when needed.
+ ContentResolver cres = context.getContentResolver();
+
+ mObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean self) {
+ setRequiresReload(true);
+ }
+ };
+ cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
+
+ loadDictionary();
+ }
+
+ @Override
+ public synchronized void close() {
+ if (mObserver != null) {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ super.close();
+ }
+
+ @Override
+ public void loadDictionaryAsync() {
+ // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
+ // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
+ // This is correct for locale processing.
+ // For this example, we'll look at the "en_US_POSIX" case.
+ final String[] localeElements =
+ TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
+ final int length = localeElements.length;
+
+ final StringBuilder request = new StringBuilder("(locale is NULL)");
+ String localeSoFar = "";
+ // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
+ // and request = "(locale is NULL)"
+ for (int i = 0; i < length; ++i) {
+ // i | localeSoFar | localeElements
+ // 0 | "" | ["en", "US", "POSIX"]
+ // 1 | "en_" | ["en", "US", "POSIX"]
+ // 2 | "en_US_" | ["en", "en_US", "POSIX"]
+ localeElements[i] = localeSoFar + localeElements[i];
+ localeSoFar = localeElements[i] + "_";
+ // i | request
+ // 0 | "(locale is NULL)"
+ // 1 | "(locale is NULL) or (locale=?)"
+ // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
+ request.append(" or (locale=?)");
+ }
+ // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
+ // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
+
+ final String[] requestArguments;
+ // If length == 3, we already have all the arguments we need (common prefix is meaningless
+ // inside variants
+ if (mAlsoUseMoreRestrictiveLocales && length < 3) {
+ request.append(" or (locale like ?)");
+ // The following creates an array with one more (null) position
+ final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
+ Arrays.copyOf(localeElements, length + 1);
+ localeElementsWithMoreRestrictiveLocalesIncluded[length] =
+ localeElements[length - 1] + "_%";
+ requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
+ // If for example localeElements = ["en"]
+ // then requestArguments = ["en", "en_%"]
+ // and request = (locale is NULL) or (locale=?) or (locale like ?)
+ // If localeElements = ["en", "en_US"]
+ // then requestArguments = ["en", "en_US", "en_US_%"]
+ } else {
+ requestArguments = localeElements;
+ }
+ final Cursor cursor = mContext.getContentResolver().query(
+ Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
+ try {
+ addWords(cursor);
+ } finally {
+ if (null != cursor) cursor.close();
+ }
+ }
+
+ public boolean isEnabled() {
+ final ContentResolver cr = mContext.getContentResolver();
+ final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
+ if (client != null) {
+ client.release();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Adds a word to the user dictionary and makes it persistent.
+ *
+ * This will call upon the system interface to do the actual work through the intent readied by
+ * the system to this effect.
+ *
+ * @param word the word to add. If the word is capitalized, then the dictionary will
+ * recognize it as a capitalized word when searched.
+ * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
+ * the highest.
+ * @TODO use a higher or float range for frequency
+ */
+ public synchronized void addWordToUserDictionary(final String word, final int frequency) {
+ // TODO: do something for the UI. With the following, any sufficiently long word will
+ // look like it will go to the user dictionary but it won't.
+ // Safeguard against adding long words. Can cause stack overflow.
+ if (word.length() >= MAX_WORD_LENGTH) return;
+
+ // TODO: Add an argument to the intent to specify the frequency.
+ Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT);
+ intent.putExtra(Words.WORD, word);
+ intent.putExtra(Words.LOCALE, mLocale);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ }
+
+ private void addWords(Cursor cursor) {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ final boolean hasShortcutColumn = android.os.Build.VERSION.SDK_INT >= 16;
+ clearFusionDictionary();
+ if (cursor == null) return;
+ if (cursor.moveToFirst()) {
+ final int indexWord = cursor.getColumnIndex(Words.WORD);
+ final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(SHORTCUT) : 0;
+ final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
+ while (!cursor.isAfterLast()) {
+ final String word = cursor.getString(indexWord);
+ final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
+ final int frequency = cursor.getInt(indexFrequency);
+ // Safeguard against adding really long words.
+ if (word.length() < MAX_WORD_LENGTH) {
+ super.addWord(word, null, frequency);
+ }
+ if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
+ super.addWord(shortcut, word, frequency);
+ }
+ cursor.moveToNext();
+ }
+ }
+ }
+
+ @Override
+ protected boolean hasContentChanged() {
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
deleted file mode 100644
index c06bd736e..000000000
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.UserDictionary.Words;
-
-public class UserDictionary extends ExpandableDictionary {
-
- private static final String[] PROJECTION_QUERY = {
- Words.WORD,
- Words.FREQUENCY,
- };
-
- private static final String[] PROJECTION_ADD = {
- Words._ID,
- Words.FREQUENCY,
- Words.LOCALE,
- };
-
- private ContentObserver mObserver;
- private String mLocale;
-
- public UserDictionary(Context context, String locale) {
- super(context, Suggest.DIC_USER);
- mLocale = locale;
- // Perform a managed query. The Activity will handle closing and requerying the cursor
- // when needed.
- ContentResolver cres = context.getContentResolver();
-
- cres.registerContentObserver(Words.CONTENT_URI, true, mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- });
-
- loadDictionary();
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void loadDictionaryAsync() {
- Cursor cursor = getContext().getContentResolver()
- .query(Words.CONTENT_URI, PROJECTION_QUERY, "(locale IS NULL) or (locale=?)",
- new String[] { mLocale }, null);
- addWords(cursor);
- }
-
- /**
- * Adds a word to the dictionary and makes it persistent.
- * @param word the word to add. If the word is capitalized, then the dictionary will
- * recognize it as a capitalized word when searched.
- * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
- * the highest.
- * @TODO use a higher or float range for frequency
- */
- @Override
- public synchronized void addWord(final String word, final int frequency) {
- // Force load the dictionary here synchronously
- if (getRequiresReload()) loadDictionaryAsync();
- // Safeguard against adding long words. Can cause stack overflow.
- if (word.length() >= getMaxWordLength()) return;
-
- super.addWord(word, frequency);
-
- // Update the user dictionary provider
- final ContentValues values = new ContentValues(5);
- values.put(Words.WORD, word);
- values.put(Words.FREQUENCY, frequency);
- values.put(Words.LOCALE, mLocale);
- values.put(Words.APP_ID, 0);
-
- final ContentResolver contentResolver = getContext().getContentResolver();
- new Thread("addWord") {
- @Override
- public void run() {
- Cursor cursor = contentResolver.query(Words.CONTENT_URI, PROJECTION_ADD,
- "word=? and ((locale IS NULL) or (locale=?))",
- new String[] { word, mLocale }, null);
- if (cursor != null && cursor.moveToFirst()) {
- String locale = cursor.getString(cursor.getColumnIndex(Words.LOCALE));
- // If locale is null, we will not override the entry.
- if (locale != null && locale.equals(mLocale.toString())) {
- long id = cursor.getLong(cursor.getColumnIndex(Words._ID));
- Uri uri = Uri.withAppendedPath(Words.CONTENT_URI, Long.toString(id));
- // Update the entry with new frequency value.
- contentResolver.update(uri, values, null, null);
- }
- } else {
- // Insert new entry.
- contentResolver.insert(Words.CONTENT_URI, values);
- }
- }
- }.start();
-
- // In case the above does a synchronous callback of the change observer
- setRequiresReload(false);
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes, final WordCallback callback) {
- super.getWords(codes, callback);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- return super.isValidWord(word);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
- if (cursor == null) return;
- final int maxWordLength = getMaxWordLength();
- if (cursor.moveToFirst()) {
- final int indexWord = cursor.getColumnIndex(Words.WORD);
- final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
- while (!cursor.isAfterLast()) {
- String word = cursor.getString(indexWord);
- int frequency = cursor.getInt(indexFrequency);
- // Safeguard against adding really long words. Stack may overflow due
- // to recursion
- if (word.length() < maxWordLength) {
- super.addWord(word, frequency);
- }
- cursor.moveToNext();
- }
- }
- cursor.close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
new file mode 100644
index 000000000..5095f6582
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.os.AsyncTask;
+import android.provider.BaseColumns;
+import android.util.Log;
+
+import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Locally gathers stats about the words user types and various other signals like auto-correction
+ * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
+ */
+public class UserHistoryDictionary extends ExpandableDictionary {
+ private static final String TAG = "UserHistoryDictionary";
+ public static final boolean DBG_SAVE_RESTORE = false;
+ public static final boolean DBG_STRESS_TEST = false;
+ public static final boolean DBG_ALWAYS_WRITE = false;
+ public static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG;
+
+ /** Any pair being typed or picked */
+ private static final int FREQUENCY_FOR_TYPED = 2;
+
+ /** Maximum number of pairs. Pruning will start when databases goes above this number. */
+ private static int sMaxHistoryBigrams = 10000;
+
+ /**
+ * When it hits maximum bigram pair, it will delete until you are left with
+ * only (sMaxHistoryBigrams - sDeleteHistoryBigrams) pairs.
+ * Do not keep this number small to avoid deleting too often.
+ */
+ private static int sDeleteHistoryBigrams = 1000;
+
+ /**
+ * Database version should increase if the database structure changes
+ */
+ private static final int DATABASE_VERSION = 1;
+
+ private static final String DATABASE_NAME = "userbigram_dict.db";
+
+ /** Name of the words table in the database */
+ private static final String MAIN_TABLE_NAME = "main";
+ // TODO: Consume less space by using a unique id for locale instead of the whole
+ // 2-5 character string.
+ private static final String MAIN_COLUMN_ID = BaseColumns._ID;
+ private static final String MAIN_COLUMN_WORD1 = "word1";
+ private static final String MAIN_COLUMN_WORD2 = "word2";
+ private static final String MAIN_COLUMN_LOCALE = "locale";
+
+ /** Name of the frequency table in the database */
+ private static final String FREQ_TABLE_NAME = "frequency";
+ private static final String FREQ_COLUMN_ID = BaseColumns._ID;
+ private static final String FREQ_COLUMN_PAIR_ID = "pair_id";
+ private static final String COLUMN_FORGETTING_CURVE_VALUE = "freq";
+
+ /** Locale for which this user history dictionary is storing words */
+ private final String mLocale;
+
+ private final UserHistoryDictionaryBigramList mBigramList =
+ new UserHistoryDictionaryBigramList();
+ private final ReentrantLock mBigramListLock = new ReentrantLock();
+ private final SharedPreferences mPrefs;
+
+ private final static HashMap<String, String> sDictProjectionMap;
+ private final static ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
+ sLangDictCache = new ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>();
+
+ static {
+ sDictProjectionMap = new HashMap<String, String>();
+ sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
+ sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
+ sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
+ sDictProjectionMap.put(MAIN_COLUMN_LOCALE, MAIN_COLUMN_LOCALE);
+
+ sDictProjectionMap.put(FREQ_COLUMN_ID, FREQ_COLUMN_ID);
+ sDictProjectionMap.put(FREQ_COLUMN_PAIR_ID, FREQ_COLUMN_PAIR_ID);
+ sDictProjectionMap.put(COLUMN_FORGETTING_CURVE_VALUE, COLUMN_FORGETTING_CURVE_VALUE);
+ }
+
+ private static DatabaseHelper sOpenHelper = null;
+
+ public void setDatabaseMax(int maxHistoryBigram) {
+ sMaxHistoryBigrams = maxHistoryBigram;
+ }
+
+ public void setDatabaseDelete(int deleteHistoryBigram) {
+ sDeleteHistoryBigrams = deleteHistoryBigram;
+ }
+
+ public synchronized static UserHistoryDictionary getInstance(
+ final Context context, final String locale,
+ final int dictTypeId, final SharedPreferences sp) {
+ if (sLangDictCache.containsKey(locale)) {
+ final SoftReference<UserHistoryDictionary> ref = sLangDictCache.get(locale);
+ final UserHistoryDictionary dict = ref == null ? null : ref.get();
+ if (dict != null) {
+ if (PROFILE_SAVE_RESTORE) {
+ Log.w(TAG, "Use cached UserHistoryDictionary for " + locale);
+ }
+ return dict;
+ }
+ }
+ final UserHistoryDictionary dict =
+ new UserHistoryDictionary(context, locale, dictTypeId, sp);
+ sLangDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
+ return dict;
+ }
+
+ private UserHistoryDictionary(final Context context, final String locale, final int dicTypeId,
+ SharedPreferences sp) {
+ super(context, dicTypeId);
+ mLocale = locale;
+ mPrefs = sp;
+ if (sOpenHelper == null) {
+ sOpenHelper = new DatabaseHelper(getContext());
+ }
+ if (mLocale != null && mLocale.length() > 1) {
+ loadDictionary();
+ }
+ }
+
+ @Override
+ public void close() {
+ flushPendingWrites();
+ // Don't close the database as locale changes will require it to be reopened anyway
+ // Also, the database is written to somewhat frequently, so it needs to be kept alive
+ // throughout the life of the process.
+ // mOpenHelper.close();
+ // Ignore close because we cache UserHistoryDictionary for each language. See getInstance()
+ // above.
+ // super.close();
+ }
+
+ /**
+ * Return whether the passed charsequence is in the dictionary.
+ */
+ @Override
+ public synchronized boolean isValidWord(final CharSequence word) {
+ // TODO: figure out what is the correct thing to do here.
+ return false;
+ }
+
+ /**
+ * Pair will be added to the user history dictionary.
+ *
+ * The first word may be null. That means we don't know the context, in other words,
+ * it's only a unigram. The first word may also be an empty string : this means start
+ * context, as in beginning of a sentence for example.
+ * The second word may not be null (a NullPointerException would be thrown).
+ */
+ public int addToUserHistory(final String word1, String word2, boolean isValid) {
+ if (mBigramListLock.tryLock()) {
+ try {
+ super.addWord(
+ word2, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED);
+ // Do not insert a word as a bigram of itself
+ if (word2.equals(word1)) {
+ return 0;
+ }
+ final int freq;
+ if (null == word1) {
+ freq = FREQUENCY_FOR_TYPED;
+ } else {
+ freq = super.setBigramAndGetFrequency(
+ word1, word2, new ForgettingCurveParams(isValid));
+ }
+ mBigramList.addBigram(word1, word2);
+ return freq;
+ } finally {
+ mBigramListLock.unlock();
+ }
+ }
+ return -1;
+ }
+
+ public boolean cancelAddingUserHistory(String word1, String word2) {
+ if (mBigramListLock.tryLock()) {
+ try {
+ if (mBigramList.removeBigram(word1, word2)) {
+ return super.removeBigram(word1, word2);
+ }
+ } finally {
+ mBigramListLock.unlock();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Schedules a background thread to write any pending words to the database.
+ */
+ private void flushPendingWrites() {
+ if (mBigramListLock.isLocked()) {
+ return;
+ }
+ // Create a background thread to write the pending entries
+ new UpdateDbTask(sOpenHelper, mBigramList, mLocale, this, mPrefs).execute();
+ }
+
+ @Override
+ public void loadDictionaryAsync() {
+ // This must be run on non-main thread
+ mBigramListLock.lock();
+ try {
+ loadDictionaryAsyncLocked();
+ } finally {
+ mBigramListLock.unlock();
+ }
+ }
+
+ private void loadDictionaryAsyncLocked() {
+ if (DBG_STRESS_TEST) {
+ try {
+ Log.w(TAG, "Start stress in loading: " + mLocale);
+ Thread.sleep(15000);
+ Log.w(TAG, "End stress in loading");
+ } catch (InterruptedException e) {
+ }
+ }
+ final long last = SettingsValues.getLastUserHistoryWriteTime(mPrefs, mLocale);
+ final boolean initializing = last == 0;
+ final long now = System.currentTimeMillis();
+ // Load the words that correspond to the current input locale
+ final Cursor cursor = query(MAIN_COLUMN_LOCALE + "=?", new String[] { mLocale });
+ if (null == cursor) return;
+ try {
+ // TODO: Call SQLiteDataBase.beginTransaction / SQLiteDataBase.endTransaction
+ if (cursor.moveToFirst()) {
+ final int word1Index = cursor.getColumnIndex(MAIN_COLUMN_WORD1);
+ final int word2Index = cursor.getColumnIndex(MAIN_COLUMN_WORD2);
+ final int fcIndex = cursor.getColumnIndex(COLUMN_FORGETTING_CURVE_VALUE);
+ while (!cursor.isAfterLast()) {
+ final String word1 = cursor.getString(word1Index);
+ final String word2 = cursor.getString(word2Index);
+ final int fc = cursor.getInt(fcIndex);
+ if (DBG_SAVE_RESTORE) {
+ Log.d(TAG, "--- Load user history: " + word1 + ", " + word2 + ","
+ + mLocale + "," + this);
+ }
+ // Safeguard against adding really long words. Stack may overflow due
+ // to recursive lookup
+ if (null == word1) {
+ super.addWord(word2, null /* shortcut */, fc);
+ } else if (word1.length() < BinaryDictionary.MAX_WORD_LENGTH
+ && word2.length() < BinaryDictionary.MAX_WORD_LENGTH) {
+ super.setBigramAndGetFrequency(
+ word1, word2, initializing ? new ForgettingCurveParams(true)
+ : new ForgettingCurveParams(fc, now, last));
+ }
+ mBigramList.addBigram(word1, word2, (byte)fc);
+ cursor.moveToNext();
+ }
+ }
+ } finally {
+ cursor.close();
+ if (PROFILE_SAVE_RESTORE) {
+ final long diff = System.currentTimeMillis() - now;
+ Log.w(TAG, "PROF: Load User HistoryDictionary: "
+ + mLocale + ", " + diff + "ms.");
+ }
+ }
+ }
+
+ /**
+ * Query the database
+ */
+ private static Cursor query(String selection, String[] selectionArgs) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+ // main INNER JOIN frequency ON (main._id=freq.pair_id)
+ qb.setTables(MAIN_TABLE_NAME + " INNER JOIN " + FREQ_TABLE_NAME + " ON ("
+ + MAIN_TABLE_NAME + "." + MAIN_COLUMN_ID + "=" + FREQ_TABLE_NAME + "."
+ + FREQ_COLUMN_PAIR_ID +")");
+
+ qb.setProjectionMap(sDictProjectionMap);
+
+ // Get the database and run the query
+ try {
+ SQLiteDatabase db = sOpenHelper.getReadableDatabase();
+ Cursor c = qb.query(db,
+ new String[] {
+ MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD2, COLUMN_FORGETTING_CURVE_VALUE },
+ selection, selectionArgs, null, null, null);
+ return c;
+ } catch (android.database.sqlite.SQLiteCantOpenDatabaseException e) {
+ // Can't open the database : presumably we can't access storage. That may happen
+ // when the device is wedged; do a best effort to still start the keyboard.
+ return null;
+ }
+ }
+
+ /**
+ * This class helps open, create, and upgrade the database file.
+ */
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("PRAGMA foreign_keys = ON;");
+ db.execSQL("CREATE TABLE " + MAIN_TABLE_NAME + " ("
+ + MAIN_COLUMN_ID + " INTEGER PRIMARY KEY,"
+ + MAIN_COLUMN_WORD1 + " TEXT,"
+ + MAIN_COLUMN_WORD2 + " TEXT,"
+ + MAIN_COLUMN_LOCALE + " TEXT"
+ + ");");
+ db.execSQL("CREATE TABLE " + FREQ_TABLE_NAME + " ("
+ + FREQ_COLUMN_ID + " INTEGER PRIMARY KEY,"
+ + FREQ_COLUMN_PAIR_ID + " INTEGER,"
+ + COLUMN_FORGETTING_CURVE_VALUE + " INTEGER,"
+ + "FOREIGN KEY(" + FREQ_COLUMN_PAIR_ID + ") REFERENCES " + MAIN_TABLE_NAME
+ + "(" + MAIN_COLUMN_ID + ")" + " ON DELETE CASCADE"
+ + ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS " + MAIN_TABLE_NAME);
+ db.execSQL("DROP TABLE IF EXISTS " + FREQ_TABLE_NAME);
+ onCreate(db);
+ }
+ }
+
+ /**
+ * Async task to write pending words to the database so that it stays in sync with
+ * the in-memory trie.
+ */
+ private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
+ private final UserHistoryDictionaryBigramList mBigramList;
+ private final DatabaseHelper mDbHelper;
+ private final String mLocale;
+ private final UserHistoryDictionary mUserHistoryDictionary;
+ private final SharedPreferences mPrefs;
+
+ public UpdateDbTask(
+ DatabaseHelper openHelper, UserHistoryDictionaryBigramList pendingWrites,
+ String locale, UserHistoryDictionary dict, SharedPreferences prefs) {
+ mBigramList = pendingWrites;
+ mLocale = locale;
+ mDbHelper = openHelper;
+ mUserHistoryDictionary = dict;
+ mPrefs = prefs;
+ }
+
+ /** Prune any old data if the database is getting too big. */
+ private static void checkPruneData(SQLiteDatabase db) {
+ db.execSQL("PRAGMA foreign_keys = ON;");
+ Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID },
+ null, null, null, null, null);
+ try {
+ int totalRowCount = c.getCount();
+ // prune out old data if we have too much data
+ if (totalRowCount > sMaxHistoryBigrams) {
+ int numDeleteRows = (totalRowCount - sMaxHistoryBigrams)
+ + sDeleteHistoryBigrams;
+ int pairIdColumnId = c.getColumnIndex(FREQ_COLUMN_PAIR_ID);
+ c.moveToFirst();
+ int count = 0;
+ while (count < numDeleteRows && !c.isAfterLast()) {
+ String pairId = c.getString(pairIdColumnId);
+ // Deleting from MAIN table will delete the frequencies
+ // due to FOREIGN KEY .. ON DELETE CASCADE
+ db.delete(MAIN_TABLE_NAME, MAIN_COLUMN_ID + "=?",
+ new String[] { pairId });
+ c.moveToNext();
+ count++;
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... v) {
+ SQLiteDatabase db = null;
+ if (mUserHistoryDictionary.mBigramListLock.tryLock()) {
+ try {
+ try {
+ db = mDbHelper.getWritableDatabase();
+ } catch (android.database.sqlite.SQLiteCantOpenDatabaseException e) {
+ // If we can't open the db, don't do anything. Exit through the next test
+ // for non-nullity of the db variable.
+ }
+ if (null == db) {
+ // Not much we can do. Just exit.
+ return null;
+ }
+ db.beginTransaction();
+ return doLoadTaskLocked(db);
+ } finally {
+ if (db != null) {
+ db.endTransaction();
+ }
+ mUserHistoryDictionary.mBigramListLock.unlock();
+ }
+ }
+ return null;
+ }
+
+ private Void doLoadTaskLocked(SQLiteDatabase db) {
+ if (DBG_STRESS_TEST) {
+ try {
+ Log.w(TAG, "Start stress in closing: " + mLocale);
+ Thread.sleep(15000);
+ Log.w(TAG, "End stress in closing");
+ } catch (InterruptedException e) {
+ }
+ }
+ final long now = PROFILE_SAVE_RESTORE ? System.currentTimeMillis() : 0;
+ int profTotal = 0;
+ int profInsert = 0;
+ int profDelete = 0;
+ db.execSQL("PRAGMA foreign_keys = ON;");
+ final boolean addLevel0Bigram = mBigramList.size() <= sMaxHistoryBigrams;
+
+ // Write all the entries to the db
+ for (String word1 : mBigramList.keySet()) {
+ final HashMap<String, Byte> word1Bigrams = mBigramList.getBigrams(word1);
+ for (String word2 : word1Bigrams.keySet()) {
+ if (PROFILE_SAVE_RESTORE) {
+ ++profTotal;
+ }
+ // Get new frequency. Do not insert unigrams/bigrams which freq is "-1".
+ final int freq; // -1, or 0~255
+ if (word1 == null) { // unigram
+ freq = FREQUENCY_FOR_TYPED;
+ final byte prevFc = word1Bigrams.get(word2);
+ if (prevFc == FREQUENCY_FOR_TYPED) {
+ // No need to update since we found no changes for this entry.
+ // Just skip to the next entry.
+ if (DBG_SAVE_RESTORE) {
+ Log.d(TAG, "Skip update user history: " + word1 + "," + word2
+ + "," + prevFc);
+ }
+ if (!DBG_ALWAYS_WRITE) {
+ continue;
+ }
+ }
+ } else { // bigram
+ final NextWord nw = mUserHistoryDictionary.getBigramWord(word1, word2);
+ if (nw != null) {
+ final ForgettingCurveParams fcp = nw.getFcParams();
+ final byte prevFc = word1Bigrams.get(word2);
+ final byte fc = (byte)fcp.getFc();
+ final boolean isValid = fcp.isValid();
+ if (prevFc > 0 && prevFc == fc) {
+ // No need to update since we found no changes for this entry.
+ // Just skip to the next entry.
+ if (DBG_SAVE_RESTORE) {
+ Log.d(TAG, "Skip update user history: " + word1 + ","
+ + word2 + "," + prevFc);
+ }
+ if (!DBG_ALWAYS_WRITE) {
+ continue;
+ } else {
+ freq = fc;
+ }
+ } else if (UserHistoryForgettingCurveUtils.
+ needsToSave(fc, isValid, addLevel0Bigram)) {
+ freq = fc;
+ } else {
+ freq = -1;
+ }
+ } else {
+ freq = -1;
+ }
+ }
+ // TODO: this process of making a text search for each pair each time
+ // is terribly inefficient. Optimize this.
+ // Find pair id
+ Cursor c = null;
+ try {
+ if (null != word1) {
+ c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
+ MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND "
+ + MAIN_COLUMN_LOCALE + "=?",
+ new String[] { word1, word2, mLocale }, null, null,
+ null);
+ } else {
+ c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
+ MAIN_COLUMN_WORD1 + " IS NULL AND " + MAIN_COLUMN_WORD2
+ + "=? AND " + MAIN_COLUMN_LOCALE + "=?",
+ new String[] { word2, mLocale }, null, null, null);
+ }
+
+ final int pairId;
+ if (c.moveToFirst()) {
+ if (PROFILE_SAVE_RESTORE) {
+ ++profDelete;
+ }
+ // Delete existing pair
+ pairId = c.getInt(c.getColumnIndex(MAIN_COLUMN_ID));
+ db.delete(FREQ_TABLE_NAME, FREQ_COLUMN_PAIR_ID + "=?",
+ new String[] { Integer.toString(pairId) });
+ } else {
+ // Create new pair
+ Long pairIdLong = db.insert(MAIN_TABLE_NAME, null,
+ getContentValues(word1, word2, mLocale));
+ pairId = pairIdLong.intValue();
+ }
+ if (freq > 0) {
+ if (PROFILE_SAVE_RESTORE) {
+ ++profInsert;
+ }
+ if (DBG_SAVE_RESTORE) {
+ Log.d(TAG, "--- Save user history: " + word1 + ", " + word2
+ + mLocale + "," + this);
+ }
+ // Insert new frequency
+ db.insert(FREQ_TABLE_NAME, null,
+ getFrequencyContentValues(pairId, freq));
+ // Update an existing bigram entry in mBigramList too in order to
+ // synchronize the SQL DB and mBigramList.
+ mBigramList.updateBigram(word1, word2, (byte)freq);
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+ }
+
+ checkPruneData(db);
+ // Save the timestamp after we finish writing the SQL DB.
+ SettingsValues.setLastUserHistoryWriteTime(mPrefs, mLocale);
+ if (PROFILE_SAVE_RESTORE) {
+ final long diff = System.currentTimeMillis() - now;
+ Log.w(TAG, "PROF: Write User HistoryDictionary: " + mLocale + ", "+ diff
+ + "ms. Total: " + profTotal + ". Insert: " + profInsert + ". Delete: "
+ + profDelete);
+ }
+ db.setTransactionSuccessful();
+ return null;
+ }
+
+ private static ContentValues getContentValues(String word1, String word2, String locale) {
+ ContentValues values = new ContentValues(3);
+ values.put(MAIN_COLUMN_WORD1, word1);
+ values.put(MAIN_COLUMN_WORD2, word2);
+ values.put(MAIN_COLUMN_LOCALE, locale);
+ return values;
+ }
+
+ private static ContentValues getFrequencyContentValues(int pairId, int frequency) {
+ ContentValues values = new ContentValues(2);
+ values.put(FREQ_COLUMN_PAIR_ID, pairId);
+ values.put(COLUMN_FORGETTING_CURVE_VALUE, frequency);
+ return values;
+ }
+ }
+
+}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
new file mode 100644
index 000000000..28847745e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Set;
+
+/**
+ * A store of bigrams which will be updated when the user history dictionary is closed
+ * All bigrams including stale ones in SQL DB should be stored in this class to avoid adding stale
+ * bigrams when we write to the SQL DB.
+ */
+public class UserHistoryDictionaryBigramList {
+ public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0;
+ private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName();
+ private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = new HashMap<String, Byte>();
+ private final HashMap<String, HashMap<String, Byte>> mBigramMap =
+ new HashMap<String, HashMap<String, Byte>>();
+ private int mSize = 0;
+
+ public void evictAll() {
+ mSize = 0;
+ mBigramMap.clear();
+ }
+
+ /**
+ * Called when the user typed a word.
+ */
+ public void addBigram(String word1, String word2) {
+ addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE);
+ }
+
+ /**
+ * Called when loaded from the SQL DB.
+ */
+ public void addBigram(String word1, String word2, byte fcValue) {
+ if (UserHistoryDictionary.DBG_SAVE_RESTORE) {
+ Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue);
+ }
+ final HashMap<String, Byte> map;
+ if (mBigramMap.containsKey(word1)) {
+ map = mBigramMap.get(word1);
+ } else {
+ map = new HashMap<String, Byte>();
+ mBigramMap.put(word1, map);
+ }
+ if (!map.containsKey(word2)) {
+ ++mSize;
+ map.put(word2, fcValue);
+ }
+ }
+
+ /**
+ * Called when inserted to the SQL DB.
+ */
+ public void updateBigram(String word1, String word2, byte fcValue) {
+ if (UserHistoryDictionary.DBG_SAVE_RESTORE) {
+ Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue);
+ }
+ final HashMap<String, Byte> map;
+ if (mBigramMap.containsKey(word1)) {
+ map = mBigramMap.get(word1);
+ } else {
+ return;
+ }
+ if (!map.containsKey(word2)) {
+ return;
+ }
+ map.put(word2, fcValue);
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ public boolean isEmpty() {
+ return mBigramMap.isEmpty();
+ }
+
+ public Set<String> keySet() {
+ return mBigramMap.keySet();
+ }
+
+ public HashMap<String, Byte> getBigrams(String word1) {
+ if (!mBigramMap.containsKey(word1)) {
+ return EMPTY_BIGRAM_MAP;
+ } else {
+ return mBigramMap.get(word1);
+ }
+ }
+
+ public boolean removeBigram(String word1, String word2) {
+ final HashMap<String, Byte> set = getBigrams(word1);
+ if (set.isEmpty()) {
+ return false;
+ }
+ if (set.containsKey(word2)) {
+ set.remove(word2);
+ --mSize;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
new file mode 100644
index 000000000..1de95d7b8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -0,0 +1,222 @@
+/*
+ * 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;
+
+import android.text.format.DateUtils;
+import android.util.Log;
+
+public class UserHistoryForgettingCurveUtils {
+ private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final int FC_FREQ_MAX = 127;
+ /* package */ static final int COUNT_MAX = 3;
+ private static final int FC_LEVEL_MAX = 3;
+ /* package */ static final int ELAPSED_TIME_MAX = 15;
+ private static final int ELAPSED_TIME_INTERVAL_HOURS = 6;
+ private static final long ELAPSED_TIME_INTERVAL_MILLIS = ELAPSED_TIME_INTERVAL_HOURS
+ * DateUtils.HOUR_IN_MILLIS;
+ private static final int HALF_LIFE_HOURS = 48;
+ private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1);
+
+ private UserHistoryForgettingCurveUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static class ForgettingCurveParams {
+ private byte mFc;
+ long mLastTouchedTime = 0;
+ private final boolean mIsValid;
+
+ private void updateLastTouchedTime() {
+ mLastTouchedTime = System.currentTimeMillis();
+ }
+
+ public ForgettingCurveParams(boolean isValid) {
+ this(System.currentTimeMillis(), isValid);
+ }
+
+ private ForgettingCurveParams(long now, boolean isValid) {
+ this(pushCount((byte)0, isValid), now, now, isValid);
+ }
+
+ /** This constructor is called when the user history bigram dictionary is being restored. */
+ public ForgettingCurveParams(int fc, long now, long last) {
+ // All words with level >= 1 had been saved.
+ // Invalid words with level == 0 had been saved.
+ // Valid words words with level == 0 had *not* been saved.
+ this(fc, now, last, fcToLevel((byte)fc) > 0);
+ }
+
+ private ForgettingCurveParams(int fc, long now, long last, boolean isValid) {
+ mIsValid = isValid;
+ mFc = (byte)fc;
+ mLastTouchedTime = last;
+ updateElapsedTime(now);
+ }
+
+ public boolean isValid() {
+ return mIsValid;
+ }
+
+ public byte getFc() {
+ updateElapsedTime(System.currentTimeMillis());
+ return mFc;
+ }
+
+ public int getFrequency() {
+ updateElapsedTime(System.currentTimeMillis());
+ return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
+ }
+
+ public int notifyTypedAgainAndGetFrequency() {
+ updateLastTouchedTime();
+ // TODO: Check whether this word is valid or not
+ mFc = pushCount(mFc, false);
+ return UserHistoryForgettingCurveUtils.fcToFreq(mFc);
+ }
+
+ private void updateElapsedTime(long now) {
+ final int elapsedTimeCount =
+ (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS);
+ if (elapsedTimeCount <= 0) {
+ return;
+ }
+ if (elapsedTimeCount >= MAX_PUSH_ELAPSED) {
+ mLastTouchedTime = now;
+ mFc = 0;
+ return;
+ }
+ for (int i = 0; i < elapsedTimeCount; ++i) {
+ mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS;
+ mFc = pushElapsedTime(mFc);
+ }
+ }
+ }
+
+ /* package */ static int fcToElapsedTime(byte fc) {
+ return fc & 0x0F;
+ }
+
+ /* package */ static int fcToCount(byte fc) {
+ return (fc >> 4) & 0x03;
+ }
+
+ /* package */ static int fcToLevel(byte fc) {
+ return (fc >> 6) & 0x03;
+ }
+
+ private static int calcFreq(int elapsedTime, int count, int level) {
+ if (level <= 0) {
+ // Reserved words, just return -1
+ return -1;
+ }
+ if (count == COUNT_MAX) {
+ // Temporary promote because it's frequently typed recently
+ ++level;
+ }
+ final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
+ final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
+ return MathUtils.SCORE_TABLE[l - 1][et];
+ }
+
+ /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) {
+ final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime));
+ final int c = Math.min(COUNT_MAX, Math.max(0, count));
+ final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level));
+ return (byte)(et | (c << 4) | (l << 6));
+ }
+
+ public static int fcToFreq(byte fc) {
+ final int elapsedTime = fcToElapsedTime(fc);
+ final int count = fcToCount(fc);
+ final int level = fcToLevel(fc);
+ return calcFreq(elapsedTime, count, level);
+ }
+
+ public static byte pushElapsedTime(byte fc) {
+ int elapsedTime = fcToElapsedTime(fc);
+ int count = fcToCount(fc);
+ int level = fcToLevel(fc);
+ if (elapsedTime >= ELAPSED_TIME_MAX) {
+ // Downgrade level
+ elapsedTime = 0;
+ count = COUNT_MAX;
+ --level;
+ } else {
+ ++elapsedTime;
+ }
+ return calcFc(elapsedTime, count, level);
+ }
+
+ public static byte pushCount(byte fc, boolean isValid) {
+ final int elapsedTime = fcToElapsedTime(fc);
+ int count = fcToCount(fc);
+ int level = fcToLevel(fc);
+ if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) {
+ // Upgrade level
+ ++level;
+ count = 0;
+ if (DEBUG) {
+ Log.d(TAG, "Upgrade level.");
+ }
+ } else {
+ ++count;
+ }
+ return calcFc(0, count, level);
+ }
+
+ // TODO: isValid should be false for a word whose frequency is 0,
+ // or that is not in the dictionary.
+ /**
+ * Check wheather we should save the bigram to the SQL DB or not
+ */
+ public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) {
+ int level = fcToLevel(fc);
+ if (level == 0) {
+ if (isValid || !addLevel0Bigram) {
+ return false;
+ }
+ }
+ final int elapsedTime = fcToElapsedTime(fc);
+ return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0);
+ }
+
+ private static class MathUtils {
+ public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
+ static {
+ for (int i = 0; i < FC_LEVEL_MAX; ++i) {
+ final float initialFreq;
+ if (i >= 2) {
+ initialFreq = FC_FREQ_MAX;
+ } else if (i == 1) {
+ initialFreq = FC_FREQ_MAX / 2;
+ } else if (i == 0) {
+ initialFreq = FC_FREQ_MAX / 4;
+ } else {
+ continue;
+ }
+ for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
+ final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+ final float freq = initialFreq
+ * NativeUtils.powf(initialFreq, elapsedHours / HALF_LIFE_HOURS);
+ final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
+ SCORE_TABLE[i][j] = intFreq;
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 245fc20bc..a44b1f9ad 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -16,48 +16,41 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
-import com.android.inputmethod.compat.InputTypeCompatUtils;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardId;
-
import android.content.Context;
-import android.content.res.Configuration;
+import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
-import android.text.InputType;
+import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
-import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.io.BufferedReader;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
+import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
public class Utils {
- private static final String TAG = Utils.class.getSimpleName();
- private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
- private static boolean DBG = LatinImeLogger.sDBG;
- private static boolean DBG_EDIT_DISTANCE = false;
-
private Utils() {
- // Intentional empty constructor for utility class.
+ // This utility class is not publicly instantiable.
}
/**
@@ -111,85 +104,6 @@ public class Utils {
}
}
- public static boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManagerCompatWrapper imm) {
- final List<InputMethodInfoCompatWrapper> enabledImis = imm.getEnabledInputMethodList();
-
- // Filters out IMEs that have auxiliary subtypes only (including either implicitly or
- // explicitly enabled ones).
- final ArrayList<InputMethodInfoCompatWrapper> filteredImis =
- new ArrayList<InputMethodInfoCompatWrapper>();
-
- outerloop:
- for (InputMethodInfoCompatWrapper imi : enabledImis) {
- // We can return true immediately after we find two or more filtered IMEs.
- if (filteredImis.size() > 1) return true;
- final List<InputMethodSubtypeCompatWrapper> subtypes =
- imm.getEnabledInputMethodSubtypeList(imi, true);
- // IMEs that have no subtypes should be included.
- if (subtypes.isEmpty()) {
- filteredImis.add(imi);
- continue;
- }
- // IMEs that have one or more non-auxiliary subtypes should be included.
- for (InputMethodSubtypeCompatWrapper subtype : subtypes) {
- if (!subtype.isAuxiliary()) {
- filteredImis.add(imi);
- continue outerloop;
- }
- }
- }
-
- return filteredImis.size() > 1
- // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled
- // input method subtype (The current IME should be LatinIME.)
- || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
- }
-
- public static String getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName) {
- return getInputMethodInfo(imm, packageName).getId();
- }
-
- public static InputMethodInfoCompatWrapper getInputMethodInfo(
- InputMethodManagerCompatWrapper imm, String packageName) {
- for (final InputMethodInfoCompatWrapper imi : imm.getEnabledInputMethodList()) {
- if (imi.getPackageName().equals(packageName))
- return imi;
- }
- throw new RuntimeException("Can not find input method id for " + packageName);
- }
-
- public static boolean shouldBlockedBySafetyNetForAutoCorrection(SuggestedWords suggestions,
- Suggest suggest) {
- // Safety net for auto correction.
- // Actually if we hit this safety net, it's actually a bug.
- if (suggestions.size() <= 1 || suggestions.mTypedWordValid) return false;
- // If user selected aggressive auto correction mode, there is no need to use the safety
- // net.
- if (suggest.isAggressiveAutoCorrectionMode()) return false;
- CharSequence typedWord = suggestions.getWord(0);
- // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
- // we should not use net because relatively edit distance can be big.
- if (typedWord.length() < MINIMUM_SAFETY_NET_CHAR_LENGTH) return false;
- CharSequence candidateWord = suggestions.getWord(1);
- final int typedWordLength = typedWord.length();
- final int maxEditDistanceOfNativeDictionary = typedWordLength < 5 ? 2 : typedWordLength / 2;
- final int distance = Utils.editDistance(typedWord, candidateWord);
- if (DBG) {
- Log.d(TAG, "Autocorrected edit distance = " + distance
- + ", " + maxEditDistanceOfNativeDictionary);
- }
- if (distance > maxEditDistanceOfNativeDictionary) {
- if (DBG) {
- Log.d(TAG, "Safety net: before = " + typedWord + ", after = " + candidateWord);
- Log.w(TAG, "(Error) The edit distance of this correction exceeds limit. "
- + "Turning off auto-correction.");
- }
- return true;
- } else {
- return false;
- }
- }
-
/* package */ static class RingCharBuffer {
private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
@@ -197,7 +111,6 @@ public class Utils {
/* package */ static final int BUFSIZE = 20;
private InputMethodService mContext;
private boolean mEnabled = false;
- private boolean mUsabilityStudy = false;
private int mEnd = 0;
/* package */ int mLength = 0;
private char[] mCharBuf = new char[BUFSIZE];
@@ -212,21 +125,19 @@ public class Utils {
}
public static RingCharBuffer init(InputMethodService context, boolean enabled,
boolean usabilityStudy) {
+ if (!(enabled || usabilityStudy)) return null;
sRingCharBuffer.mContext = context;
- sRingCharBuffer.mEnabled = enabled || usabilityStudy;
- sRingCharBuffer.mUsabilityStudy = usabilityStudy;
+ sRingCharBuffer.mEnabled = true;
UsabilityStudyLogUtils.getInstance().init(context);
return sRingCharBuffer;
}
- private int normalize(int in) {
+ private static int normalize(int in) {
int ret = in % BUFSIZE;
return ret < 0 ? ret + BUFSIZE : ret;
}
+ // TODO: accept code points
public void push(char c, int x, int y) {
if (!mEnabled) return;
- if (mUsabilityStudy) {
- UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
- }
mCharBuf[mEnd] = c;
mXBuf[mEnd] = x;
mYBuf[mEnd] = y;
@@ -293,116 +204,29 @@ public class Utils {
}
}
-
- /* Damerau-Levenshtein distance */
- public static int editDistance(CharSequence s, CharSequence t) {
- if (s == null || t == null) {
- throw new IllegalArgumentException("editDistance: Arguments should not be null.");
- }
- final int sl = s.length();
- final int tl = t.length();
- int[][] dp = new int [sl + 1][tl + 1];
- for (int i = 0; i <= sl; i++) {
- dp[i][0] = i;
- }
- for (int j = 0; j <= tl; j++) {
- dp[0][j] = j;
- }
- for (int i = 0; i < sl; ++i) {
- for (int j = 0; j < tl; ++j) {
- final char sc = Character.toLowerCase(s.charAt(i));
- final char tc = Character.toLowerCase(t.charAt(j));
- final int cost = sc == tc ? 0 : 1;
- dp[i + 1][j + 1] = Math.min(
- dp[i][j + 1] + 1, Math.min(dp[i + 1][j] + 1, dp[i][j] + cost));
- // Overwrite for transposition cases
- if (i > 0 && j > 0
- && sc == Character.toLowerCase(t.charAt(j - 1))
- && tc == Character.toLowerCase(s.charAt(i - 1))) {
- dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i - 1][j - 1] + cost);
- }
- }
- }
- if (DBG_EDIT_DISTANCE) {
- Log.d(TAG, "editDistance:" + s + "," + t);
- for (int i = 0; i < dp.length; ++i) {
- StringBuffer sb = new StringBuffer();
- for (int j = 0; j < dp[i].length; ++j) {
- sb.append(dp[i][j]).append(',');
- }
- Log.d(TAG, i + ":" + sb.toString());
- }
- }
- return dp[sl][tl];
- }
-
// Get the current stack trace
- public static String getStackTrace() {
+ public static String getStackTrace(final int limit) {
StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
} catch (RuntimeException e) {
StackTraceElement[] frames = e.getStackTrace();
// Start at 1 because the first frame is here and we don't care about it
- for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
+ for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+ sb.append(frames[j].toString() + "\n");
+ }
}
return sb.toString();
}
- // In dictionary.cpp, getSuggestion() method,
- // suggestion scores are computed using the below formula.
- // original score
- // := pow(mTypedLetterMultiplier (this is defined 2),
- // (the number of matched characters between typed word and suggested word))
- // * (individual word's score which defined in the unigram dictionary,
- // and this score is defined in range [0, 255].)
- // Then, the following processing is applied.
- // - If the dictionary word is matched up to the point of the user entry
- // (full match up to min(before.length(), after.length())
- // => Then multiply by FULL_MATCHED_WORDS_PROMOTION_RATE (this is defined 1.2)
- // - If the word is a true full match except for differences in accents or
- // capitalization, then treat it as if the score was 255.
- // - If before.length() == after.length()
- // => multiply by mFullWordMultiplier (this is defined 2))
- // So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2
- // For historical reasons we ignore the 1.2 modifier (because the measure for a good
- // autocorrection threshold was done at a time when it didn't exist). This doesn't change
- // the result.
- // So, we can normalize original score by dividing pow(2, min(b.l(),a.l())) * 255 * 2.
- private static final int MAX_INITIAL_SCORE = 255;
- private static final int TYPED_LETTER_MULTIPLIER = 2;
- private static final int FULL_WORD_MULTIPLIER = 2;
- private static final int S_INT_MAX = 2147483647;
- public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) {
- final int beforeLength = before.length();
- final int afterLength = after.length();
- if (beforeLength == 0 || afterLength == 0) return 0;
- final int distance = editDistance(before, after);
- // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character
- // correction.
- int spaceCount = 0;
- for (int i = 0; i < afterLength; ++i) {
- if (after.charAt(i) == Keyboard.CODE_SPACE) {
- ++spaceCount;
- }
- }
- if (spaceCount == afterLength) return 0;
- final double maximumScore = score == S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE
- * Math.pow(
- TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength - spaceCount))
- * FULL_WORD_MULTIPLIER;
- // add a weight based on edit distance.
- // distance <= max(afterLength, beforeLength) == afterLength,
- // so, 0 <= distance / afterLength <= 1
- final double weight = 1.0 - (double) distance / afterLength;
- return (score / maximumScore) * weight;
+ public static String getStackTrace() {
+ return getStackTrace(Integer.MAX_VALUE);
}
public static class UsabilityStudyLogUtils {
+ // TODO: remove code duplication with ResearchLog class
private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
private static final String FILENAME = "log.txt";
- private static final UsabilityStudyLogUtils sInstance =
- new UsabilityStudyLogUtils();
private final Handler mLoggingHandler;
private File mFile;
private File mDirectory;
@@ -413,7 +237,7 @@ public class Utils {
private UsabilityStudyLogUtils() {
mDate = new Date();
- mDateFormat = new SimpleDateFormat("dd MMM HH:mm:ss.SSS");
+ mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ");
HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
Process.THREAD_PRIORITY_BACKGROUND);
@@ -421,8 +245,13 @@ public class Utils {
mLoggingHandler = new Handler(handlerThread.getLooper());
}
+ // Initialization-on-demand holder
+ private static class OnDemandInitializationHolder {
+ public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
+ }
+
public static UsabilityStudyLogUtils getInstance() {
- return sInstance;
+ return OnDemandInitializationHolder.sInstance;
}
public void init(InputMethodService ims) {
@@ -441,8 +270,8 @@ public class Utils {
}
}
- public void writeBackSpace() {
- UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0");
+ public static void writeBackSpace(int x, int y) {
+ UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
}
public void writeChar(char c, int x, int y) {
@@ -480,32 +309,89 @@ public class Utils {
});
}
- public void printAll() {
+ private synchronized String getBufferedLogs() {
+ mWriter.flush();
+ StringBuilder sb = new StringBuilder();
+ BufferedReader br = getBufferedReader();
+ String line;
+ try {
+ while ((line = br.readLine()) != null) {
+ sb.append('\n');
+ sb.append(line);
+ }
+ } catch (IOException e) {
+ Log.e(USABILITY_TAG, "Can't read log file.");
+ } finally {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
+ }
+ try {
+ br.close();
+ } catch (IOException e) {
+ // ignore.
+ }
+ }
+ return sb.toString();
+ }
+
+ public void emailResearcherLogsAll() {
mLoggingHandler.post(new Runnable() {
@Override
public void run() {
+ final Date date = new Date();
+ date.setTime(System.currentTimeMillis());
+ final String currentDateTimeString =
+ new SimpleDateFormat("yyyyMMdd-HHmmssZ").format(date);
+ if (mFile == null) {
+ Log.w(USABILITY_TAG, "No internal log file found.");
+ return;
+ }
+ if (mIms.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
+ return;
+ }
mWriter.flush();
- StringBuilder sb = new StringBuilder();
- BufferedReader br = getBufferedReader();
- String line;
+ final String destPath = Environment.getExternalStorageDirectory()
+ + "/research-" + currentDateTimeString + ".log";
+ final File destFile = new File(destPath);
try {
- while ((line = br.readLine()) != null) {
- sb.append('\n');
- sb.append(line);
- }
- } catch (IOException e) {
- Log.e(USABILITY_TAG, "Can't read log file.");
- } finally {
- if (LatinImeLogger.sDBG) {
- Log.d(USABILITY_TAG, "output all logs\n" + sb.toString());
- }
- mIms.getCurrentInputConnection().commitText(sb.toString(), 0);
- try {
- br.close();
- } catch (IOException e) {
- // ignore.
- }
+ final FileChannel src = (new FileInputStream(mFile)).getChannel();
+ final FileChannel dest = (new FileOutputStream(destFile)).getChannel();
+ src.transferTo(0, src.size(), dest);
+ src.close();
+ dest.close();
+ } catch (FileNotFoundException e1) {
+ Log.w(USABILITY_TAG, e1);
+ return;
+ } catch (IOException e2) {
+ Log.w(USABILITY_TAG, e2);
+ return;
+ }
+ if (destFile == null || !destFile.exists()) {
+ Log.w(USABILITY_TAG, "Dest file doesn't exist.");
+ return;
+ }
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
}
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
+ intent.putExtra(Intent.EXTRA_SUBJECT,
+ "[Research Logs] " + currentDateTimeString);
+ mIms.startActivity(intent);
+ }
+ });
+ }
+
+ public void printAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
}
});
}
@@ -546,173 +432,113 @@ public class Utils {
}
}
- public static int getKeyboardMode(EditorInfo attribute) {
- if (attribute == null)
- return KeyboardId.MODE_TEXT;
-
- final int inputType = attribute.inputType;
- final int variation = inputType & InputType.TYPE_MASK_VARIATION;
-
- switch (inputType & InputType.TYPE_MASK_CLASS) {
- case InputType.TYPE_CLASS_NUMBER:
- case InputType.TYPE_CLASS_DATETIME:
- return KeyboardId.MODE_NUMBER;
- case InputType.TYPE_CLASS_PHONE:
- return KeyboardId.MODE_PHONE;
- case InputType.TYPE_CLASS_TEXT:
- if (InputTypeCompatUtils.isEmailVariation(variation)) {
- return KeyboardId.MODE_EMAIL;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
- return KeyboardId.MODE_URL;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
- return KeyboardId.MODE_IM;
- } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- return KeyboardId.MODE_TEXT;
- } else {
- return KeyboardId.MODE_TEXT;
- }
- default:
- return KeyboardId.MODE_TEXT;
- }
- }
-
- public static boolean containsInCsv(String key, String csv) {
- if (csv == null)
- return false;
- for (String option : csv.split(",")) {
- if (option.equals(key))
- return true;
- }
- return false;
- }
-
- public static boolean inPrivateImeOptions(String packageName, String key,
- EditorInfo attribute) {
- if (attribute == null)
- return false;
- return containsInCsv(packageName != null ? packageName + "." + key : key,
- attribute.privateImeOptions);
+ public static float getDipScale(Context context) {
+ final float scale = context.getResources().getDisplayMetrics().density;
+ return scale;
}
- /**
- * Returns a main dictionary resource id
- * @return main dictionary resource id
- */
- public static int getMainDictionaryResourceId(Resources res) {
- final String MAIN_DIC_NAME = "main";
- String packageName = LatinIME.class.getPackage().getName();
- return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName);
+ /** Convert pixel to DIP */
+ public static int dipToPixel(float scale, int dip) {
+ return (int) (dip * scale + 0.5);
}
- public static void loadNativeLibrary() {
- try {
- System.loadLibrary("jni_latinime2");
- } catch (UnsatisfiedLinkError ule) {
- Log.e(TAG, "Could not load native library jni_latinime2");
+ public static class Stats {
+ public static void onNonSeparator(final char code, final int x,
+ final int y) {
+ RingCharBuffer.getInstance().push(code, x, y);
+ LatinImeLogger.logOnInputChar();
}
- }
- /**
- * Returns true if a and b are equal ignoring the case of the character.
- * @param a first character to check
- * @param b second character to check
- * @return {@code true} if a and b are equal, {@code false} otherwise.
- */
- public static boolean equalsIgnoreCase(char a, char b) {
- // Some language, such as Turkish, need testing both cases.
- return a == b
- || Character.toLowerCase(a) == Character.toLowerCase(b)
- || Character.toUpperCase(a) == Character.toUpperCase(b);
- }
+ public static void onSeparator(final int code, final int x,
+ final int y) {
+ // TODO: accept code points
+ RingCharBuffer.getInstance().push((char)code, x, y);
+ LatinImeLogger.logOnInputSeparator();
+ }
- /**
- * Returns true if a and b are equal ignoring the case of the characters, including if they are
- * both null.
- * @param a first CharSequence to check
- * @param b second CharSequence to check
- * @return {@code true} if a and b are equal, {@code false} otherwise.
- */
- public static boolean equalsIgnoreCase(CharSequence a, CharSequence b) {
- if (a == b)
- return true; // including both a and b are null.
- if (a == null || b == null)
- return false;
- final int length = a.length();
- if (length != b.length())
- return false;
- for (int i = 0; i < length; i++) {
- if (!equalsIgnoreCase(a.charAt(i), b.charAt(i)))
- return false;
+ public static void onAutoCorrection(final String typedWord, final String correctedWord,
+ final int separatorCode) {
+ if (TextUtils.isEmpty(typedWord)) return;
+ LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode);
}
- return true;
- }
- /**
- * Returns true if a and b are equal ignoring the case of the characters, including if a is null
- * and b is zero length.
- * @param a CharSequence to check
- * @param b character array to check
- * @param offset start offset of array b
- * @param length length of characters in array b
- * @return {@code true} if a and b are equal, {@code false} otherwise.
- * @throws IndexOutOfBoundsException
- * if {@code offset < 0 || length < 0 || offset + length > data.length}.
- * @throws NullPointerException if {@code b == null}.
- */
- public static boolean equalsIgnoreCase(CharSequence a, char[] b, int offset, int length) {
- if (offset < 0 || length < 0 || length > b.length - offset)
- throw new IndexOutOfBoundsException("array.length=" + b.length + " offset=" + offset
- + " length=" + length);
- if (a == null)
- return length == 0; // including a is null and b is zero length.
- if (a.length() != length)
- return false;
- for (int i = 0; i < length; i++) {
- if (!equalsIgnoreCase(a.charAt(i), b[offset + i]))
- return false;
+ public static void onAutoCorrectionCancellation() {
+ LatinImeLogger.logOnAutoCorrectionCancelled();
}
- return true;
}
- public static float getDipScale(Context context) {
- final float scale = context.getResources().getDisplayMetrics().density;
- return scale;
+ public static String getDebugInfo(final SuggestedWords suggestions, final int pos) {
+ if (!LatinImeLogger.sDBG) return null;
+ final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
+ if (wordInfo == null) return null;
+ final String info = wordInfo.getDebugString();
+ if (TextUtils.isEmpty(info)) return null;
+ return info;
}
- /** Convert pixel to DIP */
- public static int dipToPixel(float scale, int dip) {
- return (int) (dip * scale + 0.5);
+ private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
+ private static final HashMap<String, String> sDeviceOverrideValueMap =
+ new HashMap<String, String>();
+
+ public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
+ final int orientation = res.getConfiguration().orientation;
+ final String key = overrideResId + "-" + orientation;
+ if (!sDeviceOverrideValueMap.containsKey(key)) {
+ String overrideValue = defValue;
+ for (final String element : res.getStringArray(overrideResId)) {
+ if (element.startsWith(HARDWARE_PREFIX)) {
+ overrideValue = element.substring(HARDWARE_PREFIX.length());
+ break;
+ }
+ }
+ sDeviceOverrideValueMap.put(key, overrideValue);
+ }
+ return sDeviceOverrideValueMap.get(key);
}
- public static Locale setSystemLocale(Resources res, Locale newLocale) {
- final Configuration conf = res.getConfiguration();
- final Locale saveLocale = conf.locale;
- conf.locale = newLocale;
- res.updateConfiguration(conf, res.getDisplayMetrics());
- return saveLocale;
+ private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>();
+ private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
+ public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
+ final int N = ss.length;
+ if (N < 2 || N % 2 != 0) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final HashMap<String, Long> retval = new HashMap<String, Long>();
+ for (int i = 0; i < N / 2; ++i) {
+ final String localeStr = ss[i * 2];
+ final long time = Long.valueOf(ss[i * 2 + 1]);
+ retval.put(localeStr, time);
+ }
+ return retval;
}
- private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
-
- public static Locale constructLocaleFromString(String localeStr) {
- if (localeStr == null)
- return null;
- synchronized (sLocaleCache) {
- if (sLocaleCache.containsKey(localeStr))
- return sLocaleCache.get(localeStr);
- Locale retval = null;
- String[] localeParams = localeStr.split("_", 3);
- if (localeParams.length == 1) {
- retval = new Locale(localeParams[0]);
- } else if (localeParams.length == 2) {
- retval = new Locale(localeParams[0], localeParams[1]);
- } else if (localeParams.length == 3) {
- retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
- }
- if (retval != null) {
- sLocaleCache.put(localeStr, retval);
+ public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
+ if (map == null || map.isEmpty()) {
+ return "";
+ }
+ final StringBuilder builder = new StringBuilder();
+ for (String localeStr : map.keySet()) {
+ if (builder.length() > 0) {
+ builder.append(LOCALE_AND_TIME_STR_SEPARATER);
}
- return retval;
+ final Long time = map.get(localeStr);
+ builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
+ builder.append(String.valueOf(time));
+ }
+ return builder.toString();
+ }
+
+ public static void addAllSuggestions(final int dicTypeId, final int dataType,
+ final ArrayList<SuggestedWords.SuggestedWordInfo> suggestions,
+ final Dictionary.WordCallback callback) {
+ for (SuggestedWordInfo suggestion : suggestions) {
+ final String suggestionStr = suggestion.mWord.toString();
+ callback.addWord(suggestionStr.toCharArray(), 0, suggestionStr.length(),
+ suggestion.mScore, dicTypeId, dataType);
}
}
}
diff --git a/java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java b/java/src/com/android/inputmethod/latin/VibratorUtils.java
index 8e2a2e0b8..33ffdd9c9 100644
--- a/java/src/com/android/inputmethod/compat/VibratorCompatWrapper.java
+++ b/java/src/com/android/inputmethod/latin/VibratorUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * 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.
@@ -14,34 +14,37 @@
* limitations under the License.
*/
-package com.android.inputmethod.compat;
+package com.android.inputmethod.latin;
import android.content.Context;
import android.os.Vibrator;
-import java.lang.reflect.Method;
-
-public class VibratorCompatWrapper {
- private static final Method METHOD_hasVibrator = CompatUtils.getMethod(Vibrator.class,
- "hasVibrator", int.class);
-
- private static final VibratorCompatWrapper sInstance = new VibratorCompatWrapper();
+public class VibratorUtils {
+ private static final VibratorUtils sInstance = new VibratorUtils();
private Vibrator mVibrator;
- private VibratorCompatWrapper() {
+ private VibratorUtils() {
+ // This utility class is not publicly instantiable.
}
- public static VibratorCompatWrapper getInstance(Context context) {
+ public static VibratorUtils getInstance(Context context) {
if (sInstance.mVibrator == null) {
- sInstance.mVibrator =
- (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ sInstance.mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
return sInstance;
}
public boolean hasVibrator() {
- if (mVibrator == null)
+ if (mVibrator == null) {
return false;
- return (Boolean) CompatUtils.invoke(mVibrator, true, METHOD_hasVibrator);
+ }
+ return mVibrator.hasVibrator();
+ }
+
+ public void vibrate(long milliseconds) {
+ if (mVibrator == null) {
+ return;
+ }
+ mVibrator.vibrate(milliseconds);
}
}
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
index 4377373d2..a0de2f970 100644
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
@@ -17,13 +17,17 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+
import java.util.HashMap;
+import java.util.Locale;
-public class WhitelistDictionary extends Dictionary {
+public class WhitelistDictionary extends ExpandableDictionary {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = WhitelistDictionary.class.getSimpleName();
@@ -31,22 +35,18 @@ public class WhitelistDictionary extends Dictionary {
private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
new HashMap<String, Pair<Integer, String>>();
- private static final WhitelistDictionary sInstance = new WhitelistDictionary();
-
- private WhitelistDictionary() {
- }
-
- public static WhitelistDictionary init(Context context) {
- synchronized (sInstance) {
- if (context != null) {
- // Wordlist is initialized by the proper language in Suggestion.java#init
- sInstance.initWordlist(
- context.getResources().getStringArray(R.array.wordlist_whitelist));
- } else {
- sInstance.mWhitelistWords.clear();
+ // TODO: Conform to the async load contact of ExpandableDictionary
+ public WhitelistDictionary(final Context context, final Locale locale) {
+ super(context, Suggest.DIC_WHITELIST);
+ // TODO: Move whitelist dictionary into main dictionary.
+ final RunInLocale<Void> job = new RunInLocale<Void>() {
+ @Override
+ protected Void job(Resources res) {
+ initWordlist(res.getStringArray(R.array.wordlist_whitelist));
+ return null;
}
- }
- return sInstance;
+ };
+ job.runInLocale(context.getResources(), locale);
}
private void initWordlist(String[] wordlist) {
@@ -66,6 +66,7 @@ public class WhitelistDictionary extends Dictionary {
if (before != null && after != null) {
mWhitelistWords.put(
before.toLowerCase(), new Pair<Integer, String>(score, after));
+ addWord(after, null /* shortcut */, score);
}
}
} catch (NumberFormatException e) {
@@ -75,26 +76,34 @@ public class WhitelistDictionary extends Dictionary {
}
}
- public String getWhiteListedWord(String before) {
+ public String getWhitelistedWord(String before) {
if (before == null) return null;
final String lowerCaseBefore = before.toLowerCase();
if(mWhitelistWords.containsKey(lowerCaseBefore)) {
if (DBG) {
- Log.d(TAG, "--- found whiteListedWord: " + lowerCaseBefore);
+ Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore);
}
return mWhitelistWords.get(lowerCaseBefore).second;
}
return null;
}
- // Not used for WhitelistDictionary. We use getWhitelistedWord() in Suggest.java instead
- @Override
- public void getWords(WordComposer composer, WordCallback callback) {
- }
-
- @Override
- public boolean isValidWord(CharSequence word) {
+ // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist
+ // lists that word a should autocorrect to word b, and word c would autocorrect to
+ // an upper-cased version of a. In this case, the way this return value is used would
+ // remove the first candidate when the user typed the upper-cased version of A.
+ // Example : abc -> def and xyz -> Abc
+ // A user typing Abc would experience it being autocorrected to something else (not
+ // necessarily def).
+ // There is no such combination in the whitelist at the time and there probably won't
+ // ever be - it doesn't make sense. But still.
+ public boolean shouldForciblyAutoCorrectFrom(CharSequence word) {
if (TextUtils.isEmpty(word)) return false;
- return !TextUtils.isEmpty(getWhiteListedWord(word.toString()));
+ final String correction = getWhitelistedWord(word.toString());
+ if (TextUtils.isEmpty(correction)) return false;
+ return !correction.equals(word);
}
+
+ // Leave implementation of getWords and isValidWord to the superclass.
+ // The words have been added to the ExpandableDictionary with addWord() inside initWordlist.
}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index af5e4b179..ca9caa1d3 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -1,12 +1,12 @@
/*
* Copyright (C) 2008 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
@@ -16,9 +16,12 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
-import java.util.ArrayList;
+import java.util.Arrays;
/**
* A place to store the currently composing word with information such as adjacent key codes as well
@@ -28,38 +31,35 @@ public class WordComposer {
public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
public static final int NOT_A_COORDINATE = -1;
- /**
- * The list of unicode values for each keystroke (including surrounding keys)
- */
- private ArrayList<int[]> mCodes;
+ private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
- private int mTypedLength;
+ private int[] mPrimaryKeyCodes;
private int[] mXCoordinates;
private int[] mYCoordinates;
-
- /**
- * The word chosen from the candidate list, until it is committed.
- */
- private String mPreferredWord;
-
private StringBuilder mTypedWord;
+ private CharSequence mAutoCorrection;
+ private boolean mIsResumed;
+ // Cache these values for performance
private int mCapsCount;
-
private boolean mAutoCapitalized;
-
+ private int mTrailingSingleQuotesCount;
+ private int mCodePointSize;
+
/**
* Whether the user chose to capitalize the first char of the word.
*/
private boolean mIsFirstCharCapitalized;
public WordComposer() {
- final int N = BinaryDictionary.MAX_WORD_LENGTH;
- mCodes = new ArrayList<int[]>(N);
+ mPrimaryKeyCodes = new int[N];
mTypedWord = new StringBuilder(N);
- mTypedLength = 0;
mXCoordinates = new int[N];
mYCoordinates = new int[N];
+ mAutoCorrection = null;
+ mTrailingSingleQuotesCount = 0;
+ mIsResumed = false;
+ refreshSize();
}
public WordComposer(WordComposer source) {
@@ -67,44 +67,53 @@ public class WordComposer {
}
public void init(WordComposer source) {
- mCodes = new ArrayList<int[]>(source.mCodes);
- mPreferredWord = source.mPreferredWord;
+ mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
mTypedWord = new StringBuilder(source.mTypedWord);
+ mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
+ mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
mCapsCount = source.mCapsCount;
- mAutoCapitalized = source.mAutoCapitalized;
mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
- mTypedLength = source.mTypedLength;
- mXCoordinates = source.mXCoordinates;
- mYCoordinates = source.mYCoordinates;
+ mAutoCapitalized = source.mAutoCapitalized;
+ mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
+ mIsResumed = source.mIsResumed;
+ refreshSize();
}
/**
* Clear out the keys registered so far.
*/
public void reset() {
- mCodes.clear();
- mTypedLength = 0;
- mIsFirstCharCapitalized = false;
- mPreferredWord = null;
mTypedWord.setLength(0);
+ mAutoCorrection = null;
mCapsCount = 0;
+ mIsFirstCharCapitalized = false;
+ mTrailingSingleQuotesCount = 0;
+ mIsResumed = false;
+ refreshSize();
+ }
+
+ public final void refreshSize() {
+ mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
}
/**
* Number of keystrokes in the composing word.
* @return the number of keystrokes
*/
- public int size() {
- return mCodes.size();
+ public final int size() {
+ return mCodePointSize;
}
- /**
- * Returns the codes at a particular position in the word.
- * @param index the position in the word
- * @return the unicode for the pressed and surrounding keys
- */
- public int[] getCodesAt(int index) {
- return mCodes.get(index);
+ public final boolean isComposingWord() {
+ return size() > 0;
+ }
+
+ // TODO: make sure that the index should not exceed MAX_WORD_LENGTH
+ public int getCodeAt(int index) {
+ if (index >= BinaryDictionary.MAX_WORD_LENGTH) {
+ return -1;
+ }
+ return mPrimaryKeyCodes[index];
}
public int[] getXCoordinates() {
@@ -115,71 +124,128 @@ public class WordComposer {
return mYCoordinates;
}
+ private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
+ if (index == 0) return Character.isUpperCase(codePoint);
+ return previous && !Character.isUpperCase(codePoint);
+ }
+
+ // TODO: remove input keyDetector
+ public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
+ final int keyX;
+ final int keyY;
+ if (null == keyDetector
+ || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
+ || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
+ || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
+ || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
+ keyX = x;
+ keyY = y;
+ } else {
+ keyX = keyDetector.getTouchX(x);
+ keyY = keyDetector.getTouchY(y);
+ }
+ add(primaryCode, keyX, keyY);
+ }
+
/**
- * Add a new keystroke, with codes[0] containing the pressed key's unicode and the rest of
- * the array containing unicode for adjacent keys, sorted by reducing probability/proximity.
- * @param codes the array of unicode values
+ * Add a new keystroke, with the pressed key's code point with the touch point coordinates.
*/
- public void add(int primaryCode, int[] codes, int x, int y) {
- mTypedWord.append((char) primaryCode);
- correctPrimaryJuxtapos(primaryCode, codes);
- mCodes.add(codes);
- if (mTypedLength < BinaryDictionary.MAX_WORD_LENGTH) {
- mXCoordinates[mTypedLength] = x;
- mYCoordinates[mTypedLength] = y;
+ private void add(int primaryCode, int keyX, int keyY) {
+ final int newIndex = size();
+ mTypedWord.appendCodePoint(primaryCode);
+ refreshSize();
+ if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
+ mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
+ ? Character.toLowerCase(primaryCode) : primaryCode;
+ mXCoordinates[newIndex] = keyX;
+ mYCoordinates[newIndex] = keyY;
}
- ++mTypedLength;
- if (Character.isUpperCase((char) primaryCode)) mCapsCount++;
+ mIsFirstCharCapitalized = isFirstCharCapitalized(
+ newIndex, primaryCode, mIsFirstCharCapitalized);
+ if (Character.isUpperCase(primaryCode)) mCapsCount++;
+ if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
+ ++mTrailingSingleQuotesCount;
+ } else {
+ mTrailingSingleQuotesCount = 0;
+ }
+ mAutoCorrection = null;
+ }
+
+ /**
+ * Internal method to retrieve reasonable proximity info for a character.
+ */
+ private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
+ for (final Key key : keyboard.mKeys) {
+ if (key.mCode == codePoint) {
+ final int x = key.mX + key.mWidth / 2;
+ final int y = key.mY + key.mHeight / 2;
+ add(codePoint, x, y);
+ return;
+ }
+ }
+ add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
}
/**
- * Swaps the first and second values in the codes array if the primary code is not the first
- * value in the array but the second. This happens when the preferred key is not the key that
- * the user released the finger on.
- * @param primaryCode the preferred character
- * @param codes array of codes based on distance from touch point
+ * Set the currently composing word to the one passed as an argument.
+ * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
*/
- private void correctPrimaryJuxtapos(int primaryCode, int[] codes) {
- if (codes.length < 2) return;
- if (codes[0] > 0 && codes[1] > 0 && codes[0] != primaryCode && codes[1] == primaryCode) {
- codes[1] = codes[0];
- codes[0] = primaryCode;
+ public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
+ reset();
+ final int length = word.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+ int codePoint = Character.codePointAt(word, i);
+ addKeyInfo(codePoint, keyboard);
}
+ mIsResumed = true;
}
/**
* Delete the last keystroke as a result of hitting backspace.
*/
public void deleteLast() {
- final int codesSize = mCodes.size();
- if (codesSize > 0) {
- mCodes.remove(codesSize - 1);
- final int lastPos = mTypedWord.length() - 1;
- char last = mTypedWord.charAt(lastPos);
- mTypedWord.deleteCharAt(lastPos);
- if (Character.isUpperCase(last)) mCapsCount--;
+ final int size = size();
+ if (size > 0) {
+ // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs
+ final int stringBuilderLength = mTypedWord.length();
+ if (stringBuilderLength < size) {
+ throw new RuntimeException(
+ "In WordComposer: mCodes and mTypedWords have non-matching lengths");
+ }
+ final int lastChar = mTypedWord.codePointBefore(stringBuilderLength);
+ if (Character.isSupplementaryCodePoint(lastChar)) {
+ mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength);
+ } else {
+ mTypedWord.deleteCharAt(stringBuilderLength - 1);
+ }
+ if (Character.isUpperCase(lastChar)) mCapsCount--;
+ refreshSize();
}
- if (mTypedLength > 0) {
- --mTypedLength;
+ // We may have deleted the last one.
+ if (0 == size()) {
+ mIsFirstCharCapitalized = false;
}
+ if (mTrailingSingleQuotesCount > 0) {
+ --mTrailingSingleQuotesCount;
+ } else {
+ int i = mTypedWord.length();
+ while (i > 0) {
+ i = mTypedWord.offsetByCodePoints(i, -1);
+ if (Keyboard.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break;
+ ++mTrailingSingleQuotesCount;
+ }
+ }
+ mAutoCorrection = null;
}
/**
* Returns the word as it was typed, without any correction applied.
- * @return the word that was typed so far
+ * @return the word that was typed so far. Never returns null.
*/
- public CharSequence getTypedWord() {
- int wordSize = mCodes.size();
- if (wordSize == 0) {
- return null;
- }
- return mTypedWord;
+ public String getTypedWord() {
+ return mTypedWord.toString();
}
- public void setFirstCharCapitalized(boolean capitalized) {
- mIsFirstCharCapitalized = capitalized;
- }
-
/**
* Whether or not the user typed a capital letter as the first letter in the word
* @return capitalization preference
@@ -188,6 +254,10 @@ public class WordComposer {
return mIsFirstCharCapitalized;
}
+ public int trailingSingleQuotesCount() {
+ return mTrailingSingleQuotesCount;
+ }
+
/**
* Whether or not all of the user typed chars are upper case
* @return true if all user typed chars are upper case, false otherwise
@@ -197,29 +267,13 @@ public class WordComposer {
}
/**
- * Stores the user's selected word, before it is actually committed to the text field.
- * @param preferred
- */
- public void setPreferredWord(String preferred) {
- mPreferredWord = preferred;
- }
-
- /**
- * Return the word chosen by the user, or the typed word if no other word was chosen.
- * @return the preferred word
- */
- public CharSequence getPreferredWord() {
- return mPreferredWord != null ? mPreferredWord : getTypedWord();
- }
-
- /**
* Returns true if more than one character is upper case, otherwise returns false.
*/
public boolean isMostlyCaps() {
return mCapsCount > 1;
}
- /**
+ /**
* Saves the reason why the word is capitalized - whether it was automatic or
* due to the user hitting shift in the middle of a sentence.
* @param auto whether it was an automatic capitalization due to start of sentence
@@ -235,4 +289,62 @@ public class WordComposer {
public boolean isAutoCapitalized() {
return mAutoCapitalized;
}
+
+ /**
+ * Sets the auto-correction for this word.
+ */
+ public void setAutoCorrection(final CharSequence correction) {
+ mAutoCorrection = correction;
+ }
+
+ /**
+ * @return the auto-correction for this word, or null if none.
+ */
+ public CharSequence getAutoCorrectionOrNull() {
+ return mAutoCorrection;
+ }
+
+ /**
+ * @return whether we started composing this word by resuming suggestion on an existing string
+ */
+ public boolean isResumed() {
+ return mIsResumed;
+ }
+
+ // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
+ public LastComposedWord commitWord(final int type, final String committedWord,
+ final int separatorCode, final CharSequence prevWord) {
+ // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
+ // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
+ // the last composed word to ensure this does not happen.
+ final int[] primaryKeyCodes = mPrimaryKeyCodes;
+ final int[] xCoordinates = mXCoordinates;
+ final int[] yCoordinates = mYCoordinates;
+ mPrimaryKeyCodes = new int[N];
+ mXCoordinates = new int[N];
+ mYCoordinates = new int[N];
+ final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
+ xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode,
+ prevWord);
+ if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
+ && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
+ lastComposedWord.deactivate();
+ }
+ mTypedWord.setLength(0);
+ refreshSize();
+ mAutoCorrection = null;
+ mIsResumed = false;
+ return lastComposedWord;
+ }
+
+ public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+ mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
+ mXCoordinates = lastComposedWord.mXCoordinates;
+ mYCoordinates = lastComposedWord.mYCoordinates;
+ mTypedWord.setLength(0);
+ mTypedWord.append(lastComposedWord.mTypedWord);
+ refreshSize();
+ mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
+ mIsResumed = true;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/WordListInfo.java
index eb740e111..54f04d78f 100644
--- a/java/src/com/android/inputmethod/latin/PrivateBinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/WordListInfo.java
@@ -1,4 +1,4 @@
-/*
+/**
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -16,14 +16,14 @@
package com.android.inputmethod.latin;
-import android.content.Context;
-
-import java.util.List;
-import java.util.Locale;
-
-class PrivateBinaryDictionaryGetter {
- private PrivateBinaryDictionaryGetter() {}
- public static List<AssetFileAddress> getDictionaryFiles(Locale locale, Context context) {
- return null;
+/**
+ * Information container for a word list.
+ */
+public class WordListInfo {
+ public final String mId;
+ public final String mLocale;
+ public WordListInfo(final String id, final String locale) {
+ mId = id;
+ mLocale = locale;
}
}
diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
new file mode 100644
index 000000000..481cdfa47
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.res.TypedArray;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+public class XmlParseUtils {
+ private XmlParseUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ @SuppressWarnings("serial")
+ public static class ParseException extends XmlPullParserException {
+ public ParseException(String msg, XmlPullParser parser) {
+ super(msg + " at " + parser.getPositionDescription());
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class IllegalStartTag extends ParseException {
+ public IllegalStartTag(XmlPullParser parser, String parent) {
+ super("Illegal start tag " + parser.getName() + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class IllegalEndTag extends ParseException {
+ public IllegalEndTag(XmlPullParser parser, String parent) {
+ super("Illegal end tag " + parser.getName() + " in " + parent, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class IllegalAttribute extends ParseException {
+ public IllegalAttribute(XmlPullParser parser, String attribute) {
+ super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
+ }
+ }
+
+ @SuppressWarnings("serial")
+ public static class NonEmptyTag extends ParseException{
+ public NonEmptyTag(String tag, XmlPullParser parser) {
+ super(tag + " must be empty tag", parser);
+ }
+ }
+
+ public static void checkEndTag(String tag, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (parser.next() == XmlPullParser.END_TAG && tag.equals(parser.getName()))
+ return;
+ throw new NonEmptyTag(tag, parser);
+ }
+
+ public static void checkAttributeExists(TypedArray attr, int attrId, String attrName,
+ String tag, XmlPullParser parser) throws XmlPullParserException {
+ if (attr.hasValue(attrId))
+ return;
+ throw new ParseException(
+ "No " + attrName + " attribute found in <" + tag + "/>", parser);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/define/JniLibName.java b/java/src/com/android/inputmethod/latin/define/JniLibName.java
new file mode 100644
index 000000000..e23e1a968
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/define/JniLibName.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 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.define;
+
+public final class JniLibName {
+ private JniLibName() {
+ // This class is not publicly instantiable.
+ }
+
+ public static final String JNI_LIB_NAME = "jni_latinime";
+}
diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
new file mode 100644
index 000000000..de2057669
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
@@ -0,0 +1,25 @@
+/*
+ * 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.define;
+
+public final class ProductionFlag {
+ private ProductionFlag() {
+ // This class is not publicly instantiable.
+ }
+
+ public static final boolean IS_EXPERIMENTAL = false;
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
new file mode 100644
index 000000000..2c3eee74c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -0,0 +1,1413 @@
+/*
+ * Copyright (C) 2011 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.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Reads and writes XML files for a FusionDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public class BinaryDictInputOutput {
+
+ final static boolean DBG = MakedictLog.DBG;
+
+ /* Node layout is as follows:
+ * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE
+ * 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
+ * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
+ * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
+ * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+ * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
+ * s | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL
+ * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS
+ * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
+ *
+ * c | IF FLAG_HAS_MULTIPLE_CHARS
+ * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * a | end 1 byte, = 0
+ * r | ELSE
+ * s | char 1 or 3 bytes
+ * | END
+ *
+ * f |
+ * r | IF FLAG_IS_TERMINAL
+ * e | frequency 1 byte
+ * q |
+ *
+ * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
+ * h | // nothing
+ * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
+ * l | children address, 1 byte
+ * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
+ * r | children address, 2 bytes
+ * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
+ * n | children address, 3 bytes
+ * A | END
+ * d
+ * dress
+ *
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
+ * | shortcut string list
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+ * | bigrams address list
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * bigram address list is:
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+ * | 1 = must take -address, 0 = must take +address
+ * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
+ * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
+ * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
+ * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
+ * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+ * | read 1 byte, add top 4 bits
+ * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | read 2 bytes, add top 4 bits
+ * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+ * | read 3 bytes, add top 4 bits
+ * | END
+ * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
+ * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is
+ *
+ * shortcut string list is:
+ * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * | reserved = 3 bits, must be 0
+ * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * <shortcut> = | string of characters at the char format described above, with the terminator
+ * | used to signal the end of the string.
+ * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags
+ */
+
+ private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
+ private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
+ private static final int MINIMUM_SUPPORTED_VERSION = 1;
+ private static final int MAXIMUM_SUPPORTED_VERSION = 2;
+ private static final int NOT_A_VERSION_NUMBER = -1;
+ private static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
+
+ // These options need to be the same numeric values as the one in the native reading code.
+ private static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
+ private static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
+ private static final int CONTAINS_BIGRAMS_FLAG = 0x8;
+
+ // TODO: Make this value adaptative to content data, store it in the header, and
+ // use it in the reading code.
+ private static final int MAX_WORD_LENGTH = 48;
+
+ private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
+ private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+
+ private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+ private static final int FLAG_IS_TERMINAL = 0x10;
+ private static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
+ private static final int FLAG_HAS_BIGRAMS = 0x04;
+
+ private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
+ private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
+ private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
+ private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
+ private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+
+ private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+ private static final int GROUP_TERMINATOR_SIZE = 1;
+ private static final int GROUP_FLAGS_SIZE = 1;
+ private static final int GROUP_FREQUENCY_SIZE = 1;
+ private static final int GROUP_MAX_ADDRESS_SIZE = 3;
+ private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
+ private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+ private static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
+
+ private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
+ private static final int INVALID_CHARACTER = -1;
+
+ private static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
+ private static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
+
+ private static final int MAX_TERMINAL_FREQUENCY = 255;
+ private static final int MAX_BIGRAM_FREQUENCY = 15;
+
+ // Arbitrary limit to how much passes we consider address size compression should
+ // terminate in. At the time of this writing, our largest dictionary completes
+ // compression in five passes.
+ // If the number of passes exceeds this number, makedict bails with an exception on
+ // suspicion that a bug might be causing an infinite loop.
+ private static final int MAX_PASSES = 24;
+
+ /**
+ * A class grouping utility function for our specific character encoding.
+ */
+ private static class CharEncoding {
+
+ private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
+ private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
+
+ /**
+ * Helper method to find out whether this code fits on one byte
+ */
+ private static boolean fitsOnOneByte(int character) {
+ return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
+ && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
+ }
+
+ /**
+ * Compute the size of a character given its character code.
+ *
+ * Char format is:
+ * 1 byte = bbbbbbbb match
+ * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte
+ * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because
+ * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with
+ * 00011111 would be outside unicode.
+ * else: iso-latin-1 code
+ * This allows for the whole unicode range to be encoded, including chars outside of
+ * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control
+ * characters which should never happen anyway (and still work, but take 3 bytes).
+ *
+ * @param character the character code.
+ * @return the size in binary encoded-form, either 1 or 3 bytes.
+ */
+ private static int getCharSize(int character) {
+ // See char encoding in FusionDictionary.java
+ if (fitsOnOneByte(character)) return 1;
+ if (INVALID_CHARACTER == character) return 1;
+ return 3;
+ }
+
+ /**
+ * Compute the byte size of a character array.
+ */
+ private static int getCharArraySize(final int[] chars) {
+ int size = 0;
+ for (int character : chars) size += getCharSize(character);
+ return size;
+ }
+
+ /**
+ * Writes a char array to a byte buffer.
+ *
+ * @param codePoints the code point array to write.
+ * @param buffer the byte buffer to write to.
+ * @param index the index in buffer to write the character array to.
+ * @return the index after the last character.
+ */
+ private static int writeCharArray(final int[] codePoints, final byte[] buffer, int index) {
+ for (int codePoint : codePoints) {
+ if (1 == getCharSize(codePoint)) {
+ buffer[index++] = (byte)codePoint;
+ } else {
+ buffer[index++] = (byte)(0xFF & (codePoint >> 16));
+ buffer[index++] = (byte)(0xFF & (codePoint >> 8));
+ buffer[index++] = (byte)(0xFF & codePoint);
+ }
+ }
+ return index;
+ }
+
+ /**
+ * Writes a string with our character format to a byte buffer.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param buffer the byte buffer to write to.
+ * @param origin the offset to write from.
+ * @param word the string to write.
+ * @return the size written, in bytes.
+ */
+ private static int writeString(final byte[] buffer, final int origin,
+ final String word) {
+ final int length = word.length();
+ int index = origin;
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ if (1 == getCharSize(codePoint)) {
+ buffer[index++] = (byte)codePoint;
+ } else {
+ buffer[index++] = (byte)(0xFF & (codePoint >> 16));
+ buffer[index++] = (byte)(0xFF & (codePoint >> 8));
+ buffer[index++] = (byte)(0xFF & codePoint);
+ }
+ }
+ buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+ return index - origin;
+ }
+
+ /**
+ * Writes a string with our character format to a ByteArrayOutputStream.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param buffer the ByteArrayOutputStream to write to.
+ * @param word the string to write.
+ */
+ private static void writeString(ByteArrayOutputStream buffer, final String word) {
+ final int length = word.length();
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ if (1 == getCharSize(codePoint)) {
+ buffer.write((byte) codePoint);
+ } else {
+ buffer.write((byte) (0xFF & (codePoint >> 16)));
+ buffer.write((byte) (0xFF & (codePoint >> 8)));
+ buffer.write((byte) (0xFF & codePoint));
+ }
+ }
+ buffer.write(GROUP_CHARACTERS_TERMINATOR);
+ }
+
+ /**
+ * Reads a string from a RandomAccessFile. This is the converse of the above method.
+ */
+ private static String readString(final RandomAccessFile source) throws IOException {
+ final StringBuilder s = new StringBuilder();
+ int character = readChar(source);
+ while (character != INVALID_CHARACTER) {
+ s.appendCodePoint(character);
+ character = readChar(source);
+ }
+ return s.toString();
+ }
+
+ /**
+ * Reads a character from the file.
+ *
+ * This follows the character format documented earlier in this source file.
+ *
+ * @param source the file, positioned over an encoded character.
+ * @return the character code.
+ */
+ private static int readChar(RandomAccessFile source) throws IOException {
+ int character = source.readUnsignedByte();
+ if (!fitsOnOneByte(character)) {
+ if (GROUP_CHARACTERS_TERMINATOR == character)
+ return INVALID_CHARACTER;
+ character <<= 16;
+ character += source.readUnsignedShort();
+ }
+ return character;
+ }
+ }
+
+ /**
+ * Compute the binary size of the character array in a group
+ *
+ * If only one character, this is the size of this character. If many, it's the sum of their
+ * sizes + 1 byte for the terminator.
+ *
+ * @param group the group
+ * @return the size of the char array, including the terminator if any
+ */
+ private static int getGroupCharactersSize(CharGroup group) {
+ int size = CharEncoding.getCharArraySize(group.mChars);
+ if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the binary size of the group count
+ * @param count the group count
+ * @return the size of the group count, either 1 or 2 bytes.
+ */
+ private static int getGroupCountSize(final int count) {
+ if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
+ return 1;
+ } else if (MAX_CHARGROUPS_IN_A_NODE >= count) {
+ return 2;
+ } else {
+ throw new RuntimeException("Can't have more than " + MAX_CHARGROUPS_IN_A_NODE
+ + " groups in a node (found " + count +")");
+ }
+ }
+
+ /**
+ * Compute the binary size of the group count for a node
+ * @param node the node
+ * @return the size of the group count, either 1 or 2 bytes.
+ */
+ private static int getGroupCountSize(final Node node) {
+ return getGroupCountSize(node.mData.size());
+ }
+
+ /**
+ * Compute the size of a shortcut in bytes.
+ */
+ private static int getShortcutSize(final WeightedString shortcut) {
+ int size = GROUP_ATTRIBUTE_FLAGS_SIZE;
+ final String word = shortcut.mWord;
+ final int length = word.length();
+ for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
+ final int codePoint = word.codePointAt(i);
+ size += CharEncoding.getCharSize(codePoint);
+ }
+ size += GROUP_TERMINATOR_SIZE;
+ return size;
+ }
+
+ /**
+ * Compute the size of a shortcut list in bytes.
+ *
+ * This is known in advance and does not change according to position in the file
+ * like address lists do.
+ */
+ private static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) {
+ if (null == shortcutList) return 0;
+ int size = GROUP_SHORTCUT_LIST_SIZE_SIZE;
+ for (final WeightedString shortcut : shortcutList) {
+ size += getShortcutSize(shortcut);
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
+ *
+ * @param group the CharGroup to compute the size of.
+ * @return the maximum size of the group.
+ */
+ private static int getCharGroupMaximumSize(CharGroup group) {
+ int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+ // If terminal, one byte for the frequency
+ if (group.isTerminal()) size += GROUP_FREQUENCY_SIZE;
+ size += GROUP_MAX_ADDRESS_SIZE; // For children address
+ size += getShortcutListSize(group.mShortcutTargets);
+ if (null != group.mBigrams) {
+ size += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
+ * group.mBigrams.size();
+ }
+ return size;
+ }
+
+ /**
+ * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
+ * it in the 'actualSize' member of the node.
+ *
+ * @param node the node to compute the maximum size of.
+ */
+ private static void setNodeMaximumSize(Node node) {
+ int size = getGroupCountSize(node);
+ for (CharGroup g : node.mData) {
+ final int groupSize = getCharGroupMaximumSize(g);
+ g.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ node.mCachedSize = size;
+ }
+
+ /**
+ * Helper method to hide the actual value of the no children address.
+ */
+ private static boolean hasChildrenAddress(int address) {
+ return NO_CHILDREN_ADDRESS != address;
+ }
+
+ /**
+ * Compute the size, in bytes, that an address will occupy.
+ *
+ * This can be used either for children addresses (which are always positive) or for
+ * attribute, which may be positive or negative but
+ * store their sign bit separately.
+ *
+ * @param address the address
+ * @return the byte size.
+ */
+ private static int getByteSize(int address) {
+ assert(address < 0x1000000);
+ if (!hasChildrenAddress(address)) {
+ return 0;
+ } else if (Math.abs(address) < 0x100) {
+ return 1;
+ } else if (Math.abs(address) < 0x10000) {
+ return 2;
+ } else {
+ return 3;
+ }
+ }
+ // End utility methods.
+
+ // This method is responsible for finding a nice ordering of the nodes that favors run-time
+ // cache performance and dictionary size.
+ /* package for tests */ static ArrayList<Node> flattenTree(Node root) {
+ final int treeSize = FusionDictionary.countCharGroups(root);
+ MakedictLog.i("Counted nodes : " + treeSize);
+ final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
+ return flattenTreeInner(flatTree, root);
+ }
+
+ private static ArrayList<Node> flattenTreeInner(ArrayList<Node> list, Node node) {
+ // Removing the node is necessary if the tails are merged, because we would then
+ // add the same node several times when we only want it once. A number of places in
+ // the code also depends on any node being only once in the list.
+ // Merging tails can only be done if there are no attributes. Searching for attributes
+ // in LatinIME code depends on a total breadth-first ordering, which merging tails
+ // breaks. If there are no attributes, it should be fine (and reduce the file size)
+ // to merge tails, and removing the node from the list would be necessary. However,
+ // we don't merge tails because breaking the breadth-first ordering would result in
+ // extreme overhead at bigram lookup time (it would make the search function O(n) instead
+ // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty
+ // high).
+ // If no nodes are ever merged, we can't have the same node twice in the list, hence
+ // searching for duplicates in unnecessary. It is also very performance consuming,
+ // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making
+ // this simple list.remove operation O(n*n) overall. On Android this overhead is very
+ // high.
+ // For future reference, the code to remove duplicate is a simple : list.remove(node);
+ list.add(node);
+ final ArrayList<CharGroup> branches = node.mData;
+ final int nodeSize = branches.size();
+ for (CharGroup group : branches) {
+ if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
+ }
+ return list;
+ }
+
+ /**
+ * Finds the absolute address of a word in the dictionary.
+ *
+ * @param dict the dictionary in which to search.
+ * @param word the word we are searching for.
+ * @return the word address. If it is not found, an exception is thrown.
+ */
+ private static int findAddressOfWord(final FusionDictionary dict, final String word) {
+ return FusionDictionary.findWordInTree(dict.mRoot, word).mCachedAddress;
+ }
+
+ /**
+ * Computes the actual node size, based on the cached addresses of the children nodes.
+ *
+ * Each node stores its tentative address. During dictionary address computing, these
+ * are not final, but they can be used to compute the node size (the node size depends
+ * on the address of the children because the number of bytes necessary to store an
+ * address depends on its numeric value. The return value indicates whether the node
+ * contents (as in, any of the addresses stored in the cache fields) have changed with
+ * respect to their previous value.
+ *
+ * @param node the node to compute the size of.
+ * @param dict the dictionary in which the word/attributes are to be found.
+ * @return false if none of the cached addresses inside the node changed, true otherwise.
+ */
+ private static boolean computeActualNodeSize(Node node, FusionDictionary dict) {
+ boolean changed = false;
+ int size = getGroupCountSize(node);
+ for (CharGroup group : node.mData) {
+ if (group.mCachedAddress != node.mCachedAddress + size) {
+ changed = true;
+ group.mCachedAddress = node.mCachedAddress + size;
+ }
+ int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
+ if (null != group.mChildren) {
+ final int offsetBasePoint= groupSize + node.mCachedAddress + size;
+ final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
+ groupSize += getByteSize(offset);
+ }
+ groupSize += getShortcutListSize(group.mShortcutTargets);
+ if (null != group.mBigrams) {
+ for (WeightedString bigram : group.mBigrams) {
+ final int offsetBasePoint = groupSize + node.mCachedAddress + size
+ + GROUP_FLAGS_SIZE;
+ final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
+ final int offset = addressOfBigram - offsetBasePoint;
+ groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+ }
+ }
+ group.mCachedSize = groupSize;
+ size += groupSize;
+ }
+ if (node.mCachedSize != size) {
+ node.mCachedSize = size;
+ changed = true;
+ }
+ return changed;
+ }
+
+ /**
+ * Computes the byte size of a list of nodes and updates each node cached position.
+ *
+ * @param flatNodes the array of nodes.
+ * @return the byte size of the entire stack.
+ */
+ private static int stackNodes(ArrayList<Node> flatNodes) {
+ int nodeOffset = 0;
+ for (Node n : flatNodes) {
+ n.mCachedAddress = nodeOffset;
+ int groupCountSize = getGroupCountSize(n);
+ int groupOffset = 0;
+ for (CharGroup g : n.mData) {
+ g.mCachedAddress = groupCountSize + nodeOffset + groupOffset;
+ groupOffset += g.mCachedSize;
+ }
+ if (groupOffset + groupCountSize != n.mCachedSize) {
+ throw new RuntimeException("Bug : Stored and computed node size differ");
+ }
+ nodeOffset += n.mCachedSize;
+ }
+ return nodeOffset;
+ }
+
+ /**
+ * Compute the addresses and sizes of an ordered node array.
+ *
+ * This method takes a node array and will update its cached address and size values
+ * so that they can be written into a file. It determines the smallest size each of the
+ * nodes can be given the addresses of its children and attributes, and store that into
+ * each node.
+ * The order of the node is given by the order of the array. This method makes no effort
+ * to find a good order; it only mechanically computes the size this order results in.
+ *
+ * @param dict the dictionary
+ * @param flatNodes the ordered array of nodes
+ * @return the same array it was passed. The nodes have been updated for address and size.
+ */
+ private static ArrayList<Node> computeAddresses(FusionDictionary dict,
+ ArrayList<Node> flatNodes) {
+ // First get the worst sizes and offsets
+ for (Node n : flatNodes) setNodeMaximumSize(n);
+ final int offset = stackNodes(flatNodes);
+
+ MakedictLog.i("Compressing the array addresses. Original size : " + offset);
+ MakedictLog.i("(Recursively seen size : " + offset + ")");
+
+ int passes = 0;
+ boolean changesDone = false;
+ do {
+ changesDone = false;
+ for (Node n : flatNodes) {
+ final int oldNodeSize = n.mCachedSize;
+ final boolean changed = computeActualNodeSize(n, dict);
+ final int newNodeSize = n.mCachedSize;
+ if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
+ changesDone |= changed;
+ }
+ stackNodes(flatNodes);
+ ++passes;
+ if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
+ } while (changesDone);
+
+ final Node lastNode = flatNodes.get(flatNodes.size() - 1);
+ MakedictLog.i("Compression complete in " + passes + " passes.");
+ MakedictLog.i("After address compression : "
+ + (lastNode.mCachedAddress + lastNode.mCachedSize));
+
+ return flatNodes;
+ }
+
+ /**
+ * Sanity-checking method.
+ *
+ * This method checks an array of node for juxtaposition, that is, it will do
+ * nothing if each node's cached address is actually the previous node's address
+ * plus the previous node's size.
+ * If this is not the case, it will throw an exception.
+ *
+ * @param array the array node to check
+ */
+ private static void checkFlatNodeArray(ArrayList<Node> array) {
+ int offset = 0;
+ int index = 0;
+ for (Node n : array) {
+ if (n.mCachedAddress != offset) {
+ throw new RuntimeException("Wrong address for node " + index
+ + " : expected " + offset + ", got " + n.mCachedAddress);
+ }
+ ++index;
+ offset += n.mCachedSize;
+ }
+ }
+
+ /**
+ * Helper method to write a variable-size address to a file.
+ *
+ * @param buffer the buffer to write to.
+ * @param index the index in the buffer to write the address to.
+ * @param address the address to write.
+ * @return the size in bytes the address actually took.
+ */
+ private static int writeVariableAddress(final byte[] buffer, int index, final int address) {
+ switch (getByteSize(address)) {
+ case 1:
+ buffer[index++] = (byte)address;
+ return 1;
+ case 2:
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 2;
+ case 3:
+ buffer[index++] = (byte)(0xFF & (address >> 16));
+ buffer[index++] = (byte)(0xFF & (address >> 8));
+ buffer[index++] = (byte)(0xFF & address);
+ return 3;
+ case 0:
+ return 0;
+ default:
+ throw new RuntimeException("Address " + address + " has a strange size");
+ }
+ }
+
+ private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
+ final int childrenOffset) {
+ byte flags = 0;
+ if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+ if (group.mFrequency >= 0) {
+ flags |= FLAG_IS_TERMINAL;
+ }
+ if (null != group.mChildren) {
+ switch (getByteSize(childrenOffset)) {
+ case 1:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Node with a strange address");
+ }
+ }
+ if (null != group.mShortcutTargets) {
+ if (DBG && 0 == group.mShortcutTargets.size()) {
+ throw new RuntimeException("0-sized shortcut list must be null");
+ }
+ flags |= FLAG_HAS_SHORTCUT_TARGETS;
+ }
+ if (null != group.mBigrams) {
+ if (DBG && 0 == group.mBigrams.size()) {
+ throw new RuntimeException("0-sized bigram list must be null");
+ }
+ flags |= FLAG_HAS_BIGRAMS;
+ }
+ return flags;
+ }
+
+ /**
+ * Makes the flag value for a bigram.
+ *
+ * @param more whether there are more bigrams after this one.
+ * @param offset the offset of the bigram.
+ * @param bigramFrequency the frequency of the bigram, 0..255.
+ * @param unigramFrequency the unigram frequency of the same word, 0..255.
+ * @param word the second bigram, for debugging purposes
+ * @return the flags
+ */
+ private static final int makeBigramFlags(final boolean more, final int offset,
+ int bigramFrequency, final int unigramFrequency, final String word) {
+ int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
+ + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+ switch (getByteSize(offset)) {
+ case 1:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ break;
+ case 2:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ break;
+ case 3:
+ bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ break;
+ default:
+ throw new RuntimeException("Strange offset size");
+ }
+ if (unigramFrequency > bigramFrequency) {
+ MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word
+ + "\". Bigram freq is " + bigramFrequency + ", unigram freq for "
+ + word + " is " + unigramFrequency);
+ bigramFrequency = unigramFrequency;
+ }
+ // We compute the difference between 255 (which means probability = 1) and the
+ // unigram score. We split this into a number of discrete steps.
+ // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15
+ // represents an increase of 16 steps: a value of 15 will be interpreted as the median
+ // value of the 16th step. In all justice, if the bigram frequency is low enough to be
+ // rounded below the first step (which means it is less than half a step higher than the
+ // unigram frequency) then the unigram frequency itself is the best approximation of the
+ // bigram freq that we could possibly supply, hence we should *not* include this bigram
+ // in the file at all.
+ // until this is done, we'll write 0 and slightly overestimate this case.
+ // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step
+ // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to
+ // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the
+ // step size. Then we compute the start of the first step (the one where value 0 starts)
+ // by adding half-a-step to the unigramFrequency. From there, we compute the integer
+ // number of steps to the bigramFrequency. One last thing: we want our steps to include
+ // their lower bound and exclude their higher bound so we need to have the first step
+ // start at exactly 1 unit higher than floor(unigramFreq + half a step).
+ // Note : to reconstruct the score, the dictionary reader will need to divide
+ // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise, and add
+ // (discretizedFrequency + 0.5) times this value to get the median value of the step,
+ // which is the best approximation. This is how we get the most precise result with
+ // only four bits.
+ final float stepSize =
+ (MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5f + MAX_BIGRAM_FREQUENCY);
+ final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f);
+ final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize);
+ // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1
+ // here. The best approximation would be the unigram freq itself, so we should not
+ // include this bigram in the dictionary. For now, register as 0, and live with the
+ // small over-estimation that we get in this case. TODO: actually remove this bigram
+ // if discretizedFrequency < 0.
+ final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0;
+ bigramFlags += finalBigramFrequency & FLAG_ATTRIBUTE_FREQUENCY;
+ return bigramFlags;
+ }
+
+ /**
+ * Makes the 2-byte value for options flags.
+ */
+ private static final int makeOptionsValue(final FusionDictionary dictionary) {
+ final DictionaryOptions options = dictionary.mOptions;
+ final boolean hasBigrams = dictionary.hasBigrams();
+ return (options.mFrenchLigatureProcessing ? FRENCH_LIGATURE_PROCESSING_FLAG : 0)
+ + (options.mGermanUmlautProcessing ? GERMAN_UMLAUT_PROCESSING_FLAG : 0)
+ + (hasBigrams ? CONTAINS_BIGRAMS_FLAG : 0);
+ }
+
+ /**
+ * Makes the flag value for a shortcut.
+ *
+ * @param more whether there are more attributes after this one.
+ * @param frequency the frequency of the attribute, 0..15
+ * @return the flags
+ */
+ private static final int makeShortcutFlags(final boolean more, final int frequency) {
+ return (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0) + (frequency & FLAG_ATTRIBUTE_FREQUENCY);
+ }
+
+ /**
+ * Write a node to memory. The node is expected to have its final position cached.
+ *
+ * This can be an empty map, but the more is inside the faster the lookups will be. It can
+ * be carried on as long as nodes do not move.
+ *
+ * @param dict the dictionary the node is a part of (for relative offsets).
+ * @param buffer the memory buffer to write to.
+ * @param node the node to write.
+ * @return the address of the END of the node.
+ */
+ private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+ int index = node.mCachedAddress;
+
+ final int groupCount = node.mData.size();
+ final int countSize = getGroupCountSize(node);
+ if (1 == countSize) {
+ buffer[index++] = (byte)groupCount;
+ } else if (2 == countSize) {
+ // We need to signal 2-byte size by setting the top bit of the MSB to 1, so
+ // we | 0x80 to do this.
+ buffer[index++] = (byte)((groupCount >> 8) | 0x80);
+ buffer[index++] = (byte)(groupCount & 0xFF);
+ } else {
+ throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
+ }
+ int groupAddress = index;
+ for (int i = 0; i < groupCount; ++i) {
+ CharGroup group = node.mData.get(i);
+ if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
+ + "the same as the cached address of the group : "
+ + index + " <> " + group.mCachedAddress);
+ groupAddress += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ // Sanity checks.
+ if (DBG && group.mFrequency > MAX_TERMINAL_FREQUENCY) {
+ throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+ + " : " + group.mFrequency);
+ }
+ if (group.mFrequency >= 0) groupAddress += GROUP_FREQUENCY_SIZE;
+ final int childrenOffset = null == group.mChildren
+ ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
+ byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
+ buffer[index++] = flags;
+ index = CharEncoding.writeCharArray(group.mChars, buffer, index);
+ if (group.hasSeveralChars()) {
+ buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+ }
+ if (group.mFrequency >= 0) {
+ buffer[index++] = (byte) group.mFrequency;
+ }
+ final int shift = writeVariableAddress(buffer, index, childrenOffset);
+ index += shift;
+ groupAddress += shift;
+
+ // Write shortcuts
+ if (null != group.mShortcutTargets) {
+ final int indexOfShortcutByteSize = index;
+ index += GROUP_SHORTCUT_LIST_SIZE_SIZE;
+ groupAddress += GROUP_SHORTCUT_LIST_SIZE_SIZE;
+ final Iterator<WeightedString> shortcutIterator = group.mShortcutTargets.iterator();
+ while (shortcutIterator.hasNext()) {
+ final WeightedString target = shortcutIterator.next();
+ ++groupAddress;
+ int shortcutFlags = makeShortcutFlags(shortcutIterator.hasNext(),
+ target.mFrequency);
+ buffer[index++] = (byte)shortcutFlags;
+ final int shortcutShift = CharEncoding.writeString(buffer, index, target.mWord);
+ index += shortcutShift;
+ groupAddress += shortcutShift;
+ }
+ final int shortcutByteSize = index - indexOfShortcutByteSize;
+ if (shortcutByteSize > 0xFFFF) {
+ throw new RuntimeException("Shortcut list too large");
+ }
+ buffer[indexOfShortcutByteSize] = (byte)(shortcutByteSize >> 8);
+ buffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF);
+ }
+ // Write bigrams
+ if (null != group.mBigrams) {
+ final Iterator<WeightedString> bigramIterator = group.mBigrams.iterator();
+ while (bigramIterator.hasNext()) {
+ final WeightedString bigram = bigramIterator.next();
+ final CharGroup target =
+ FusionDictionary.findWordInTree(dict.mRoot, bigram.mWord);
+ final int addressOfBigram = target.mCachedAddress;
+ final int unigramFrequencyForThisWord = target.mFrequency;
+ ++groupAddress;
+ final int offset = addressOfBigram - groupAddress;
+ int bigramFlags = makeBigramFlags(bigramIterator.hasNext(), offset,
+ bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
+ buffer[index++] = (byte)bigramFlags;
+ final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
+ index += bigramShift;
+ groupAddress += bigramShift;
+ }
+ }
+
+ }
+ 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 "
+ + node.mCachedSize + " bytes");
+ return index;
+ }
+
+ /**
+ * Dumps a collection of useful statistics about a node array.
+ *
+ * This prints purely informative stuff, like the total estimated file size, the
+ * number of nodes, of character groups, the repartition of each address size, etc
+ *
+ * @param nodes the node array.
+ */
+ private static void showStatistics(ArrayList<Node> nodes) {
+ int firstTerminalAddress = Integer.MAX_VALUE;
+ int lastTerminalAddress = Integer.MIN_VALUE;
+ int size = 0;
+ int charGroups = 0;
+ int maxGroups = 0;
+ int maxRuns = 0;
+ for (Node n : nodes) {
+ if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
+ for (CharGroup cg : n.mData) {
+ ++charGroups;
+ if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
+ if (cg.mFrequency >= 0) {
+ if (n.mCachedAddress < firstTerminalAddress)
+ firstTerminalAddress = n.mCachedAddress;
+ if (n.mCachedAddress > lastTerminalAddress)
+ lastTerminalAddress = n.mCachedAddress;
+ }
+ }
+ if (n.mCachedAddress + n.mCachedSize > size) size = n.mCachedAddress + n.mCachedSize;
+ }
+ final int[] groupCounts = new int[maxGroups + 1];
+ final int[] runCounts = new int[maxRuns + 1];
+ for (Node n : nodes) {
+ ++groupCounts[n.mData.size()];
+ for (CharGroup cg : n.mData) {
+ ++runCounts[cg.mChars.length];
+ }
+ }
+
+ MakedictLog.i("Statistics:\n"
+ + " total file size " + size + "\n"
+ + " " + nodes.size() + " nodes\n"
+ + " " + charGroups + " groups (" + ((float)charGroups / nodes.size())
+ + " groups per node)\n"
+ + " first terminal at " + firstTerminalAddress + "\n"
+ + " last terminal at " + lastTerminalAddress + "\n"
+ + " Group stats : max = " + maxGroups);
+ for (int i = 0; i < groupCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + groupCounts[i]);
+ }
+ MakedictLog.i(" Character run stats : max = " + maxRuns);
+ for (int i = 0; i < runCounts.length; ++i) {
+ MakedictLog.i(" " + i + " : " + runCounts[i]);
+ }
+ }
+
+ /**
+ * Dumps a FusionDictionary to a file.
+ *
+ * This is the public entry point to write a dictionary to a file.
+ *
+ * @param destination the stream to write the binary data to.
+ * @param dict the dictionary to write.
+ * @param version the version of the format to write, currently either 1 or 2.
+ */
+ public static void writeDictionaryBinary(final OutputStream destination,
+ final FusionDictionary dict, final int version)
+ throws IOException, UnsupportedFormatException {
+
+ // Addresses are limited to 3 bytes, but since addresses can be relative to each node, the
+ // structure itself is not limited to 16MB. However, if it is over 16MB deciding the order
+ // of the nodes becomes a quite complicated problem, because though the dictionary itself
+ // does not have a size limit, each node must still be within 16MB of all its children and
+ // parents. As long as this is ensured, the dictionary file may grow to any size.
+
+ if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
+ throw new UnsupportedFormatException("Requested file format version " + version
+ + ", but this implementation only supports versions "
+ + MINIMUM_SUPPORTED_VERSION + " through " + MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
+
+ // The magic number in big-endian order.
+ if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ // Magic number for version 2+.
+ headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 24)));
+ headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 16)));
+ headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 8)));
+ headerBuffer.write((byte) (0xFF & VERSION_2_MAGIC_NUMBER));
+ // Dictionary version.
+ headerBuffer.write((byte) (0xFF & (version >> 8)));
+ headerBuffer.write((byte) (0xFF & version));
+ } else {
+ // Magic number for version 1.
+ headerBuffer.write((byte) (0xFF & (VERSION_1_MAGIC_NUMBER >> 8)));
+ headerBuffer.write((byte) (0xFF & VERSION_1_MAGIC_NUMBER));
+ // Dictionary version.
+ headerBuffer.write((byte) (0xFF & version));
+ }
+ // Options flags
+ final int options = makeOptionsValue(dict);
+ headerBuffer.write((byte) (0xFF & (options >> 8)));
+ headerBuffer.write((byte) (0xFF & options));
+ if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ final int headerSizeOffset = headerBuffer.size();
+ // Placeholder to be written later with header size.
+ for (int i = 0; i < 4; ++i) {
+ headerBuffer.write(0);
+ }
+ // Write out the options.
+ for (final String key : dict.mOptions.mAttributes.keySet()) {
+ final String value = dict.mOptions.mAttributes.get(key);
+ CharEncoding.writeString(headerBuffer, key);
+ CharEncoding.writeString(headerBuffer, value);
+ }
+ final int size = headerBuffer.size();
+ final byte[] bytes = headerBuffer.toByteArray();
+ // Write out the header size.
+ bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24));
+ bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16));
+ bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8));
+ bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0));
+ destination.write(bytes);
+ } else {
+ headerBuffer.writeTo(destination);
+ }
+
+ headerBuffer.close();
+
+ // Leave the choice of the optimal node order to the flattenTree function.
+ MakedictLog.i("Flattening the tree...");
+ ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
+
+ MakedictLog.i("Computing addresses...");
+ computeAddresses(dict, flatNodes);
+ MakedictLog.i("Checking array...");
+ if (DBG) checkFlatNodeArray(flatNodes);
+
+ // Create a buffer that matches the final dictionary size.
+ final Node lastNode = flatNodes.get(flatNodes.size() - 1);
+ final int bufferSize =(lastNode.mCachedAddress + lastNode.mCachedSize);
+ final byte[] buffer = new byte[bufferSize];
+ int index = 0;
+
+ MakedictLog.i("Writing file...");
+ int dataEndOffset = 0;
+ for (Node n : flatNodes) {
+ dataEndOffset = writePlacedNode(dict, buffer, n);
+ }
+
+ if (DBG) showStatistics(flatNodes);
+
+ destination.write(buffer, 0, dataEndOffset);
+
+ destination.close();
+ MakedictLog.i("Done");
+ }
+
+
+ // Input methods: Read a binary dictionary to memory.
+ // readDictionaryBinary is the public entry point for them.
+
+ static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
+ private static CharGroupInfo readCharGroup(RandomAccessFile source,
+ final int originalGroupAddress) throws IOException {
+ int addressPointer = originalGroupAddress;
+ final int flags = source.readUnsignedByte();
+ ++addressPointer;
+ final int characters[];
+ if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+ int index = 0;
+ int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ while (-1 != character) {
+ characterBuffer[index++] = character;
+ character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ }
+ characters = Arrays.copyOfRange(characterBuffer, 0, index);
+ } else {
+ final int character = CharEncoding.readChar(source);
+ addressPointer += CharEncoding.getCharSize(character);
+ characters = new int[] { character };
+ }
+ final int frequency;
+ if (0 != (FLAG_IS_TERMINAL & flags)) {
+ ++addressPointer;
+ frequency = source.readUnsignedByte();
+ } else {
+ frequency = CharGroup.NOT_A_TERMINAL;
+ }
+ int childrenAddress = addressPointer;
+ switch (flags & MASK_GROUP_ADDRESS_TYPE) {
+ case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
+ childrenAddress += source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
+ childrenAddress += source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
+ childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
+ addressPointer += 3;
+ break;
+ case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
+ default:
+ childrenAddress = NO_CHILDREN_ADDRESS;
+ break;
+ }
+ ArrayList<WeightedString> shortcutTargets = null;
+ if (0 != (flags & FLAG_HAS_SHORTCUT_TARGETS)) {
+ final long pointerBefore = source.getFilePointer();
+ shortcutTargets = new ArrayList<WeightedString>();
+ source.readUnsignedShort(); // Skip the size
+ while (true) {
+ final int targetFlags = source.readUnsignedByte();
+ final String word = CharEncoding.readString(source);
+ shortcutTargets.add(new WeightedString(word,
+ targetFlags & FLAG_ATTRIBUTE_FREQUENCY));
+ if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ }
+ addressPointer += (source.getFilePointer() - pointerBefore);
+ }
+ ArrayList<PendingAttribute> bigrams = null;
+ if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+ bigrams = new ArrayList<PendingAttribute>();
+ while (true) {
+ final int bigramFlags = source.readUnsignedByte();
+ ++addressPointer;
+ final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+ int bigramAddress = addressPointer;
+ switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+ bigramAddress += sign * source.readUnsignedByte();
+ addressPointer += 1;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+ bigramAddress += sign * source.readUnsignedShort();
+ addressPointer += 2;
+ break;
+ case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
+ final int offset = ((source.readUnsignedByte() << 16)
+ + source.readUnsignedShort());
+ bigramAddress += sign * offset;
+ addressPointer += 3;
+ break;
+ default:
+ throw new RuntimeException("Has bigrams with no address");
+ }
+ bigrams.add(new PendingAttribute(bigramFlags & FLAG_ATTRIBUTE_FREQUENCY,
+ bigramAddress));
+ if (0 == (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ }
+ }
+ return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
+ childrenAddress, shortcutTargets, bigrams);
+ }
+
+ /**
+ * Reads and returns the char group count out of a file and forwards the pointer.
+ */
+ private static int readCharGroupCount(RandomAccessFile source) throws IOException {
+ final int msb = source.readUnsignedByte();
+ if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
+ return msb;
+ } else {
+ return ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
+ + source.readUnsignedByte();
+ }
+ }
+
+ // The word cache here is a stopgap bandaid to help the catastrophic performance
+ // of this method. Since it performs direct, unbuffered random access to the file and
+ // may be called hundreds of thousands of times, the resulting performance is not
+ // reasonable without some kind of cache. Thus:
+ // TODO: perform buffered I/O here and in other places in the code.
+ private static TreeMap<Integer, String> wordCache = new TreeMap<Integer, String>();
+ /**
+ * Finds, as a string, the word at the address passed as an argument.
+ *
+ * @param source the file to read from.
+ * @param headerSize the size of the header.
+ * @param address the address to seek.
+ * @return the word, as a string.
+ * @throws IOException if the file can't be read.
+ */
+ private static String getWordAtAddress(final RandomAccessFile source, final long headerSize,
+ int address) throws IOException {
+ final String cachedString = wordCache.get(address);
+ if (null != cachedString) return cachedString;
+ final long originalPointer = source.getFilePointer();
+ source.seek(headerSize);
+ final int count = readCharGroupCount(source);
+ int groupOffset = getGroupCountSize(count);
+ final StringBuilder builder = new StringBuilder();
+ String result = null;
+
+ CharGroupInfo last = null;
+ for (int i = count - 1; i >= 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ groupOffset = info.mEndAddress;
+ if (info.mOriginalAddress == address) {
+ builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
+ result = builder.toString();
+ break; // and return
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ if (info.mChildrenAddress > address) {
+ if (null == last) continue;
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ last = info;
+ }
+ if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
+ builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
+ source.seek(last.mChildrenAddress + headerSize);
+ groupOffset = last.mChildrenAddress + 1;
+ i = source.readUnsignedByte();
+ last = null;
+ continue;
+ }
+ }
+ source.seek(originalPointer);
+ wordCache.put(address, result);
+ return result;
+ }
+
+ /**
+ * Reads a single node from a binary file.
+ *
+ * This methods reads the file at the current position of its file pointer. A node is
+ * fully expected to start at the current position.
+ * This will recursively read other nodes into the structure, populating the reverse
+ * maps on the fly and using them to keep track of already read nodes.
+ *
+ * @param source the data file, correctly positioned at the start of a node.
+ * @param headerSize the size, in bytes, of the file header.
+ * @param reverseNodeMap a mapping from addresses to already read nodes.
+ * @param reverseGroupMap a mapping from addresses to already read character groups.
+ * @return the read node with all his children already read.
+ */
+ private static Node readNode(RandomAccessFile source, long headerSize,
+ Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+ throws IOException {
+ final int nodeOrigin = (int)(source.getFilePointer() - headerSize);
+ final int count = readCharGroupCount(source);
+ final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
+ int groupOffset = nodeOrigin + getGroupCountSize(count);
+ for (int i = count; i > 0; --i) {
+ CharGroupInfo info = readCharGroup(source, groupOffset);
+ 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(source, headerSize, bigram.mAddress);
+ bigrams.add(new WeightedString(word, bigram.mFrequency));
+ }
+ }
+ if (hasChildrenAddress(info.mChildrenAddress)) {
+ Node children = reverseNodeMap.get(info.mChildrenAddress);
+ if (null == children) {
+ final long currentPosition = source.getFilePointer();
+ source.seek(info.mChildrenAddress + headerSize);
+ children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
+ source.seek(currentPosition);
+ }
+ nodeContents.add(
+ new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
+ children));
+ } else {
+ nodeContents.add(
+ new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency));
+ }
+ groupOffset = info.mEndAddress;
+ }
+ final Node node = new Node(nodeContents);
+ node.mCachedAddress = nodeOrigin;
+ reverseNodeMap.put(node.mCachedAddress, node);
+ return node;
+ }
+
+ /**
+ * Helper function to get the binary format version from the header.
+ */
+ private static int getFormatVersion(final RandomAccessFile source) throws IOException {
+ final int magic_v1 = source.readUnsignedShort();
+ if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte();
+ final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort();
+ if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort();
+ return NOT_A_VERSION_NUMBER;
+ }
+
+ /**
+ * Reads a random access file and returns the memory representation of the dictionary.
+ *
+ * This high-level method takes a binary file and reads its contents, populating a
+ * FusionDictionary structure. The optional dict argument is an existing dictionary to
+ * which words from the file should be added. If it is null, a new dictionary is created.
+ *
+ * @param source the file to read.
+ * @param dict an optional dictionary to add words to, or null.
+ * @return the created (or merged) dictionary.
+ */
+ public static FusionDictionary readDictionaryBinary(final RandomAccessFile source,
+ final FusionDictionary dict) throws IOException, UnsupportedFormatException {
+ // Check file version
+ final int version = getFormatVersion(source);
+ if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) {
+ throw new UnsupportedFormatException("This file has version " + version
+ + ", but this implementation does not support versions above "
+ + MAXIMUM_SUPPORTED_VERSION);
+ }
+
+ // Read options
+ final int optionsFlags = source.readUnsignedShort();
+
+ final long headerSize;
+ final HashMap<String, String> options = new HashMap<String, String>();
+ if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
+ headerSize = source.getFilePointer();
+ } else {
+ headerSize = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16)
+ + (source.readUnsignedByte() << 8) + source.readUnsignedByte();
+ while (source.getFilePointer() < headerSize) {
+ final String key = CharEncoding.readString(source);
+ final String value = CharEncoding.readString(source);
+ options.put(key, value);
+ }
+ source.seek(headerSize);
+ }
+
+ Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
+ Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
+ final Node root = readNode(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+
+ FusionDictionary newDict = new FusionDictionary(root,
+ new FusionDictionary.DictionaryOptions(options,
+ 0 != (optionsFlags & GERMAN_UMLAUT_PROCESSING_FLAG),
+ 0 != (optionsFlags & FRENCH_LIGATURE_PROCESSING_FLAG)));
+ if (null != dict) {
+ for (final Word w : dict) {
+ newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets);
+ }
+ for (final Word w : dict) {
+ // By construction a binary dictionary may not have bigrams pointing to
+ // words that are not also registered as unigrams so we don't have to avoid
+ // them explicitly here.
+ for (final WeightedString bigram : w.mBigrams) {
+ newDict.setBigram(w.mWord, bigram.mWord, bigram.mFrequency);
+ }
+ }
+ }
+
+ return newDict;
+ }
+
+ /**
+ * Basic test to find out whether the file is a binary dictionary or not.
+ *
+ * Concretely this only tests the magic number.
+ *
+ * @param filename The name of the file to test.
+ * @return true if it's a binary dictionary, false otherwise
+ */
+ public static boolean isBinaryDictionary(final String filename) {
+ try {
+ RandomAccessFile f = new RandomAccessFile(filename, "r");
+ final int version = getFormatVersion(f);
+ return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
new file mode 100644
index 000000000..ef7dbb251
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 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.makedict.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+/**
+ * Raw char group info straight out of a file. This will contain numbers for addresses.
+ */
+public class CharGroupInfo {
+
+ public final int mOriginalAddress;
+ public final int mEndAddress;
+ public final int mFlags;
+ public final int[] mCharacters;
+ public final int mFrequency;
+ public final int mChildrenAddress;
+ public final ArrayList<WeightedString> mShortcutTargets;
+ public final ArrayList<PendingAttribute> mBigrams;
+
+ public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+ final int[] characters, final int frequency, final int childrenAddress,
+ final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<PendingAttribute> bigrams) {
+ mOriginalAddress = originalAddress;
+ mEndAddress = endAddress;
+ mFlags = flags;
+ mCharacters = characters;
+ mFrequency = frequency;
+ mChildrenAddress = childrenAddress;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
new file mode 100644
index 000000000..8b53c9427
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -0,0 +1,767 @@
+/*
+ * Copyright (C) 2011 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * A dictionary that can fusion heads and tails of words for more compression.
+ */
+public class FusionDictionary implements Iterable<Word> {
+
+ private static final boolean DBG = MakedictLog.DBG;
+
+ /**
+ * A node of the dictionary, containing several CharGroups.
+ *
+ * A node is but an ordered array of CharGroups, which essentially contain all the
+ * real information.
+ * This class also contains fields to cache size and address, to help with binary
+ * generation.
+ */
+ public static class Node {
+ ArrayList<CharGroup> mData;
+ // To help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+ public Node() {
+ mData = new ArrayList<CharGroup>();
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ public Node(ArrayList<CharGroup> data) {
+ mData = data;
+ mCachedSize = Integer.MIN_VALUE;
+ mCachedAddress = Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * A string with a frequency.
+ *
+ * This represents an "attribute", that is either a bigram or a shortcut.
+ */
+ public static class WeightedString {
+ final String mWord;
+ int mFrequency;
+ public WeightedString(String word, int frequency) {
+ mWord = word;
+ mFrequency = frequency;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] { mWord, mFrequency });
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof WeightedString)) return false;
+ WeightedString w = (WeightedString)o;
+ return mWord.equals(w.mWord) && mFrequency == w.mFrequency;
+ }
+ }
+
+ /**
+ * A group of characters, with a frequency, shortcut targets, bigrams, and children.
+ *
+ * This is the central class of the in-memory representation. A CharGroup is what can
+ * be seen as a traditional "trie node", except it can hold several characters at the
+ * same time. A CharGroup essentially represents one or several characters in the middle
+ * of the trie trie; as such, it can be a terminal, and it can have children.
+ * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+ * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other
+ * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or
+ * bigrams, but a non-terminal may not. Moreover, children, if present, are null.
+ */
+ public static class CharGroup {
+ public static final int NOT_A_TERMINAL = -1;
+ final int mChars[];
+ ArrayList<WeightedString> mShortcutTargets;
+ ArrayList<WeightedString> mBigrams;
+ int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
+ Node mChildren;
+ // The two following members to help with binary generation
+ int mCachedSize;
+ int mCachedAddress;
+
+ public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams, final int frequency) {
+ mChars = chars;
+ mFrequency = frequency;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ mChildren = null;
+ }
+
+ public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams, final int frequency, final Node children) {
+ mChars = chars;
+ mFrequency = frequency;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ mChildren = children;
+ }
+
+ public void addChild(CharGroup n) {
+ if (null == mChildren) {
+ mChildren = new Node();
+ }
+ mChildren.mData.add(n);
+ }
+
+ public boolean isTerminal() {
+ return NOT_A_TERMINAL != mFrequency;
+ }
+
+ public boolean hasSeveralChars() {
+ assert(mChars.length > 0);
+ return 1 < mChars.length;
+ }
+
+ /**
+ * Adds a word to the bigram list. Updates the frequency if the word already
+ * exists.
+ */
+ public void addBigram(final String word, final int frequency) {
+ if (mBigrams == null) {
+ mBigrams = new ArrayList<WeightedString>();
+ }
+ WeightedString bigram = getBigram(word);
+ if (bigram != null) {
+ bigram.mFrequency = frequency;
+ } else {
+ bigram = new WeightedString(word, frequency);
+ mBigrams.add(bigram);
+ }
+ }
+
+ /**
+ * Gets the shortcut target for the given word. Returns null if the word is not in the
+ * shortcut list.
+ */
+ public WeightedString getShortcut(final String word) {
+ // TODO: Don't do a linear search
+ if (mShortcutTargets != null) {
+ final int size = mShortcutTargets.size();
+ for (int i = 0; i < size; ++i) {
+ WeightedString shortcut = mShortcutTargets.get(i);
+ if (shortcut.mWord.equals(word)) {
+ return shortcut;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the bigram for the given word.
+ * Returns null if the word is not in the bigrams list.
+ */
+ public WeightedString getBigram(final String word) {
+ // TODO: Don't do a linear search
+ if (mBigrams != null) {
+ final int size = mBigrams.size();
+ for (int i = 0; i < size; ++i) {
+ WeightedString bigram = mBigrams.get(i);
+ if (bigram.mWord.equals(word)) {
+ return bigram;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates the CharGroup with the given properties. Adds the shortcut and bigram lists to
+ * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only
+ * updated if they are higher than the existing ones.
+ */
+ public void update(int frequency, ArrayList<WeightedString> shortcutTargets,
+ ArrayList<WeightedString> bigrams) {
+ if (frequency > mFrequency) {
+ mFrequency = frequency;
+ }
+ if (shortcutTargets != null) {
+ if (mShortcutTargets == null) {
+ mShortcutTargets = shortcutTargets;
+ } else {
+ final int size = shortcutTargets.size();
+ for (int i = 0; i < size; ++i) {
+ final WeightedString shortcut = shortcutTargets.get(i);
+ final WeightedString existingShortcut = getShortcut(shortcut.mWord);
+ if (existingShortcut == null) {
+ mShortcutTargets.add(shortcut);
+ } else if (existingShortcut.mFrequency < shortcut.mFrequency) {
+ existingShortcut.mFrequency = shortcut.mFrequency;
+ }
+ }
+ }
+ }
+ if (bigrams != null) {
+ if (mBigrams == null) {
+ mBigrams = bigrams;
+ } else {
+ final int size = bigrams.size();
+ for (int i = 0; i < size; ++i) {
+ final WeightedString bigram = bigrams.get(i);
+ final WeightedString existingBigram = getBigram(bigram.mWord);
+ if (existingBigram == null) {
+ mBigrams.add(bigram);
+ } else if (existingBigram.mFrequency < bigram.mFrequency) {
+ existingBigram.mFrequency = bigram.mFrequency;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Options global to the dictionary.
+ *
+ * There are no options at the moment, so this class is empty.
+ */
+ public static class DictionaryOptions {
+ public final boolean mGermanUmlautProcessing;
+ public final boolean mFrenchLigatureProcessing;
+ public final HashMap<String, String> mAttributes;
+ public DictionaryOptions(final HashMap<String, String> attributes,
+ final boolean germanUmlautProcessing, final boolean frenchLigatureProcessing) {
+ mAttributes = attributes;
+ mGermanUmlautProcessing = germanUmlautProcessing;
+ mFrenchLigatureProcessing = frenchLigatureProcessing;
+ }
+ }
+
+ public final DictionaryOptions mOptions;
+ public final Node mRoot;
+
+ public FusionDictionary(final Node root, final DictionaryOptions options) {
+ mRoot = root;
+ mOptions = options;
+ }
+
+ public void addOptionAttribute(final String key, final String value) {
+ mOptions.mAttributes.put(key, value);
+ }
+
+ /**
+ * Helper method to convert a String to an int array.
+ */
+ static private int[] getCodePoints(final String word) {
+ // TODO: this is a copy-paste of the contents of StringUtils.toCodePointArray,
+ // which is not visible from the makedict package. Factor this code.
+ final char[] characters = word.toCharArray();
+ final int length = characters.length;
+ final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
+ int codePoint = Character.codePointAt(characters, 0);
+ int dsti = 0;
+ for (int srci = Character.charCount(codePoint);
+ srci < length; srci += Character.charCount(codePoint), ++dsti) {
+ codePoints[dsti] = codePoint;
+ codePoint = Character.codePointAt(characters, srci);
+ }
+ codePoints[dsti] = codePoint;
+ return codePoints;
+ }
+
+ /**
+ * Helper method to add a word as a string.
+ *
+ * This method adds a word to the dictionary with the given frequency. Optional
+ * lists of bigrams and shortcuts can be passed here. For each word inside,
+ * they will be added to the dictionary as necessary.
+ *
+ * @param word the word to add.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param shortcutTargets a list of shortcut targets for this word, or null.
+ */
+ public void add(final String word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets) {
+ add(getCodePoints(word), frequency, shortcutTargets);
+ }
+
+ /**
+ * Sanity check for a node.
+ *
+ * This method checks that all CharGroups in a node are ordered as expected.
+ * If they are, nothing happens. If they aren't, an exception is thrown.
+ */
+ private void checkStack(Node node) {
+ ArrayList<CharGroup> stack = node.mData;
+ int lastValue = -1;
+ for (int i = 0; i < stack.size(); ++i) {
+ int currentValue = stack.get(i).mChars[0];
+ if (currentValue <= lastValue)
+ throw new RuntimeException("Invalid stack");
+ else
+ lastValue = currentValue;
+ }
+ }
+
+ /**
+ * Helper method to add a new bigram to the dictionary.
+ *
+ * @param word1 the previous word of the context
+ * @param word2 the next word of the context
+ * @param frequency the bigram frequency
+ */
+ public void setBigram(final String word1, final String word2, final int frequency) {
+ CharGroup charGroup = findWordInTree(mRoot, word1);
+ if (charGroup != null) {
+ final CharGroup charGroup2 = findWordInTree(mRoot, word2);
+ if (charGroup2 == null) {
+ add(getCodePoints(word2), 0, null);
+ }
+ charGroup.addBigram(word2, frequency);
+ } else {
+ throw new RuntimeException("First word of bigram not found");
+ }
+ }
+
+ /**
+ * Add a word to this dictionary.
+ *
+ * The shortcuts, if any, have to be in the dictionary already. If they aren't,
+ * an exception is thrown.
+ *
+ * @param word the word, as an int array.
+ * @param frequency the frequency of the word, in the range [0..255].
+ * @param shortcutTargets an optional list of shortcut targets for this word (null if none).
+ */
+ private void add(final int[] word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets) {
+ assert(frequency >= 0 && frequency <= 255);
+ Node currentNode = mRoot;
+ int charIndex = 0;
+
+ CharGroup currentGroup = null;
+ int differentCharIndex = 0; // Set by the loop to the index of the char that differs
+ int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
+ while (CHARACTER_NOT_FOUND != nodeIndex) {
+ currentGroup = currentNode.mData.get(nodeIndex);
+ differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex);
+ if (ARRAYS_ARE_EQUAL != differentCharIndex
+ && differentCharIndex < currentGroup.mChars.length) break;
+ if (null == currentGroup.mChildren) break;
+ charIndex += currentGroup.mChars.length;
+ if (charIndex >= word.length) break;
+ currentNode = currentGroup.mChildren;
+ nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+ }
+
+ if (-1 == nodeIndex) {
+ // No node at this point to accept the word. Create one.
+ final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
+ final CharGroup newGroup = new CharGroup(
+ Arrays.copyOfRange(word, charIndex, word.length),
+ shortcutTargets, null /* bigrams */, frequency);
+ currentNode.mData.add(insertionIndex, newGroup);
+ if (DBG) checkStack(currentNode);
+ } else {
+ // There is a word with a common prefix.
+ if (differentCharIndex == currentGroup.mChars.length) {
+ if (charIndex + differentCharIndex >= word.length) {
+ // The new word is a prefix of an existing word, but the node on which it
+ // should end already exists as is. Since the old CharNode was not a terminal,
+ // make it one by filling in its frequency and other attributes
+ currentGroup.update(frequency, shortcutTargets, null);
+ } else {
+ // The new word matches the full old word and extends past it.
+ // We only have to create a new node and add it to the end of this.
+ final CharGroup newNode = new CharGroup(
+ Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
+ shortcutTargets, null /* bigrams */, frequency);
+ currentGroup.mChildren = new Node();
+ currentGroup.mChildren.mData.add(newNode);
+ }
+ } else {
+ if (0 == differentCharIndex) {
+ // Exact same word. Update the frequency if higher. This will also add the
+ // new shortcuts to the existing shortcut list if it already exists.
+ currentGroup.update(frequency, shortcutTargets, null);
+ } else {
+ // Partial prefix match only. We have to replace the current node with a node
+ // containing the current prefix and create two new ones for the tails.
+ Node newChildren = new Node();
+ final CharGroup newOldWord = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
+ currentGroup.mChars.length), currentGroup.mShortcutTargets,
+ currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren);
+ newChildren.mData.add(newOldWord);
+
+ final CharGroup newParent;
+ if (charIndex + differentCharIndex >= word.length) {
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ shortcutTargets, null /* bigrams */, frequency, newChildren);
+ } else {
+ newParent = new CharGroup(
+ Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ null /* shortcutTargets */, null /* bigrams */, -1, newChildren);
+ final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word,
+ charIndex + differentCharIndex, word.length),
+ shortcutTargets, null /* bigrams */, frequency);
+ final int addIndex = word[charIndex + differentCharIndex]
+ > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+ newChildren.mData.add(addIndex, newWord);
+ }
+ currentNode.mData.set(nodeIndex, newParent);
+ }
+ if (DBG) checkStack(currentNode);
+ }
+ }
+ }
+
+ private static int ARRAYS_ARE_EQUAL = 0;
+
+ /**
+ * Custom comparison of two int arrays taken to contain character codes.
+ *
+ * This method compares the two arrays passed as an argument in a lexicographic way,
+ * with an offset in the dst string.
+ * This method does NOT test for the first character. It is taken to be equal.
+ * I repeat: this method starts the comparison at 1 <> dstOffset + 1.
+ * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the
+ * strings are equal. This works BECAUSE we don't look at the first character.
+ *
+ * @param src the left-hand side string of the comparison.
+ * @param dst the right-hand side string of the comparison.
+ * @param dstOffset the offset in the right-hand side string.
+ * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't.
+ */
+ private static int compareArrays(final int[] src, final int[] dst, int dstOffset) {
+ // We do NOT test the first char, because we come from a method that already
+ // tested it.
+ for (int i = 1; i < src.length; ++i) {
+ if (dstOffset + i >= dst.length) return i;
+ if (src[i] != dst[dstOffset + i]) return i;
+ }
+ if (dst.length > src.length) return src.length;
+ return ARRAYS_ARE_EQUAL;
+ }
+
+ /**
+ * Helper class that compares and sorts two chargroups according to their
+ * first element only. I repeat: ONLY the first element is considered, the rest
+ * is ignored.
+ * This comparator imposes orderings that are inconsistent with equals.
+ */
+ static private class CharGroupComparator implements java.util.Comparator<CharGroup> {
+ @Override
+ public int compare(CharGroup c1, CharGroup c2) {
+ if (c1.mChars[0] == c2.mChars[0]) return 0;
+ return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+ }
+ }
+ final static private CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+
+ /**
+ * Finds the insertion index of a character within a node.
+ */
+ private static int findInsertionIndex(final Node node, int character) {
+ final ArrayList<CharGroup> data = node.mData;
+ final CharGroup reference = new CharGroup(new int[] { character },
+ null /* shortcutTargets */, null /* bigrams */, 0);
+ int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
+ return result >= 0 ? result : -result - 1;
+ }
+
+ private static int CHARACTER_NOT_FOUND = -1;
+
+ /**
+ * Find the index of a char in a node, if it exists.
+ *
+ * @param node the node to search in.
+ * @param character the character to search for.
+ * @return the position of the character if it's there, or CHARACTER_NOT_FOUND = -1 else.
+ */
+ private static int findIndexOfChar(final Node node, int character) {
+ final int insertionIndex = findInsertionIndex(node, character);
+ if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND;
+ return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
+ : CHARACTER_NOT_FOUND;
+ }
+
+ /**
+ * Helper method to find a word in a given branch.
+ */
+ public static CharGroup findWordInTree(Node node, final String s) {
+ int index = 0;
+ final StringBuilder checker = DBG ? new StringBuilder() : null;
+
+ CharGroup currentGroup;
+ do {
+ int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
+ if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
+ currentGroup = node.mData.get(indexOfGroup);
+ if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
+ index += currentGroup.mChars.length;
+ if (index < s.length()) {
+ node = currentGroup.mChildren;
+ }
+ } while (null != node && index < s.length());
+
+ if (DBG && !s.equals(checker.toString())) return null;
+ return currentGroup;
+ }
+
+ /**
+ * Helper method to find out whether a word is in the dict or not.
+ */
+ public boolean hasWord(final String s) {
+ if (null == s || "".equals(s)) {
+ throw new RuntimeException("Can't search for a null or empty string");
+ }
+ return null != findWordInTree(mRoot, s);
+ }
+
+ /**
+ * Recursively count the number of character groups in a given branch of the trie.
+ *
+ * @param node the parent node.
+ * @return the number of char groups in all the branch under this node.
+ */
+ public static int countCharGroups(final Node node) {
+ final int nodeSize = node.mData.size();
+ int size = nodeSize;
+ for (int i = nodeSize - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countCharGroups(group.mChildren);
+ }
+ return size;
+ }
+
+ /**
+ * Recursively count the number of nodes in a given branch of the trie.
+ *
+ * @param node the node to count.
+ * @return the number of nodes in this branch.
+ */
+ public static int countNodes(final Node node) {
+ int size = 1;
+ for (int i = node.mData.size() - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mChildren)
+ size += countNodes(group.mChildren);
+ }
+ return size;
+ }
+
+ // Recursively find out whether there are any bigrams.
+ // This can be pretty expensive especially if there aren't any (we return as soon
+ // as we find one, so it's much cheaper if there are bigrams)
+ private static boolean hasBigramsInternal(final Node node) {
+ if (null == node) return false;
+ for (int i = node.mData.size() - 1; i >= 0; --i) {
+ CharGroup group = node.mData.get(i);
+ if (null != group.mBigrams) return true;
+ if (hasBigramsInternal(group.mChildren)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Finds out whether there are any bigrams in this dictionary.
+ *
+ * @return true if there is any bigram, false otherwise.
+ */
+ // TODO: this is expensive especially for large dictionaries without any bigram.
+ // The up side is, this is always accurate and correct and uses no memory. We should
+ // find a more efficient way of doing this, without compromising too much on memory
+ // and ease of use.
+ public boolean hasBigrams() {
+ return hasBigramsInternal(mRoot);
+ }
+
+ // Historically, the tails of the words were going to be merged to save space.
+ // However, that would prevent the code to search for a specific address in log(n)
+ // time so this was abandoned.
+ // The code is still of interest as it does add some compression to any dictionary
+ // that has no need for attributes. Implementations that does not read attributes should be
+ // able to read a dictionary with merged tails.
+ // Also, the following code does support frequencies, as in, it will only merges
+ // tails that share the same frequency. Though it would result in the above loss of
+ // performance while searching by address, it is still technically possible to merge
+ // tails that contain attributes, but this code does not take that into account - it does
+ // not compare attributes and will merge terminals with different attributes regardless.
+ public void mergeTails() {
+ MakedictLog.i("Do not merge tails");
+ return;
+
+// MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
+// MakedictLog.i("Number of groups : " + countCharGroups(root));
+//
+// final HashMap<String, ArrayList<Node>> repository =
+// new HashMap<String, ArrayList<Node>>();
+// mergeTailsInner(repository, root);
+//
+// MakedictLog.i("Number of different pseudohashes : " + repository.size());
+// int size = 0;
+// for (ArrayList<Node> a : repository.values()) {
+// size += a.size();
+// }
+// MakedictLog.i("Number of nodes after merge : " + (1 + size));
+// MakedictLog.i("Recursively seen nodes : " + countNodes(root));
+ }
+
+ // The following methods are used by the deactivated mergeTails()
+// private static boolean isEqual(Node a, Node b) {
+// if (null == a && null == b) return true;
+// if (null == a || null == b) return false;
+// if (a.data.size() != b.data.size()) return false;
+// final int size = a.data.size();
+// for (int i = size - 1; i >= 0; --i) {
+// CharGroup aGroup = a.data.get(i);
+// CharGroup bGroup = b.data.get(i);
+// if (aGroup.frequency != bGroup.frequency) return false;
+// if (aGroup.alternates == null && bGroup.alternates != null) return false;
+// if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
+// if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
+// if (!isEqual(aGroup.children, bGroup.children)) return false;
+// }
+// return true;
+// }
+
+// static private HashMap<String, ArrayList<Node>> mergeTailsInner(
+// final HashMap<String, ArrayList<Node>> map, final Node node) {
+// final ArrayList<CharGroup> branches = node.data;
+// final int nodeSize = branches.size();
+// for (int i = 0; i < nodeSize; ++i) {
+// CharGroup group = branches.get(i);
+// if (null != group.children) {
+// String pseudoHash = getPseudoHash(group.children);
+// ArrayList<Node> similarList = map.get(pseudoHash);
+// if (null == similarList) {
+// similarList = new ArrayList<Node>();
+// map.put(pseudoHash, similarList);
+// }
+// boolean merged = false;
+// for (Node similar : similarList) {
+// if (isEqual(group.children, similar)) {
+// group.children = similar;
+// merged = true;
+// break;
+// }
+// }
+// if (!merged) {
+// similarList.add(group.children);
+// }
+// mergeTailsInner(map, group.children);
+// }
+// }
+// return map;
+// }
+
+// private static String getPseudoHash(final Node node) {
+// StringBuilder s = new StringBuilder();
+// for (CharGroup g : node.data) {
+// s.append(g.frequency);
+// for (int ch : g.chars){
+// s.append(Character.toChars(ch));
+// }
+// }
+// return s.toString();
+// }
+
+ /**
+ * Iterator to walk through a dictionary.
+ *
+ * This is purely for convenience.
+ */
+ public static class DictionaryIterator implements Iterator<Word> {
+
+ private static class Position {
+ public Iterator<CharGroup> pos;
+ public int length;
+ public Position(ArrayList<CharGroup> groups) {
+ pos = groups.iterator();
+ length = 0;
+ }
+ }
+ final StringBuilder mCurrentString;
+ final LinkedList<Position> mPositions;
+
+ public DictionaryIterator(ArrayList<CharGroup> root) {
+ mCurrentString = new StringBuilder();
+ mPositions = new LinkedList<Position>();
+ final Position rootPos = new Position(root);
+ mPositions.add(rootPos);
+ }
+
+ @Override
+ public boolean hasNext() {
+ for (Position p : mPositions) {
+ if (p.pos.hasNext()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Word next() {
+ Position currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - currentPos.length);
+
+ do {
+ if (currentPos.pos.hasNext()) {
+ final CharGroup currentGroup = currentPos.pos.next();
+ currentPos.length = currentGroup.mChars.length;
+ for (int i : currentGroup.mChars)
+ mCurrentString.append(Character.toChars(i));
+ if (null != currentGroup.mChildren) {
+ currentPos = new Position(currentGroup.mChildren.mData);
+ mPositions.addLast(currentPos);
+ }
+ if (currentGroup.mFrequency >= 0)
+ return new Word(mCurrentString.toString(), currentGroup.mFrequency,
+ currentGroup.mShortcutTargets, currentGroup.mBigrams);
+ } else {
+ mPositions.removeLast();
+ currentPos = mPositions.getLast();
+ mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
+ }
+ } while(true);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Unsupported yet");
+ }
+
+ }
+
+ /**
+ * Method to return an iterator.
+ *
+ * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x
+ * and say : for (Word w : x) {}
+ */
+ @Override
+ public Iterator<Word> iterator() {
+ return new DictionaryIterator(mRoot.mData);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
new file mode 100644
index 000000000..3f0cd0796
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 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 android.util.Log;
+
+/**
+ * Wrapper to redirect log events to the right output medium.
+ */
+public class MakedictLog {
+ public static final boolean DBG = false;
+ private static final String TAG = MakedictLog.class.getSimpleName();
+
+ public static void d(String message) {
+ if (DBG) {
+ Log.d(TAG, message);
+ }
+ }
+
+ public static void i(String message) {
+ if (DBG) {
+ Log.i(TAG, message);
+ }
+ }
+
+ public static void w(String message) {
+ Log.w(TAG, message);
+ }
+
+ public static void e(String message) {
+ Log.e(TAG, message);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
new file mode 100644
index 000000000..5b41d27f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * A not-yet-resolved attribute.
+ *
+ * An attribute is either a bigram or a shortcut.
+ * All instances of this class are always immutable.
+ */
+public class PendingAttribute {
+ public final int mFrequency;
+ public final int mAddress;
+ public PendingAttribute(final int frequency, final int address) {
+ mFrequency = frequency;
+ mAddress = address;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
new file mode 100644
index 000000000..bd42fb8fa
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 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;
+
+/**
+ * Simple exception thrown when a file format is not recognized.
+ */
+public class UnsupportedFormatException extends Exception {
+ public UnsupportedFormatException(String description) {
+ super(description);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java
new file mode 100644
index 000000000..d07826757
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Word.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 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.makedict.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Utility class for a word with a frequency.
+ *
+ * This is chiefly used to iterate a dictionary.
+ */
+public class Word implements Comparable<Word> {
+ final String mWord;
+ final int mFrequency;
+ final ArrayList<WeightedString> mShortcutTargets;
+ final ArrayList<WeightedString> mBigrams;
+
+ private int mHashCode = 0;
+
+ public Word(final String word, final int frequency,
+ final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams) {
+ mWord = word;
+ mFrequency = frequency;
+ mShortcutTargets = shortcutTargets;
+ mBigrams = bigrams;
+ }
+
+ private static int computeHashCode(Word word) {
+ return Arrays.hashCode(new Object[] {
+ word.mWord,
+ word.mFrequency,
+ word.mShortcutTargets.hashCode(),
+ word.mBigrams.hashCode()
+ });
+ }
+
+ /**
+ * Three-way comparison.
+ *
+ * A Word x is greater than a word y if x has a higher frequency. If they have the same
+ * frequency, they are sorted in lexicographic order.
+ */
+ @Override
+ public int compareTo(Word w) {
+ if (mFrequency < w.mFrequency) return 1;
+ if (mFrequency > w.mFrequency) return -1;
+ return mWord.compareTo(w.mWord);
+ }
+
+ /**
+ * Equality test.
+ *
+ * Words are equal if they have the same frequency, the same spellings, and the same
+ * attributes.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof Word)) return false;
+ Word w = (Word)o;
+ return mFrequency == w.mFrequency && mWord.equals(w.mWord)
+ && mShortcutTargets.equals(w.mShortcutTargets)
+ && mBigrams.equals(w.mBigrams);
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHashCode == 0) {
+ mHashCode = computeHashCode(this);
+ }
+ return mHashCode;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
new file mode 100644
index 000000000..7fffc31c0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -0,0 +1,837 @@
+/*
+ * Copyright (C) 2011 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.spellcheck;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.service.textservice.SpellCheckerService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.ContactsBinaryDictionary;
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.Dictionary.WordCallback;
+import com.android.inputmethod.latin.DictionaryCollection;
+import com.android.inputmethod.latin.DictionaryFactory;
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
+import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
+import com.android.inputmethod.latin.UserBinaryDictionary;
+import com.android.inputmethod.latin.WhitelistDictionary;
+import com.android.inputmethod.latin.WordComposer;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Service for spell checking, using LatinIME's dictionaries and mechanisms.
+ */
+public class AndroidSpellCheckerService extends SpellCheckerService
+ implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final int POOL_SIZE = 2;
+
+ public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
+
+ private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
+ private static final int CAPITALIZE_FIRST = 1; // First only
+ private static final int CAPITALIZE_ALL = 2; // All caps
+
+ private final static String[] EMPTY_STRING_ARRAY = new String[0];
+ private Map<String, DictionaryPool> mDictionaryPools =
+ Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
+ private Map<String, UserBinaryDictionary> mUserDictionaries =
+ Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>());
+ private Map<String, Dictionary> mWhitelistDictionaries =
+ Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+ private ContactsBinaryDictionary mContactsDictionary;
+
+ // The threshold for a candidate to be offered as a suggestion.
+ private float mSuggestionThreshold;
+ // The threshold for a suggestion to be considered "recommended".
+ private float mRecommendedThreshold;
+ // Whether to use the contacts dictionary
+ private boolean mUseContactsDictionary;
+ private final Object mUseContactsLock = new Object();
+
+ private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
+ new HashSet<WeakReference<DictionaryCollection>>();
+
+ public static final int SCRIPT_LATIN = 0;
+ public static final int SCRIPT_CYRILLIC = 1;
+ private static final String SINGLE_QUOTE = "\u0027";
+ private static final String APOSTROPHE = "\u2019";
+ private static final TreeMap<String, Integer> mLanguageToScript;
+ static {
+ // List of the supported languages and their associated script. We won't check
+ // words written in another script than the selected script, because we know we
+ // don't have those in our dictionary so we will underline everything and we
+ // will never have any suggestions, so it makes no sense checking them, and this
+ // is done in {@link #shouldFilterOut}. Also, the script is used to choose which
+ // proximity to pass to the dictionary descent algorithm.
+ // IMPORTANT: this only contains languages - do not write countries in there.
+ // Only the language is searched from the map.
+ mLanguageToScript = new TreeMap<String, Integer>();
+ mLanguageToScript.put("en", SCRIPT_LATIN);
+ mLanguageToScript.put("fr", SCRIPT_LATIN);
+ mLanguageToScript.put("de", SCRIPT_LATIN);
+ mLanguageToScript.put("nl", SCRIPT_LATIN);
+ mLanguageToScript.put("cs", SCRIPT_LATIN);
+ mLanguageToScript.put("es", SCRIPT_LATIN);
+ mLanguageToScript.put("it", SCRIPT_LATIN);
+ mLanguageToScript.put("hr", SCRIPT_LATIN);
+ mLanguageToScript.put("pt", SCRIPT_LATIN);
+ mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
+ // TODO: Make a persian proximity, and activate the Farsi subtype.
+ // mLanguageToScript.put("fa", SCRIPT_PERSIAN);
+ }
+
+ @Override public void onCreate() {
+ super.onCreate();
+ mSuggestionThreshold =
+ Float.parseFloat(getString(R.string.spellchecker_suggestion_threshold_value));
+ mRecommendedThreshold =
+ Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value));
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
+ }
+
+ private static int getScriptFromLocale(final Locale locale) {
+ final Integer script = mLanguageToScript.get(locale.getLanguage());
+ if (null == script) {
+ throw new RuntimeException("We have been called with an unsupported language: \""
+ + locale.getLanguage() + "\". Framework bug?");
+ }
+ return script;
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+ if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
+ synchronized(mUseContactsLock) {
+ mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
+ if (mUseContactsDictionary) {
+ startUsingContactsDictionaryLocked();
+ } else {
+ stopUsingContactsDictionaryLocked();
+ }
+ }
+ }
+
+ private void startUsingContactsDictionaryLocked() {
+ if (null == mContactsDictionary) {
+ // TODO: use the right locale for each session
+ mContactsDictionary =
+ new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
+ }
+ final Iterator<WeakReference<DictionaryCollection>> iterator =
+ mDictionaryCollectionsList.iterator();
+ while (iterator.hasNext()) {
+ final WeakReference<DictionaryCollection> dictRef = iterator.next();
+ final DictionaryCollection dict = dictRef.get();
+ if (null == dict) {
+ iterator.remove();
+ } else {
+ dict.addDictionary(mContactsDictionary);
+ }
+ }
+ }
+
+ private void stopUsingContactsDictionaryLocked() {
+ if (null == mContactsDictionary) return;
+ final Dictionary contactsDict = mContactsDictionary;
+ // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no longer needed
+ mContactsDictionary = null;
+ final Iterator<WeakReference<DictionaryCollection>> iterator =
+ mDictionaryCollectionsList.iterator();
+ while (iterator.hasNext()) {
+ final WeakReference<DictionaryCollection> dictRef = iterator.next();
+ final DictionaryCollection dict = dictRef.get();
+ if (null == dict) {
+ iterator.remove();
+ } else {
+ dict.removeDictionary(contactsDict);
+ }
+ }
+ contactsDict.close();
+ }
+
+ @Override
+ public Session createSession() {
+ return new AndroidSpellCheckerSession(this);
+ }
+
+ private static SuggestionsInfo getNotInDictEmptySuggestions() {
+ return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
+ }
+
+ private static SuggestionsInfo getInDictEmptySuggestions() {
+ return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
+ EMPTY_STRING_ARRAY);
+ }
+
+ private static class SuggestionsGatherer implements WordCallback {
+ public static class Result {
+ public final String[] mSuggestions;
+ public final boolean mHasRecommendedSuggestions;
+ public Result(final String[] gatheredSuggestions,
+ final boolean hasRecommendedSuggestions) {
+ mSuggestions = gatheredSuggestions;
+ mHasRecommendedSuggestions = hasRecommendedSuggestions;
+ }
+ }
+
+ private final ArrayList<CharSequence> mSuggestions;
+ private final int[] mScores;
+ private final String mOriginalText;
+ private final float mSuggestionThreshold;
+ private final float mRecommendedThreshold;
+ private final int mMaxLength;
+ private int mLength = 0;
+
+ // The two following attributes are only ever filled if the requested max length
+ // is 0 (or less, which is treated the same).
+ private String mBestSuggestion = null;
+ private int mBestScore = Integer.MIN_VALUE; // As small as possible
+
+ SuggestionsGatherer(final String originalText, final float suggestionThreshold,
+ final float recommendedThreshold, final int maxLength) {
+ mOriginalText = originalText;
+ mSuggestionThreshold = suggestionThreshold;
+ mRecommendedThreshold = recommendedThreshold;
+ mMaxLength = maxLength;
+ mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
+ mScores = new int[mMaxLength];
+ }
+
+ @Override
+ synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
+ int dicTypeId, int dataType) {
+ final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
+ // binarySearch returns the index if the element exists, and -<insertion index> - 1
+ // if it doesn't. See documentation for binarySearch.
+ final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
+
+ if (insertIndex == 0 && mLength >= mMaxLength) {
+ // In the future, we may want to keep track of the best suggestion score even if
+ // we are asked for 0 suggestions. In this case, we can use the following
+ // (tested) code to keep it:
+ // If the maxLength is 0 (should never be less, but if it is, it's treated as 0)
+ // then we need to keep track of the best suggestion in mBestScore and
+ // mBestSuggestion. This is so that we know whether the best suggestion makes
+ // the score cutoff, since we need to know that to return a meaningful
+ // looksLikeTypo.
+ // if (0 >= mMaxLength) {
+ // if (score > mBestScore) {
+ // mBestScore = score;
+ // mBestSuggestion = new String(word, wordOffset, wordLength);
+ // }
+ // }
+ return true;
+ }
+ if (insertIndex >= mMaxLength) {
+ // We found a suggestion, but its score is too weak to be kept considering
+ // the suggestion limit.
+ return true;
+ }
+
+ // Compute the normalized score and skip this word if it's normalized score does not
+ // make the threshold.
+ final String wordString = new String(word, wordOffset, wordLength);
+ final float normalizedScore =
+ BinaryDictionary.calcNormalizedScore(mOriginalText, wordString, score);
+ if (normalizedScore < mSuggestionThreshold) {
+ if (DBG) Log.i(TAG, wordString + " does not make the score threshold");
+ return true;
+ }
+
+ if (mLength < mMaxLength) {
+ final int copyLen = mLength - insertIndex;
+ ++mLength;
+ System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
+ mSuggestions.add(insertIndex, wordString);
+ } else {
+ System.arraycopy(mScores, 1, mScores, 0, insertIndex);
+ mSuggestions.add(insertIndex, wordString);
+ mSuggestions.remove(0);
+ }
+ mScores[insertIndex] = score;
+
+ return true;
+ }
+
+ public Result getResults(final int capitalizeType, final Locale locale) {
+ final String[] gatheredSuggestions;
+ final boolean hasRecommendedSuggestions;
+ if (0 == mLength) {
+ // Either we found no suggestions, or we found some BUT the max length was 0.
+ // If we found some mBestSuggestion will not be null. If it is null, then
+ // we found none, regardless of the max length.
+ if (null == mBestSuggestion) {
+ gatheredSuggestions = null;
+ hasRecommendedSuggestions = false;
+ } else {
+ gatheredSuggestions = EMPTY_STRING_ARRAY;
+ final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+ mOriginalText, mBestSuggestion, mBestScore);
+ hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
+ }
+ } else {
+ if (DBG) {
+ if (mLength != mSuggestions.size()) {
+ Log.e(TAG, "Suggestion size is not the same as stored mLength");
+ }
+ for (int i = mLength - 1; i >= 0; --i) {
+ Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i));
+ }
+ }
+ Collections.reverse(mSuggestions);
+ StringUtils.removeDupes(mSuggestions);
+ if (CAPITALIZE_ALL == capitalizeType) {
+ for (int i = 0; i < mSuggestions.size(); ++i) {
+ // get(i) returns a CharSequence which is actually a String so .toString()
+ // should return the same object.
+ mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale));
+ }
+ } else if (CAPITALIZE_FIRST == capitalizeType) {
+ for (int i = 0; i < mSuggestions.size(); ++i) {
+ // Likewise
+ mSuggestions.set(i, StringUtils.toTitleCase(
+ mSuggestions.get(i).toString(), locale));
+ }
+ }
+ // This returns a String[], while toArray() returns an Object[] which cannot be cast
+ // into a String[].
+ gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY);
+
+ final int bestScore = mScores[mLength - 1];
+ final CharSequence bestSuggestion = mSuggestions.get(0);
+ final float normalizedScore =
+ BinaryDictionary.calcNormalizedScore(
+ mOriginalText, bestSuggestion.toString(), bestScore);
+ hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
+ if (DBG) {
+ Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore);
+ Log.i(TAG, "Normalized score = " + normalizedScore
+ + " (threshold " + mRecommendedThreshold
+ + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions);
+ }
+ }
+ return new Result(gatheredSuggestions, hasRecommendedSuggestions);
+ }
+ }
+
+ @Override
+ public boolean onUnbind(final Intent intent) {
+ closeAllDictionaries();
+ return false;
+ }
+
+ private void closeAllDictionaries() {
+ final Map<String, DictionaryPool> oldPools = mDictionaryPools;
+ mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
+ final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries;
+ mUserDictionaries =
+ Collections.synchronizedMap(new TreeMap<String, UserBinaryDictionary>());
+ final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
+ mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+ new Thread("spellchecker_close_dicts") {
+ @Override
+ public void run() {
+ for (DictionaryPool pool : oldPools.values()) {
+ pool.close();
+ }
+ for (Dictionary dict : oldUserDictionaries.values()) {
+ dict.close();
+ }
+ for (Dictionary dict : oldWhitelistDictionaries.values()) {
+ dict.close();
+ }
+ synchronized (mUseContactsLock) {
+ if (null != mContactsDictionary) {
+ // The synchronously loaded contacts dictionary should have been in one
+ // or several pools, but it is shielded against multiple closing and it's
+ // safe to call it several times.
+ final ContactsBinaryDictionary dictToClose = mContactsDictionary;
+ // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY
+ // is no longer needed
+ mContactsDictionary = null;
+ dictToClose.close();
+ }
+ }
+ }
+ }.start();
+ }
+
+ private DictionaryPool getDictionaryPool(final String locale) {
+ DictionaryPool pool = mDictionaryPools.get(locale);
+ if (null == pool) {
+ final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
+ pool = new DictionaryPool(POOL_SIZE, this, localeObject);
+ mDictionaryPools.put(locale, pool);
+ }
+ return pool;
+ }
+
+ public DictAndProximity createDictAndProximity(final Locale locale) {
+ final int script = getScriptFromLocale(locale);
+ final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(
+ SpellCheckerProximityInfo.getProximityForScript(script),
+ SpellCheckerProximityInfo.ROW_SIZE,
+ SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH,
+ SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT);
+ final DictionaryCollection dictionaryCollection =
+ DictionaryFactory.createMainDictionaryFromManager(this, locale,
+ true /* useFullEditDistance */);
+ final String localeStr = locale.toString();
+ UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
+ if (null == userDictionary) {
+ userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
+ mUserDictionaries.put(localeStr, userDictionary);
+ }
+ dictionaryCollection.addDictionary(userDictionary);
+ Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
+ if (null == whitelistDictionary) {
+ whitelistDictionary = new WhitelistDictionary(this, locale);
+ mWhitelistDictionaries.put(localeStr, whitelistDictionary);
+ }
+ dictionaryCollection.addDictionary(whitelistDictionary);
+ synchronized (mUseContactsLock) {
+ if (mUseContactsDictionary) {
+ if (null == mContactsDictionary) {
+ // TODO: use the right locale. We can't do it right now because the
+ // spell checker is reusing the contacts dictionary across sessions
+ // without regard for their locale, so we need to fix that first.
+ mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
+ Locale.getDefault());
+ }
+ }
+ dictionaryCollection.addDictionary(mContactsDictionary);
+ mDictionaryCollectionsList.add(
+ new WeakReference<DictionaryCollection>(dictionaryCollection));
+ }
+ return new DictAndProximity(dictionaryCollection, proximityInfo);
+ }
+
+ // This method assumes the text is not empty or null.
+ private static int getCapitalizationType(String text) {
+ // If the first char is not uppercase, then the word is either all lower case,
+ // and in either case we return CAPITALIZE_NONE.
+ if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
+ final int len = text.length();
+ int capsCount = 1;
+ for (int i = 1; i < len; i = text.offsetByCodePoints(i, 1)) {
+ if (1 != capsCount && i != capsCount) break;
+ if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
+ }
+ // We know the first char is upper case. So we want to test if either everything
+ // else is lower case, or if everything else is upper case. If the string is
+ // exactly one char long, then we will arrive here with capsCount 1, and this is
+ // correct, too.
+ if (1 == capsCount) return CAPITALIZE_FIRST;
+ return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
+ }
+
+ private static class AndroidSpellCheckerSession extends Session {
+ // Immutable, but need the locale which is not available in the constructor yet
+ private DictionaryPool mDictionaryPool;
+ // Likewise
+ private Locale mLocale;
+ // Cache this for performance
+ private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
+
+ private final AndroidSpellCheckerService mService;
+
+ private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
+
+ private static class SuggestionsParams {
+ public final String[] mSuggestions;
+ public final int mFlags;
+ public SuggestionsParams(String[] suggestions, int flags) {
+ mSuggestions = suggestions;
+ mFlags = flags;
+ }
+ }
+
+ private static class SuggestionsCache {
+ private static final char CHAR_DELIMITER = '\uFFFC';
+ private static final int MAX_CACHE_SIZE = 50;
+ private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
+ new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
+
+ // TODO: Support n-gram input
+ private static String generateKey(String query, String prevWord) {
+ if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) {
+ return query;
+ }
+ return query + CHAR_DELIMITER + prevWord;
+ }
+
+ // TODO: Support n-gram input
+ public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) {
+ return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord));
+ }
+
+ // TODO: Support n-gram input
+ public void putSuggestionsToCache(
+ String query, String prevWord, String[] suggestions, int flags) {
+ if (suggestions == null || TextUtils.isEmpty(query)) {
+ return;
+ }
+ mUnigramSuggestionsInfoCache.put(
+ generateKey(query, prevWord), new SuggestionsParams(suggestions, flags));
+ }
+ }
+
+ AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
+ mService = service;
+ }
+
+ @Override
+ public void onCreate() {
+ final String localeString = getLocale();
+ mDictionaryPool = mService.getDictionaryPool(localeString);
+ mLocale = LocaleUtils.constructLocaleFromString(localeString);
+ mScript = getScriptFromLocale(mLocale);
+ }
+
+ /*
+ * Returns whether the code point is a letter that makes sense for the specified
+ * locale for this spell checker.
+ * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
+ * and is limited to EFIGS languages and Russian.
+ * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
+ * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
+ */
+ private static boolean isLetterCheckableByLanguage(final int codePoint,
+ final int script) {
+ switch (script) {
+ case SCRIPT_LATIN:
+ // Our supported latin script dictionaries (EFIGS) at the moment only include
+ // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
+ // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
+ // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
+ // excluded from isLetter anyway.
+ return codePoint <= 0x2AF && Character.isLetter(codePoint);
+ case SCRIPT_CYRILLIC:
+ // All Cyrillic characters are in the 400~52F block. There are some in the upper
+ // Unicode range, but they are archaic characters that are not used in modern
+ // russian and are not used by our dictionary.
+ return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
+ default:
+ // Should never come here
+ throw new RuntimeException("Impossible value of script: " + script);
+ }
+ }
+
+ /**
+ * Finds out whether a particular string should be filtered out of spell checking.
+ *
+ * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
+ * we know we will never recognize, this accepts a script identifier that should be one
+ * of the SCRIPT_* constants defined above, to rule out quickly characters from very
+ * different languages.
+ *
+ * @param text the string to evaluate.
+ * @param script the identifier for the script this spell checker recognizes
+ * @return true if we should filter this text out, false otherwise
+ */
+ private static boolean shouldFilterOut(final String text, final int script) {
+ if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
+
+ // TODO: check if an equivalent processing can't be done more quickly with a
+ // compiled regexp.
+ // Filter by first letter
+ final int firstCodePoint = text.codePointAt(0);
+ // Filter out words that don't start with a letter or an apostrophe
+ if (!isLetterCheckableByLanguage(firstCodePoint, script)
+ && '\'' != firstCodePoint) return true;
+
+ // Filter contents
+ final int length = text.length();
+ int letterCount = 0;
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // Any word containing a '@' is probably an e-mail address
+ // Any word containing a '/' is probably either an ad-hoc combination of two
+ // words or a URI - in either case we don't want to spell check that
+ if ('@' == codePoint || '/' == codePoint) return true;
+ if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
+ }
+ // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
+ // in this word are letters
+ return (letterCount * 4 < length * 3);
+ }
+
+ private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
+ TextInfo ti, SentenceSuggestionsInfo ssi) {
+ final String typedText = ti.getText();
+ if (!typedText.contains(SINGLE_QUOTE)) {
+ return null;
+ }
+ final int N = ssi.getSuggestionsCount();
+ final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
+ final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
+ final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
+ new ArrayList<SuggestionsInfo>();
+ String currentWord = null;
+ for (int i = 0; i < N; ++i) {
+ final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
+ final int flags = si.getSuggestionsAttributes();
+ if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
+ continue;
+ }
+ final int offset = ssi.getOffsetAt(i);
+ final int length = ssi.getLengthAt(i);
+ final String subText = typedText.substring(offset, offset + length);
+ final String prevWord = currentWord;
+ currentWord = subText;
+ if (!subText.contains(SINGLE_QUOTE)) {
+ continue;
+ }
+ final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
+ if (splitTexts == null || splitTexts.length <= 1) {
+ continue;
+ }
+ final int splitNum = splitTexts.length;
+ for (int j = 0; j < splitNum; ++j) {
+ final String splitText = splitTexts[j];
+ if (TextUtils.isEmpty(splitText)) {
+ continue;
+ }
+ if (mSuggestionsCache.getSuggestionsFromCache(
+ splitText, prevWord) == null) {
+ continue;
+ }
+ final int newLength = splitText.length();
+ // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
+ final int newFlags = 0;
+ final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
+ newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
+ if (DBG) {
+ Log.d(TAG, "Override and remove old span over: "
+ + splitText + ", " + offset + "," + newLength);
+ }
+ additionalOffsets.add(offset);
+ additionalLengths.add(newLength);
+ additionalSuggestionsInfos.add(newSi);
+ }
+ }
+ final int additionalSize = additionalOffsets.size();
+ if (additionalSize <= 0) {
+ return null;
+ }
+ final int suggestionsSize = N + additionalSize;
+ final int[] newOffsets = new int[suggestionsSize];
+ final int[] newLengths = new int[suggestionsSize];
+ final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
+ int i;
+ for (i = 0; i < N; ++i) {
+ newOffsets[i] = ssi.getOffsetAt(i);
+ newLengths[i] = ssi.getLengthAt(i);
+ newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
+ }
+ for (; i < suggestionsSize; ++i) {
+ newOffsets[i] = additionalOffsets.get(i - N);
+ newLengths[i] = additionalLengths.get(i - N);
+ newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
+ }
+ return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
+ }
+
+ @Override
+ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
+ TextInfo[] textInfos, int suggestionsLimit) {
+ final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
+ textInfos, suggestionsLimit);
+ if (retval == null || retval.length != textInfos.length) {
+ return retval;
+ }
+ for (int i = 0; i < retval.length; ++i) {
+ final SentenceSuggestionsInfo tempSsi =
+ fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
+ if (tempSsi != null) {
+ retval[i] = tempSsi;
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit, boolean sequentialWords) {
+ final int length = textInfos.length;
+ final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+ for (int i = 0; i < length; ++i) {
+ final String prevWord;
+ if (sequentialWords && i > 0) {
+ final String prevWordCandidate = textInfos[i - 1].getText();
+ // Note that an empty string would be used to indicate the initial word
+ // in the future.
+ prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
+ } else {
+ prevWord = null;
+ }
+ retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
+ retval[i].setCookieAndSequence(
+ textInfos[i].getCookie(), textInfos[i].getSequence());
+ }
+ return retval;
+ }
+
+ // Note : this must be reentrant
+ /**
+ * Gets a list of suggestions for a specific string. This returns a list of possible
+ * corrections for the text passed as an argument. It may split or group words, and
+ * even perform grammatical analysis.
+ */
+ @Override
+ public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
+ final int suggestionsLimit) {
+ return onGetSuggestions(textInfo, null, suggestionsLimit);
+ }
+
+ private SuggestionsInfo onGetSuggestions(
+ final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
+ try {
+ final String inText = textInfo.getText();
+ final SuggestionsParams cachedSuggestionsParams =
+ mSuggestionsCache.getSuggestionsFromCache(inText, prevWord);
+ if (cachedSuggestionsParams != null) {
+ if (DBG) {
+ Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
+ }
+ return new SuggestionsInfo(
+ cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
+ }
+
+ if (shouldFilterOut(inText, mScript)) {
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.takeOrGetNull();
+ if (null == dictInfo) return getNotInDictEmptySuggestions();
+ return dictInfo.mDictionary.isValidWord(inText) ?
+ getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+ }
+ final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
+
+ // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
+ final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
+ mService.mSuggestionThreshold, mService.mRecommendedThreshold,
+ suggestionsLimit);
+ final WordComposer composer = new WordComposer();
+ final int length = text.length();
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // The getXYForCodePointAndScript method returns (Y << 16) + X
+ final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
+ codePoint, mScript);
+ if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
+ composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
+ WordComposer.NOT_A_COORDINATE, null);
+ } else {
+ composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
+ }
+ }
+
+ final int capitalizeType = getCapitalizationType(text);
+ boolean isInDict = true;
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.takeOrGetNull();
+ if (null == dictInfo) return getNotInDictEmptySuggestions();
+ dictInfo.mDictionary.getWords(composer, prevWord, suggestionsGatherer,
+ dictInfo.mProximityInfo);
+ isInDict = dictInfo.mDictionary.isValidWord(text);
+ if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
+ // We want to test the word again if it's all caps or first caps only.
+ // If it's fully down, we already tested it, if it's mixed case, we don't
+ // want to test a lowercase version of it.
+ isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
+ }
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+
+ final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
+ capitalizeType, mLocale);
+
+ if (DBG) {
+ Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
+ + suggestionsLimit);
+ Log.i(TAG, "IsInDict = " + isInDict);
+ Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
+ Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
+ if (null != result.mSuggestions) {
+ for (String suggestion : result.mSuggestions) {
+ Log.i(TAG, suggestion);
+ }
+ }
+ }
+
+ final int flags =
+ (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
+ : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
+ | (result.mHasRecommendedSuggestions
+ ? SuggestionsInfoCompatUtils
+ .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
+ : 0);
+ final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
+ mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags);
+ return retval;
+ } catch (RuntimeException e) {
+ // Don't kill the keyboard if there is a bug in the spell checker
+ if (DBG) {
+ throw e;
+ } else {
+ Log.e(TAG, "Exception while spellcheking: " + e);
+ return getNotInDictEmptySuggestions();
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
new file mode 100644
index 000000000..3dbbd40cd
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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.spellcheck;
+
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+/**
+ * A simple container for both a Dictionary and a ProximityInfo.
+ */
+public class DictAndProximity {
+ public final Dictionary mDictionary;
+ public final ProximityInfo mProximityInfo;
+ public DictAndProximity(final Dictionary dictionary, final ProximityInfo proximityInfo) {
+ mDictionary = dictionary;
+ mProximityInfo = proximityInfo;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
new file mode 100644
index 000000000..8fc632ee7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.spellcheck;
+
+import java.util.Locale;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * A blocking queue that creates dictionaries up to a certain limit as necessary.
+ */
+@SuppressWarnings("serial")
+public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+ private final AndroidSpellCheckerService mService;
+ private final int mMaxSize;
+ private final Locale mLocale;
+ private int mSize;
+ private volatile boolean mClosed;
+
+ public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service,
+ final Locale locale) {
+ super();
+ mMaxSize = maxSize;
+ mService = service;
+ mLocale = locale;
+ mSize = 0;
+ mClosed = false;
+ }
+
+ @Override
+ public DictAndProximity take() throws InterruptedException {
+ final DictAndProximity dict = poll();
+ if (null != dict) return dict;
+ synchronized(this) {
+ if (mSize >= mMaxSize) {
+ // Our pool is already full. Wait until some dictionary is ready.
+ return super.take();
+ } else {
+ ++mSize;
+ return mService.createDictAndProximity(mLocale);
+ }
+ }
+ }
+
+ // Convenience method
+ public DictAndProximity takeOrGetNull() {
+ try {
+ return take();
+ } catch (InterruptedException e) {
+ return null;
+ }
+ }
+
+ public void close() {
+ synchronized(this) {
+ mClosed = true;
+ for (DictAndProximity dict : this) {
+ dict.mDictionary.close();
+ }
+ clear();
+ }
+ }
+
+ @Override
+ public boolean offer(final DictAndProximity dict) {
+ if (mClosed) {
+ dict.mDictionary.close();
+ return false;
+ } else {
+ return super.offer(dict);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java
deleted file mode 100644
index 63c6d69d7..000000000
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2011 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.spellcheck;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import com.android.inputmethod.compat.ArraysCompatUtils;
-import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.Dictionary.DataType;
-import com.android.inputmethod.latin.Dictionary.WordCallback;
-import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.Utils;
-import com.android.inputmethod.latin.WordComposer;
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Implements spell checking methods.
- */
-public class SpellChecker {
-
- public final Dictionary mDictionary;
-
- public SpellChecker(final Context context, final Locale locale) {
- final Resources resources = context.getResources();
- final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources);
- mDictionary = DictionaryFactory.createDictionaryFromManager(context, locale,
- fallbackResourceId);
- }
-
- // Note : this must be reentrant
- /**
- * Finds out whether a word is in the dictionary or not.
- *
- * @param text the sequence containing the word to check for.
- * @param start the index of the first character of the word in text.
- * @param end the index of the next-to-last character in text.
- * @return true if the word is in the dictionary, false otherwise.
- */
- public boolean isCorrect(final CharSequence text, final int start, final int end) {
- return mDictionary.isValidWord(text.subSequence(start, end));
- }
-
- private static class SuggestionsGatherer implements WordCallback {
- private final int DEFAULT_SUGGESTION_LENGTH = 16;
- private final List<String> mSuggestions = new LinkedList<String>();
- private int[] mScores = new int[DEFAULT_SUGGESTION_LENGTH];
- private int mLength = 0;
-
- @Override
- synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
- int dicTypeId, DataType dataType) {
- if (mLength >= mScores.length) {
- final int newLength = mScores.length * 2;
- mScores = new int[newLength];
- }
- final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score);
- // binarySearch returns the index if the element exists, and -<insertion index> - 1
- // if it doesn't. See documentation for binarySearch.
- final int insertionIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
- System.arraycopy(mScores, insertionIndex, mScores, insertionIndex + 1,
- mLength - insertionIndex);
- mLength += 1;
- mScores[insertionIndex] = score;
- mSuggestions.add(insertionIndex, new String(word, wordOffset, wordLength));
- return true;
- }
-
- public List<String> getGatheredSuggestions() {
- return mSuggestions;
- }
- }
-
- // Note : this must be reentrant
- /**
- * Gets a list of suggestions for a specific string.
- *
- * This returns a list of possible corrections for the text passed as an
- * arguments. It may split or group words, and even perform grammatical
- * analysis.
- *
- * @param text the sequence containing the word to check for.
- * @param start the index of the first character of the word in text.
- * @param end the index of the next-to-last character in text.
- * @return a list of possible suggestions to replace the text.
- */
- public List<String> getSuggestions(final CharSequence text, final int start, final int end) {
- final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer();
- final WordComposer composer = new WordComposer();
- for (int i = start; i < end; ++i) {
- int character = text.charAt(i);
- composer.add(character, new int[] { character },
- WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
- }
- mDictionary.getWords(composer, suggestionsGatherer);
- return suggestionsGatherer.getGatheredSuggestions();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
new file mode 100644
index 000000000..0103e8423
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2011 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.spellcheck;
+
+import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+import java.util.TreeMap;
+
+public class SpellCheckerProximityInfo {
+ /* public for test */
+ final public static int NUL = KeyDetector.NOT_A_CODE;
+
+ // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
+ // native code - this value is passed at creation of the binary object and reused
+ // as the size of the passed array afterwards so they can't be different.
+ final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
+
+ // The number of keys in a row of the grid used by the spell checker.
+ final public static int PROXIMITY_GRID_WIDTH = 11;
+ // The number of rows in the grid used by the spell checker.
+ final public static int PROXIMITY_GRID_HEIGHT = 3;
+
+ final private static int NOT_AN_INDEX = -1;
+ final public static int NOT_A_COORDINATE_PAIR = -1;
+
+ // Helper methods
+ final protected static void buildProximityIndices(final int[] proximity,
+ final TreeMap<Integer, Integer> indices) {
+ for (int i = 0; i < proximity.length; i += ROW_SIZE) {
+ if (NUL != proximity[i]) indices.put(proximity[i], i / ROW_SIZE);
+ }
+ }
+ final protected static int computeIndex(final int characterCode,
+ final TreeMap<Integer, Integer> indices) {
+ final Integer result = indices.get(characterCode);
+ if (null == result) return NOT_AN_INDEX;
+ return result;
+ }
+
+ private static class Latin {
+ // This is a map from the code point to the index in the PROXIMITY array.
+ // At the time the native code to read the binary dictionary needs the proximity info be
+ // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input
+ // character.
+ // Since we need to build such an array, we want to be able to search in our big proximity
+ // data quickly by character, and a map is probably the best way to do this.
+ final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+
+ // The proximity here is the union of
+ // - the proximity for a QWERTY keyboard.
+ // - the proximity for an AZERTY keyboard.
+ // - the proximity for a QWERTZ keyboard.
+ // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other.
+ //
+ // The reasoning behind this construction is, almost any alphabetic text we may want
+ // to spell check has been entered with one of the keyboards above. Also, specifically
+ // to English, many spelling errors consist of the last vowel of the word being wrong
+ // because in English vowels tend to merge with each other in pronunciation.
+ final static int[] PROXIMITY = {
+ // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
+ // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
+ // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
+ 'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
+ 'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL,
+ 'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+
+ // Proximity for row 2. See comment above about size.
+ 'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
+ 's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+
+ // Proximity for row 3. See comment above about size.
+ 'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL,
+ 'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ };
+ static {
+ buildProximityIndices(PROXIMITY, INDICES);
+ }
+ static int getIndexOf(int characterCode) {
+ return computeIndex(characterCode, INDICES);
+ }
+ }
+
+ private static class Cyrillic {
+ final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+ // TODO: The following table is solely based on the keyboard layout. Consult with Russian
+ // speakers on commonly misspelled words/letters.
+ final static int[] PROXIMITY = {
+ // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
+ // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
+ // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
+ 'й', 'ц', 'ф', 'ы', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ц', 'й', 'ф', 'ы', 'в', 'у', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'у', 'ц', 'ы', 'в', 'а', 'к', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'к', 'у', 'в', 'а', 'п', 'е', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'е', 'к', 'а', 'п', 'р', 'н', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'н', 'е', 'п', 'р', 'о', 'г', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'г', 'н', 'р', 'о', 'л', 'ш', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ш', 'г', 'о', 'л', 'д', 'щ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'щ', 'ш', 'л', 'д', 'ж', 'з', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'з', 'щ', 'д', 'ж', 'э', 'х', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'х', 'з', 'ж', 'э', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+
+ // Proximity for row 2. See comment above about size.
+ 'ф', 'й', 'ц', 'ы', 'я', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ы', 'й', 'ц', 'у', 'ф', 'в', 'я', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'в', 'ц', 'у', 'к', 'ы', 'а', 'я', 'ч', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'а', 'у', 'к', 'е', 'в', 'п', 'ч', 'с', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'п', 'к', 'е', 'н', 'а', 'р', 'с', 'м', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'р', 'е', 'н', 'г', 'п', 'о', 'м', 'и', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'о', 'н', 'г', 'ш', 'р', 'л', 'и', 'т', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'л', 'г', 'ш', 'щ', 'о', 'д', 'т', 'ь', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'д', 'ш', 'щ', 'з', 'л', 'ж', 'ь', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ж', 'щ', 'з', 'х', 'д', 'э', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'э', 'з', 'х', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+
+ // Proximity for row 3. See comment above about size.
+ 'я', 'ф', 'ы', 'в', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ч', 'ы', 'в', 'а', 'я', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'с', 'в', 'а', 'п', 'ч', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'м', 'а', 'п', 'р', 'с', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'и', 'п', 'р', 'о', 'м', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'т', 'р', 'о', 'л', 'и', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ь', 'о', 'л', 'д', 'т', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'б', 'л', 'д', 'ж', 'ь', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ 'ю', 'д', 'ж', 'э', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ };
+ static {
+ buildProximityIndices(PROXIMITY, INDICES);
+ }
+ static int getIndexOf(int characterCode) {
+ return computeIndex(characterCode, INDICES);
+ }
+ }
+
+ public static int[] getProximityForScript(final int script) {
+ switch (script) {
+ case AndroidSpellCheckerService.SCRIPT_LATIN:
+ return Latin.PROXIMITY;
+ case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+ return Cyrillic.PROXIMITY;
+ default:
+ throw new RuntimeException("Wrong script supplied: " + script);
+ }
+ }
+
+ private static int getIndexOfCodeForScript(final int codePoint, final int script) {
+ switch (script) {
+ case AndroidSpellCheckerService.SCRIPT_LATIN:
+ return Latin.getIndexOf(codePoint);
+ case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+ return Cyrillic.getIndexOf(codePoint);
+ default:
+ throw new RuntimeException("Wrong script supplied: " + script);
+ }
+ }
+
+ // Returns (Y << 16) + X to avoid creating a temporary object. This is okay because
+ // X and Y are limited to PROXIMITY_GRID_WIDTH resp. PROXIMITY_GRID_HEIGHT which is very
+ // inferior to 1 << 16
+ // As an exception, this returns NOT_A_COORDINATE_PAIR if the key is not on the grid
+ public static int getXYForCodePointAndScript(final int codePoint, final int script) {
+ final int index = getIndexOfCodeForScript(codePoint, script);
+ if (NOT_AN_INDEX == index) return NOT_A_COORDINATE_PAIR;
+ final int y = index / PROXIMITY_GRID_WIDTH;
+ final int x = index % PROXIMITY_GRID_WIDTH;
+ if (y > PROXIMITY_GRID_HEIGHT) {
+ // Safety check, should be entirely useless
+ throw new RuntimeException("Wrong y coordinate in spell checker proximity");
+ }
+ return (y << 16) + x;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
new file mode 100644
index 000000000..e14db8797
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (C) 2011 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.spellcheck;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+/**
+ * Spell checker preference screen.
+ */
+public class SpellCheckerSettingsActivity extends PreferenceActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public Intent getIntent() {
+ final Intent modIntent = new Intent(super.getIntent());
+ modIntent.putExtra(EXTRA_SHOW_FRAGMENT, SpellCheckerSettingsFragment.class.getName());
+ modIntent.putExtra(EXTRA_NO_HEADERS, true);
+ return modIntent;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
new file mode 100644
index 000000000..7056874a1
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright (C) 2011 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.spellcheck;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+import com.android.inputmethod.latin.R;
+
+/**
+ * Preference screen.
+ */
+public class SpellCheckerSettingsFragment extends PreferenceFragment {
+ /**
+ * Empty constructor for fragment generation.
+ */
+ public SpellCheckerSettingsFragment() {
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ addPreferencesFromResource(R.xml.spell_checker_settings);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
new file mode 100644
index 000000000..c6fe43b69
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2011 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.suggestions;
+
+import android.content.res.Resources;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.Utils;
+
+public class MoreSuggestions extends Keyboard {
+ public static final int SUGGESTION_CODE_BASE = 1024;
+
+ MoreSuggestions(Builder.MoreSuggestionsParam params) {
+ super(params);
+ }
+
+ public static class Builder extends Keyboard.Builder<Builder.MoreSuggestionsParam> {
+ private final MoreSuggestionsView mPaneView;
+ private SuggestedWords mSuggestions;
+ private int mFromPos;
+ private int mToPos;
+
+ public static class MoreSuggestionsParam extends Keyboard.Params {
+ private final int[] mWidths = new int[SuggestionsView.MAX_SUGGESTIONS];
+ private final int[] mRowNumbers = new int[SuggestionsView.MAX_SUGGESTIONS];
+ private final int[] mColumnOrders = new int[SuggestionsView.MAX_SUGGESTIONS];
+ private final int[] mNumColumnsInRow = new int[SuggestionsView.MAX_SUGGESTIONS];
+ private static final int MAX_COLUMNS_IN_ROW = 3;
+ private int mNumRows;
+ public Drawable mDivider;
+ public int mDividerWidth;
+
+ public int layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth,
+ int maxRow, MoreSuggestionsView view) {
+ clearKeys();
+ final Resources res = view.getContext().getResources();
+ mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
+ mDividerWidth = mDivider.getIntrinsicWidth();
+ final int padding = (int) res.getDimension(
+ R.dimen.more_suggestions_key_horizontal_padding);
+ final Paint paint = view.newDefaultLabelPaint();
+
+ int row = 0;
+ int pos = fromPos, rowStartPos = fromPos;
+ final int size = Math.min(suggestions.size(), SuggestionsView.MAX_SUGGESTIONS);
+ while (pos < size) {
+ final String word = suggestions.getWord(pos).toString();
+ // TODO: Should take care of text x-scaling.
+ mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
+ final int numColumn = pos - rowStartPos + 1;
+ final int columnWidth =
+ (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
+ if (numColumn > MAX_COLUMNS_IN_ROW
+ || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
+ if ((row + 1) >= maxRow) {
+ break;
+ }
+ mNumColumnsInRow[row] = pos - rowStartPos;
+ rowStartPos = pos;
+ row++;
+ }
+ mColumnOrders[pos] = pos - rowStartPos;
+ mRowNumbers[pos] = row;
+ pos++;
+ }
+ mNumColumnsInRow[row] = pos - rowStartPos;
+ mNumRows = row + 1;
+ mBaseWidth = mOccupiedWidth = Math.max(
+ minWidth, calcurateMaxRowWidth(fromPos, pos));
+ mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
+ return pos - fromPos;
+ }
+
+ private boolean fitInWidth(int startPos, int endPos, int width) {
+ for (int pos = startPos; pos < endPos; pos++) {
+ if (mWidths[pos] > width)
+ return false;
+ }
+ return true;
+ }
+
+ private int calcurateMaxRowWidth(int startPos, int endPos) {
+ int maxRowWidth = 0;
+ int pos = startPos;
+ for (int row = 0; row < mNumRows; row++) {
+ final int numColumnInRow = mNumColumnsInRow[row];
+ int maxKeyWidth = 0;
+ while (pos < endPos && mRowNumbers[pos] == row) {
+ maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
+ pos++;
+ }
+ maxRowWidth = Math.max(maxRowWidth,
+ maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
+ }
+ return maxRowWidth;
+ }
+
+ private static final int[][] COLUMN_ORDER_TO_NUMBER = {
+ { 0, },
+ { 1, 0, },
+ { 2, 0, 1},
+ };
+
+ public int getNumColumnInRow(int pos) {
+ return mNumColumnsInRow[mRowNumbers[pos]];
+ }
+
+ public int getColumnNumber(int pos) {
+ final int columnOrder = mColumnOrders[pos];
+ final int numColumn = getNumColumnInRow(pos);
+ return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
+ }
+
+ public int getX(int pos) {
+ final int columnNumber = getColumnNumber(pos);
+ return columnNumber * (getWidth(pos) + mDividerWidth);
+ }
+
+ public int getY(int pos) {
+ final int row = mRowNumbers[pos];
+ return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
+ }
+
+ public int getWidth(int pos) {
+ final int numColumnInRow = getNumColumnInRow(pos);
+ return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
+ }
+
+ public void markAsEdgeKey(Key key, int pos) {
+ final int row = mRowNumbers[pos];
+ if (row == 0)
+ key.markAsBottomEdge(this);
+ if (row == mNumRows - 1)
+ key.markAsTopEdge(this);
+
+ final int numColumnInRow = mNumColumnsInRow[row];
+ final int column = getColumnNumber(pos);
+ if (column == 0)
+ key.markAsLeftEdge(this);
+ if (column == numColumnInRow - 1)
+ key.markAsRightEdge(this);
+ }
+ }
+
+ public Builder(MoreSuggestionsView paneView) {
+ super(paneView.getContext(), new MoreSuggestionsParam());
+ mPaneView = paneView;
+ }
+
+ public Builder layout(SuggestedWords suggestions, int fromPos, int maxWidth,
+ int minWidth, int maxRow) {
+ final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
+ final int xmlId = R.xml.kbd_suggestions_pane_template;
+ load(xmlId, keyboard.mId);
+ mParams.mVerticalGap = mParams.mTopPadding = keyboard.mVerticalGap / 2;
+
+ final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow,
+ mPaneView);
+ mFromPos = fromPos;
+ mToPos = fromPos + count;
+ mSuggestions = suggestions;
+ return this;
+ }
+
+ private static class Divider extends Key.Spacer {
+ private final Drawable mIcon;
+
+ public Divider(Keyboard.Params params, Drawable icon, int x, int y, int width,
+ int height) {
+ super(params, x, y, width, height);
+ mIcon = icon;
+ }
+
+ @Override
+ public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
+ // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+ // constructor.
+ // TODO: Drawable itself should have an alpha value.
+ mIcon.setAlpha(128);
+ return mIcon;
+ }
+ }
+
+ @Override
+ public MoreSuggestions build() {
+ final MoreSuggestionsParam params = mParams;
+ for (int pos = mFromPos; pos < mToPos; pos++) {
+ final int x = params.getX(pos);
+ final int y = params.getY(pos);
+ final int width = params.getWidth(pos);
+ final String word = mSuggestions.getWord(pos).toString();
+ final String info = Utils.getDebugInfo(mSuggestions, pos);
+ final int index = pos + SUGGESTION_CODE_BASE;
+ final Key key = new Key(
+ params, word, info, KeyboardIconsSet.ICON_UNDEFINED, index, null, x, y,
+ width, params.mDefaultRowHeight, 0);
+ params.markAsEdgeKey(key, pos);
+ params.onAddKey(key);
+ final int columnNumber = params.getColumnNumber(pos);
+ final int numColumnInRow = params.getNumColumnInRow(pos);
+ if (columnNumber < numColumnInRow - 1) {
+ final Divider divider = new Divider(params, params.mDivider, x + width, y,
+ params.mDividerWidth, params.mDefaultRowHeight);
+ params.onAddKey(divider);
+ }
+ }
+ return new MoreSuggestions(params);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
new file mode 100644
index 000000000..19287e3f3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 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.suggestions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.PopupWindow;
+
+import com.android.inputmethod.keyboard.KeyDetector;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.MoreKeysDetector;
+import com.android.inputmethod.keyboard.MoreKeysPanel;
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
+import com.android.inputmethod.keyboard.PointerTracker.KeyEventHandler;
+import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.R;
+
+/**
+ * A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting
+ * key presses and touch movements.
+ */
+public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
+ private final int[] mCoordinates = new int[2];
+
+ final KeyDetector mModalPanelKeyDetector;
+ private final KeyDetector mSlidingPanelKeyDetector;
+
+ private Controller mController;
+ KeyboardActionListener mListener;
+ private int mOriginX;
+ private int mOriginY;
+
+ static final TimerProxy EMPTY_TIMER_PROXY = new TimerProxy.Adapter();
+
+ final KeyboardActionListener mSuggestionsPaneListener =
+ new KeyboardActionListener.Adapter() {
+ @Override
+ public void onPressKey(int primaryCode) {
+ mListener.onPressKey(primaryCode);
+ }
+
+ @Override
+ public void onReleaseKey(int primaryCode, boolean withSliding) {
+ mListener.onReleaseKey(primaryCode, withSliding);
+ }
+
+ @Override
+ public void onCodeInput(int primaryCode, int x, int y) {
+ final int index = primaryCode - MoreSuggestions.SUGGESTION_CODE_BASE;
+ if (index >= 0 && index < SuggestionsView.MAX_SUGGESTIONS) {
+ mListener.onCustomRequest(index);
+ }
+ }
+
+ @Override
+ public void onCancelInput() {
+ mListener.onCancelInput();
+ }
+ };
+
+ public MoreSuggestionsView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.moreSuggestionsViewStyle);
+ }
+
+ public MoreSuggestionsView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final Resources res = context.getResources();
+ mModalPanelKeyDetector = new KeyDetector(/* keyHysteresisDistance */ 0);
+ mSlidingPanelKeyDetector = new MoreKeysDetector(
+ res.getDimension(R.dimen.more_suggestions_slide_allowance));
+ setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard != null) {
+ final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
+ final int height = keyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ @Override
+ public void setKeyboard(Keyboard keyboard) {
+ super.setKeyboard(keyboard);
+ mModalPanelKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), -getPaddingTop());
+ mSlidingPanelKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
+ -getPaddingTop() + mVerticalCorrection);
+ }
+
+ @Override
+ public KeyDetector getKeyDetector() {
+ return mSlidingPanelKeyDetector;
+ }
+
+ @Override
+ public KeyboardActionListener getKeyboardActionListener() {
+ return mSuggestionsPaneListener;
+ }
+
+ @Override
+ public DrawingProxy getDrawingProxy() {
+ return this;
+ }
+
+ @Override
+ public TimerProxy getTimerProxy() {
+ return EMPTY_TIMER_PROXY;
+ }
+
+ @Override
+ public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ // Suggestions pane needs no pop-up key preview displayed, so we pass always false with a
+ // delay of 0. The delay does not matter actually since the popup is not shown anyway.
+ super.setKeyPreviewPopupEnabled(false, 0);
+ }
+
+ @Override
+ public void showMoreKeysPanel(View parentView, Controller controller, int pointX, int pointY,
+ PopupWindow window, KeyboardActionListener listener) {
+ mController = controller;
+ mListener = listener;
+ final View container = (View)getParent();
+ final MoreSuggestions pane = (MoreSuggestions)getKeyboard();
+ final int defaultCoordX = pane.mOccupiedWidth / 2;
+ // The coordinates of panel's left-top corner in parentView's coordinate system.
+ final int x = pointX - defaultCoordX - container.getPaddingLeft();
+ final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom();
+
+ window.setContentView(container);
+ window.setWidth(container.getMeasuredWidth());
+ window.setHeight(container.getMeasuredHeight());
+ parentView.getLocationInWindow(mCoordinates);
+ window.showAtLocation(parentView, Gravity.NO_GRAVITY,
+ x + mCoordinates[0], y + mCoordinates[1]);
+
+ mOriginX = x + container.getPaddingLeft();
+ mOriginY = y + container.getPaddingTop();
+ }
+
+ private boolean mIsDismissing;
+
+ @Override
+ public boolean dismissMoreKeysPanel() {
+ if (mIsDismissing || mController == null) return false;
+ mIsDismissing = true;
+ final boolean dismissed = mController.dismissMoreKeysPanel();
+ mIsDismissing = false;
+ return dismissed;
+ }
+
+ @Override
+ public int translateX(int x) {
+ return x - mOriginX;
+ }
+
+ @Override
+ public int translateY(int y) {
+ return y - mOriginY;
+ }
+
+ private final KeyEventHandler mModalPanelKeyEventHandler = new KeyEventHandler() {
+ @Override
+ public KeyDetector getKeyDetector() {
+ return mModalPanelKeyDetector;
+ }
+
+ @Override
+ public KeyboardActionListener getKeyboardActionListener() {
+ return mSuggestionsPaneListener;
+ }
+
+ @Override
+ public DrawingProxy getDrawingProxy() {
+ return MoreSuggestionsView.this;
+ }
+
+ @Override
+ public TimerProxy getTimerProxy() {
+ return EMPTY_TIMER_PROXY;
+ }
+ };
+
+ @Override
+ public boolean onTouchEvent(MotionEvent me) {
+ final int action = me.getAction();
+ final long eventTime = me.getEventTime();
+ final int index = me.getActionIndex();
+ final int id = me.getPointerId(index);
+ final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ tracker.processMotionEvent(action, x, y, eventTime, mModalPanelKeyEventHandler);
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
new file mode 100644
index 000000000..e86390b11
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
@@ -0,0 +1,889 @@
+/*
+ * Copyright (C) 2011 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.suggestions;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Message;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.MoreKeysPanel;
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.ViewLayoutUtils;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.util.ArrayList;
+
+public class SuggestionsView extends RelativeLayout implements OnClickListener,
+ OnLongClickListener {
+ public interface Listener {
+ public boolean addWordToDictionary(String word);
+ public void pickSuggestionManually(int index, CharSequence word, int x, int y);
+ }
+
+ // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
+ public static final int MAX_SUGGESTIONS = 18;
+
+ static final boolean DBG = LatinImeLogger.sDBG;
+
+ private final ViewGroup mSuggestionsStrip;
+ private KeyboardView mKeyboardView;
+
+ private final View mMoreSuggestionsContainer;
+ private final MoreSuggestionsView mMoreSuggestionsView;
+ private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
+ private final PopupWindow mMoreSuggestionsWindow;
+
+ private final ArrayList<TextView> mWords = new ArrayList<TextView>();
+ private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
+ private final ArrayList<View> mDividers = new ArrayList<View>();
+
+ private final PopupWindow mPreviewPopup;
+ private final TextView mPreviewText;
+
+ private Listener mListener;
+ private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+
+ private final SuggestionsViewParams mParams;
+ private static final float MIN_TEXT_XSCALE = 0.70f;
+
+ private final UiHandler mHandler = new UiHandler(this);
+
+ private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
+ private static final int MSG_HIDE_PREVIEW = 0;
+
+ public UiHandler(SuggestionsView outerInstance) {
+ super(outerInstance);
+ }
+
+ @Override
+ public void dispatchMessage(Message msg) {
+ final SuggestionsView suggestionsView = getOuterInstance();
+ switch (msg.what) {
+ case MSG_HIDE_PREVIEW:
+ suggestionsView.hidePreview();
+ break;
+ }
+ }
+
+ public void cancelHidePreview() {
+ removeMessages(MSG_HIDE_PREVIEW);
+ }
+
+ public void cancelAllMessages() {
+ cancelHidePreview();
+ }
+ }
+
+ private static class SuggestionsViewParams {
+ private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
+ private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
+ private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
+ private static final int PUNCTUATIONS_IN_STRIP = 5;
+
+ public final int mPadding;
+ public final int mDividerWidth;
+ public final int mSuggestionsStripHeight;
+ public final int mSuggestionsCountInStrip;
+ public final int mMoreSuggestionsRowHeight;
+ private int mMaxMoreSuggestionsRow;
+ public final float mMinMoreSuggestionsWidth;
+ public final int mMoreSuggestionsBottomGap;
+
+ private final ArrayList<TextView> mWords;
+ private final ArrayList<View> mDividers;
+ private final ArrayList<TextView> mInfos;
+
+ private final int mColorValidTypedWord;
+ private final int mColorTypedWord;
+ private final int mColorAutoCorrect;
+ private final int mColorSuggested;
+ private final float mAlphaObsoleted;
+ private final float mCenterSuggestionWeight;
+ private final int mCenterSuggestionIndex;
+ private final Drawable mMoreSuggestionsHint;
+ private static final String MORE_SUGGESTIONS_HINT = "\u2026";
+ private static final String LEFTWARDS_ARROW = "\u2190";
+
+ private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
+ private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
+ private static final int AUTO_CORRECT_BOLD = 0x01;
+ private static final int AUTO_CORRECT_UNDERLINE = 0x02;
+ private static final int VALID_TYPED_WORD_BOLD = 0x04;
+
+ private final int mSuggestionStripOption;
+
+ private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+
+ public boolean mMoreSuggestionsAvailable;
+
+ private final TextView mWordToSaveView;
+ private final TextView mLeftwardsArrowView;
+ private final TextView mHintToSaveView;
+
+ public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
+ ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos) {
+ mWords = words;
+ mDividers = dividers;
+ mInfos = infos;
+
+ final TextView word = words.get(0);
+ final View divider = dividers.get(0);
+ mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
+ divider.measure(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mDividerWidth = divider.getMeasuredWidth();
+
+ final Resources res = word.getResources();
+ mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
+ mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
+ final float alphaValidTypedWord = getPercent(a,
+ R.styleable.SuggestionsView_alphaValidTypedWord, 100);
+ final float alphaTypedWord = getPercent(a,
+ R.styleable.SuggestionsView_alphaTypedWord, 100);
+ final float alphaAutoCorrect = getPercent(a,
+ R.styleable.SuggestionsView_alphaAutoCorrect, 100);
+ final float alphaSuggested = getPercent(a,
+ R.styleable.SuggestionsView_alphaSuggested, 100);
+ mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
+ mColorValidTypedWord = applyAlpha(
+ a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0),
+ alphaValidTypedWord);
+ mColorTypedWord = applyAlpha(
+ a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
+ mColorAutoCorrect = applyAlpha(
+ a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
+ mColorSuggested = applyAlpha(
+ a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
+ mSuggestionsCountInStrip = a.getInt(
+ R.styleable.SuggestionsView_suggestionsCountInStrip,
+ DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
+ mCenterSuggestionWeight = getPercent(a,
+ R.styleable.SuggestionsView_centerSuggestionPercentile,
+ DEFAULT_CENTER_SUGGESTION_PERCENTILE);
+ mMaxMoreSuggestionsRow = a.getInt(
+ R.styleable.SuggestionsView_maxMoreSuggestionsRow,
+ DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
+ mMinMoreSuggestionsWidth = getRatio(a,
+ R.styleable.SuggestionsView_minMoreSuggestionsWidth);
+ a.recycle();
+
+ mMoreSuggestionsHint = getMoreSuggestionsHint(res,
+ res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
+ mCenterSuggestionIndex = mSuggestionsCountInStrip / 2;
+ mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
+ R.dimen.more_suggestions_bottom_gap);
+ mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
+ R.dimen.more_suggestions_row_height);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
+ mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
+ mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
+ }
+
+ public int getMaxMoreSuggestionsRow() {
+ return mMaxMoreSuggestionsRow;
+ }
+
+ private int getMoreSuggestionsHeight() {
+ return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
+ }
+
+ public int setMoreSuggestionsHeight(int remainingHeight) {
+ final int currentHeight = getMoreSuggestionsHeight();
+ if (currentHeight <= remainingHeight) {
+ return currentHeight;
+ }
+
+ mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
+ / mMoreSuggestionsRowHeight;
+ final int newHeight = getMoreSuggestionsHeight();
+ return newHeight;
+ }
+
+ private static Drawable getMoreSuggestionsHint(Resources res, float textSize, int color) {
+ final Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setTextAlign(Align.CENTER);
+ paint.setTextSize(textSize);
+ paint.setColor(color);
+ final Rect bounds = new Rect();
+ paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
+ final int width = Math.round(bounds.width() + 0.5f);
+ final int height = Math.round(bounds.height() + 0.5f);
+ final Bitmap buffer = Bitmap.createBitmap(
+ width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(buffer);
+ canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
+ return new BitmapDrawable(res, buffer);
+ }
+
+ // Read integer value in TypedArray as percent.
+ private static float getPercent(TypedArray a, int index, int defValue) {
+ return a.getInt(index, defValue) / 100.0f;
+ }
+
+ // Read fraction value in TypedArray as float.
+ private static float getRatio(TypedArray a, int index) {
+ return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
+ }
+
+ private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) {
+ final CharSequence word = suggestedWords.getWord(pos);
+ final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
+ final boolean isTypedWordValid = pos == 0 && suggestedWords.mTypedWordValid;
+ if (!isAutoCorrect && !isTypedWordValid)
+ return word;
+
+ final int len = word.length();
+ final Spannable spannedWord = new SpannableString(word);
+ final int option = mSuggestionStripOption;
+ if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
+ || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
+ spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
+ spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ }
+ return spannedWord;
+ }
+
+ private int getWordPosition(int index, SuggestedWords suggestedWords) {
+ // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
+ // suggestions.
+ final int centerPos = suggestedWords.willAutoCorrect() ? 1 : 0;
+ if (index == mCenterSuggestionIndex) {
+ return centerPos;
+ } else if (index == centerPos) {
+ return mCenterSuggestionIndex;
+ } else {
+ return index;
+ }
+ }
+
+ private int getSuggestionTextColor(int index, SuggestedWords suggestedWords, int pos) {
+ // TODO: Need to revisit this logic with bigram suggestions
+ final boolean isSuggested = (pos != 0);
+
+ final int color;
+ if (index == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
+ color = mColorAutoCorrect;
+ } else if (index == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
+ color = mColorValidTypedWord;
+ } else if (isSuggested) {
+ color = mColorSuggested;
+ } else {
+ color = mColorTypedWord;
+ }
+ if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
+ // If we auto-correct, then the autocorrection is in slot 0 and the typed word
+ // is in slot 1.
+ if (index == mCenterSuggestionIndex && suggestedWords.mHasAutoCorrectionCandidate
+ && Suggest.shouldBlockAutoCorrectionBySafetyNet(
+ suggestedWords.getWord(1).toString(), suggestedWords.getWord(0))) {
+ return 0xFFFF0000;
+ }
+ }
+
+ if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
+ return applyAlpha(color, mAlphaObsoleted);
+ } else {
+ return color;
+ }
+ }
+
+ private static int applyAlpha(final int color, final float alpha) {
+ final int newAlpha = (int)(Color.alpha(color) * alpha);
+ return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
+ }
+
+ private static void addDivider(final ViewGroup stripView, final View divider) {
+ stripView.addView(divider);
+ final LinearLayout.LayoutParams params =
+ (LinearLayout.LayoutParams)divider.getLayoutParams();
+ params.gravity = Gravity.CENTER;
+ }
+
+ public void layout(SuggestedWords suggestedWords, ViewGroup stripView, ViewGroup placer,
+ int stripWidth) {
+ if (suggestedWords.mIsPunctuationSuggestions) {
+ layoutPunctuationSuggestions(suggestedWords, stripView);
+ return;
+ }
+
+ final int countInStrip = mSuggestionsCountInStrip;
+ setupTexts(suggestedWords, countInStrip);
+ mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
+ int x = 0;
+ for (int index = 0; index < countInStrip; index++) {
+ final int pos = getWordPosition(index, suggestedWords);
+
+ if (index != 0) {
+ final View divider = mDividers.get(pos);
+ // Add divider if this isn't the left most suggestion in suggestions strip.
+ addDivider(stripView, divider);
+ x += divider.getMeasuredWidth();
+ }
+
+ final CharSequence styled = mTexts.get(pos);
+ final TextView word = mWords.get(pos);
+ if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
+ // TODO: This "more suggestions hint" should have nicely designed icon.
+ word.setCompoundDrawablesWithIntrinsicBounds(
+ null, null, null, mMoreSuggestionsHint);
+ // HACK: To align with other TextView that has no compound drawables.
+ word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
+ } else {
+ word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ }
+
+ // Disable this suggestion if the suggestion is null or empty.
+ word.setEnabled(!TextUtils.isEmpty(styled));
+ word.setTextColor(getSuggestionTextColor(index, suggestedWords, pos));
+ final int width = getSuggestionWidth(index, stripWidth);
+ final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
+ final float scaleX = word.getTextScaleX();
+ word.setText(text); // TextView.setText() resets text scale x to 1.0.
+ word.setTextScaleX(scaleX);
+ stripView.addView(word);
+ setLayoutWeight(
+ word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
+ x += word.getMeasuredWidth();
+
+ if (DBG && pos < suggestedWords.size()) {
+ final CharSequence debugInfo = Utils.getDebugInfo(suggestedWords, pos);
+ if (debugInfo != null) {
+ final TextView info = mInfos.get(pos);
+ info.setText(debugInfo);
+ placer.addView(info);
+ info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ final int infoWidth = info.getMeasuredWidth();
+ final int y = info.getMeasuredHeight();
+ ViewLayoutUtils.placeViewAt(
+ info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
+ }
+ }
+ }
+ }
+
+ private int getSuggestionWidth(int index, int maxWidth) {
+ final int paddings = mPadding * mSuggestionsCountInStrip;
+ final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
+ final int availableWidth = maxWidth - paddings - dividers;
+ return (int)(availableWidth * getSuggestionWeight(index));
+ }
+
+ private float getSuggestionWeight(int index) {
+ if (index == mCenterSuggestionIndex) {
+ return mCenterSuggestionWeight;
+ } else {
+ // TODO: Revisit this for cases of 5 or more suggestions
+ return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
+ }
+ }
+
+ private void setupTexts(SuggestedWords suggestedWords, int countInStrip) {
+ mTexts.clear();
+ final int count = Math.min(suggestedWords.size(), countInStrip);
+ for (int pos = 0; pos < count; pos++) {
+ final CharSequence styled = getStyledSuggestionWord(suggestedWords, pos);
+ mTexts.add(styled);
+ }
+ for (int pos = count; pos < countInStrip; pos++) {
+ // Make this inactive for touches in layout().
+ mTexts.add(null);
+ }
+ }
+
+ private void layoutPunctuationSuggestions(SuggestedWords suggestedWords,
+ ViewGroup stripView) {
+ final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
+ for (int index = 0; index < countInStrip; index++) {
+ if (index != 0) {
+ // Add divider if this isn't the left most suggestion in suggestions strip.
+ addDivider(stripView, mDividers.get(index));
+ }
+
+ final TextView word = mWords.get(index);
+ word.setEnabled(true);
+ word.setTextColor(mColorAutoCorrect);
+ final CharSequence text = suggestedWords.getWord(index);
+ word.setText(text);
+ word.setTextScaleX(1.0f);
+ word.setCompoundDrawables(null, null, null, null);
+ stripView.addView(word);
+ setLayoutWeight(word, 1.0f, mSuggestionsStripHeight);
+ }
+ mMoreSuggestionsAvailable = false;
+ }
+
+ public void layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView,
+ int stripWidth, CharSequence hintText, OnClickListener listener) {
+ final int width = stripWidth - mDividerWidth - mPadding * 2;
+
+ final TextView wordView = mWordToSaveView;
+ wordView.setTextColor(mColorTypedWord);
+ final int wordWidth = (int)(width * mCenterSuggestionWeight);
+ final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
+ final float wordScaleX = wordView.getTextScaleX();
+ wordView.setTag(word);
+ wordView.setText(text);
+ wordView.setTextScaleX(wordScaleX);
+ stripView.addView(wordView);
+ setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ stripView.addView(mDividers.get(0));
+
+ final TextView leftArrowView = mLeftwardsArrowView;
+ leftArrowView.setTextColor(mColorAutoCorrect);
+ leftArrowView.setText(LEFTWARDS_ARROW);
+ stripView.addView(leftArrowView);
+
+ final TextView hintView = mHintToSaveView;
+ hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+ hintView.setTextColor(mColorAutoCorrect);
+ final int hintWidth = width - wordWidth - leftArrowView.getWidth();
+ final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
+ hintView.setText(hintText);
+ hintView.setTextScaleX(hintScaleX);
+ stripView.addView(hintView);
+ setLayoutWeight(
+ hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
+
+ wordView.setOnClickListener(listener);
+ leftArrowView.setOnClickListener(listener);
+ hintView.setOnClickListener(listener);
+ }
+
+ public CharSequence getAddToDictionaryWord() {
+ return (CharSequence)mWordToSaveView.getTag();
+ }
+
+ public boolean isAddToDictionaryShowing(View v) {
+ return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
+ }
+
+ private static void setLayoutWeight(View v, float weight, int height) {
+ final ViewGroup.LayoutParams lp = v.getLayoutParams();
+ if (lp instanceof LinearLayout.LayoutParams) {
+ final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
+ llp.weight = weight;
+ llp.width = 0;
+ llp.height = height;
+ }
+ }
+
+ private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) {
+ paint.setTextScaleX(1.0f);
+ final int width = getTextWidth(text, paint);
+ if (width <= maxWidth) {
+ return 1.0f;
+ }
+ return maxWidth / (float)width;
+ }
+
+ private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
+ TextPaint paint) {
+ if (text == null) return null;
+ paint.setTextScaleX(1.0f);
+ final int width = getTextWidth(text, paint);
+ if (width <= maxWidth) {
+ return text;
+ }
+ final float scaleX = maxWidth / (float)width;
+ if (scaleX >= MIN_TEXT_XSCALE) {
+ paint.setTextScaleX(scaleX);
+ return text;
+ }
+
+ // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
+ // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
+ final CharSequence ellipsized = TextUtils.ellipsize(
+ text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
+ paint.setTextScaleX(MIN_TEXT_XSCALE);
+ return ellipsized;
+ }
+
+ private static int getTextWidth(CharSequence text, TextPaint paint) {
+ if (TextUtils.isEmpty(text)) return 0;
+ final Typeface savedTypeface = paint.getTypeface();
+ paint.setTypeface(getTextTypeface(text));
+ final int len = text.length();
+ final float[] widths = new float[len];
+ final int count = paint.getTextWidths(text, 0, len, widths);
+ int width = 0;
+ for (int i = 0; i < count; i++) {
+ width += Math.round(widths[i] + 0.5f);
+ }
+ paint.setTypeface(savedTypeface);
+ return width;
+ }
+
+ private static Typeface getTextTypeface(CharSequence text) {
+ if (!(text instanceof SpannableString))
+ return Typeface.DEFAULT;
+
+ final SpannableString ss = (SpannableString)text;
+ final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
+ if (styles.length == 0)
+ return Typeface.DEFAULT;
+
+ switch (styles[0].getStyle()) {
+ case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
+ // TODO: BOLD_ITALIC, ITALIC case?
+ default: return Typeface.DEFAULT;
+ }
+ }
+ }
+
+ /**
+ * Construct a {@link SuggestionsView} for showing suggestions to be picked by the user.
+ * @param context
+ * @param attrs
+ */
+ public SuggestionsView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.suggestionsViewStyle);
+ }
+
+ public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.suggestions_strip, this);
+
+ mPreviewPopup = new PopupWindow(context);
+ mPreviewText = (TextView) inflater.inflate(R.layout.suggestion_preview, null);
+ mPreviewPopup.setWindowLayoutMode(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ mPreviewPopup.setContentView(mPreviewText);
+ mPreviewPopup.setBackgroundDrawable(null);
+
+ mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
+ for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) {
+ final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
+ word.setTag(pos);
+ word.setOnClickListener(this);
+ word.setOnLongClickListener(this);
+ mWords.add(word);
+ final View divider = inflater.inflate(R.layout.suggestion_divider, null);
+ divider.setTag(pos);
+ divider.setOnClickListener(this);
+ mDividers.add(divider);
+ mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
+ }
+
+ mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
+
+ mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
+ mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
+ .findViewById(R.id.more_suggestions_view);
+ mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView);
+
+ final PopupWindow moreWindow = new PopupWindow(context);
+ moreWindow.setWindowLayoutMode(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ moreWindow.setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));
+ moreWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+ moreWindow.setFocusable(true);
+ moreWindow.setOutsideTouchable(true);
+ moreWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
+ @Override
+ public void onDismiss() {
+ mKeyboardView.dimEntireKeyboard(false);
+ }
+ });
+ mMoreSuggestionsWindow = moreWindow;
+
+ final Resources res = context.getResources();
+ mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
+ R.dimen.more_suggestions_modal_tolerance);
+ mMoreSuggestionsSlidingDetector = new GestureDetector(
+ context, mMoreSuggestionsSlidingListener);
+ }
+
+ /**
+ * A connection back to the input method.
+ * @param listener
+ */
+ public void setListener(Listener listener, View inputView) {
+ mListener = listener;
+ mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
+ }
+
+ public void setSuggestions(SuggestedWords suggestedWords) {
+ if (suggestedWords == null)
+ return;
+
+ clear();
+ mSuggestedWords = suggestedWords;
+ mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.suggestionsView_setSuggestions(mSuggestedWords);
+ }
+ }
+
+ public int setMoreSuggestionsHeight(int remainingHeight) {
+ return mParams.setMoreSuggestionsHeight(remainingHeight);
+ }
+
+ public boolean isShowingAddToDictionaryHint() {
+ return mSuggestionsStrip.getChildCount() > 0
+ && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
+ }
+
+ public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) {
+ clear();
+ mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
+ }
+
+ public boolean dismissAddToDictionaryHint() {
+ if (isShowingAddToDictionaryHint()) {
+ clear();
+ return true;
+ }
+ return false;
+ }
+
+ public SuggestedWords getSuggestions() {
+ return mSuggestedWords;
+ }
+
+ public void clear() {
+ mSuggestionsStrip.removeAllViews();
+ removeAllViews();
+ addView(mSuggestionsStrip);
+ dismissMoreSuggestions();
+ }
+
+ private void hidePreview() {
+ mPreviewPopup.dismiss();
+ }
+
+ private void addToDictionary(CharSequence word) {
+ mListener.addWordToDictionary(word.toString());
+ }
+
+ private final KeyboardActionListener mMoreSuggestionsListener =
+ new KeyboardActionListener.Adapter() {
+ @Override
+ public boolean onCustomRequest(int requestCode) {
+ final int index = requestCode;
+ final CharSequence word = mSuggestedWords.getWord(index);
+ // TODO: change caller path so coordinates are passed through here
+ mListener.pickSuggestionManually(index, word, NOT_A_TOUCH_COORDINATE,
+ NOT_A_TOUCH_COORDINATE);
+ dismissMoreSuggestions();
+ return true;
+ }
+
+ @Override
+ public void onCancelInput() {
+ dismissMoreSuggestions();
+ }
+ };
+
+ private final MoreKeysPanel.Controller mMoreSuggestionsController =
+ new MoreKeysPanel.Controller() {
+ @Override
+ public boolean dismissMoreKeysPanel() {
+ return dismissMoreSuggestions();
+ }
+ };
+
+ private boolean dismissMoreSuggestions() {
+ if (mMoreSuggestionsWindow.isShowing()) {
+ mMoreSuggestionsWindow.dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ return showMoreSuggestions();
+ }
+
+ private boolean showMoreSuggestions() {
+ final SuggestionsViewParams params = mParams;
+ if (params.mMoreSuggestionsAvailable) {
+ final int stripWidth = getWidth();
+ final View container = mMoreSuggestionsContainer;
+ final int maxWidth = stripWidth - container.getPaddingLeft()
+ - container.getPaddingRight();
+ final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
+ builder.layout(mSuggestedWords, params.mSuggestionsCountInStrip, maxWidth,
+ (int)(maxWidth * params.mMinMoreSuggestionsWidth),
+ params.getMaxMoreSuggestionsRow());
+ mMoreSuggestionsView.setKeyboard(builder.build());
+ container.measure(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
+ final int pointX = stripWidth / 2;
+ final int pointY = -params.mMoreSuggestionsBottomGap;
+ moreKeysPanel.showMoreKeysPanel(
+ this, mMoreSuggestionsController, pointX, pointY,
+ mMoreSuggestionsWindow, mMoreSuggestionsListener);
+ mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
+ mOriginX = mLastX;
+ mOriginY = mLastY;
+ mKeyboardView.dimEntireKeyboard(true);
+ for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
+ mWords.get(i).setPressed(false);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // Working variables for onLongClick and dispatchTouchEvent.
+ private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
+ private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0;
+ private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1;
+ private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2;
+ private int mLastX;
+ private int mLastY;
+ private int mOriginX;
+ private int mOriginY;
+ private final int mMoreSuggestionsModalTolerance;
+ private final GestureDetector mMoreSuggestionsSlidingDetector;
+ private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) {
+ final float dy = me.getY() - down.getY();
+ if (deltaY > 0 && dy < 0) {
+ return showMoreSuggestions();
+ }
+ return false;
+ }
+ };
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent me) {
+ if (!mMoreSuggestionsWindow.isShowing()
+ || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) {
+ mLastX = (int)me.getX();
+ mLastY = (int)me.getY();
+ if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) {
+ return true;
+ }
+ return super.dispatchTouchEvent(me);
+ }
+
+ final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
+ final int action = me.getAction();
+ final long eventTime = me.getEventTime();
+ final int index = me.getActionIndex();
+ final int id = me.getPointerId(index);
+ final PointerTracker tracker = PointerTracker.getPointerTracker(id, moreKeysPanel);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ final int translatedX = moreKeysPanel.translateX(x);
+ final int translatedY = moreKeysPanel.translateY(y);
+
+ if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) {
+ if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
+ || mOriginY - y >= mMoreSuggestionsModalTolerance) {
+ // Decided to be in the sliding input mode only when the touch point has been moved
+ // upward.
+ mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE;
+ tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
+ } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
+ // Decided to be in the modal input mode
+ mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
+ }
+ return true;
+ }
+
+ // MORE_SUGGESTIONS_IN_SLIDING_MODE
+ tracker.processMotionEvent(action, translatedX, translatedY, eventTime, moreKeysPanel);
+ return true;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mParams.isAddToDictionaryShowing(view)) {
+ addToDictionary(mParams.getAddToDictionaryWord());
+ clear();
+ return;
+ }
+
+ final Object tag = view.getTag();
+ if (!(tag instanceof Integer))
+ return;
+ final int index = (Integer) tag;
+ if (index >= mSuggestedWords.size())
+ return;
+
+ final CharSequence word = mSuggestedWords.getWord(index);
+ mListener.pickSuggestionManually(index, word, mLastX, mLastY);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mHandler.cancelAllMessages();
+ hidePreview();
+ dismissMoreSuggestions();
+ }
+}