From 979f8690967ff5409fe18f5085858ccdb8e0ccf1 Mon Sep 17 00:00:00 2001 From: satok Date: Fri, 20 Aug 2010 14:35:02 +0900 Subject: [PATCH] DO NOT MERGE. Backport LatinIME from master to Gingerbread TODO: Cleanup will follow. Change-Id: I4a68ba9f2f55760aa24187f1f13fdfa8a0b70963 --- java/AndroidManifest.xml | 2 - java/proguard.flags | 5 + .../sym_keyboard_feedback_delete.png | Bin 1278 -> 0 bytes java/res/drawable-hdpi/btn_close_normal.png | Bin 0 -> 1884 bytes java/res/drawable-hdpi/btn_close_pressed.png | Bin 0 -> 2737 bytes java/res/drawable-hdpi/btn_close_selected.png | Bin 0 -> 2807 bytes .../btn_keyboard_key_normal_off_stone.9.png | Bin 0 -> 2348 bytes .../btn_keyboard_key_normal_on_stone.9.png | Bin 0 -> 2379 bytes .../btn_keyboard_key_normal_stone.9.png | Bin 0 -> 2211 bytes .../btn_keyboard_normal_metal.9.png | Bin 0 -> 7614 bytes .../drawable-hdpi/btn_keyboard_toggle_off.png | Bin 0 -> 1425 bytes .../drawable-hdpi/btn_keyboard_toggle_on.png | Bin 0 -> 1733 bytes java/res/drawable-hdpi/btn_led_off.9.png | Bin 0 -> 1279 bytes java/res/drawable-hdpi/btn_led_on.9.png | Bin 0 -> 1607 bytes .../dialog_top_dark_bottom_medium.9.png | Bin 0 -> 49933 bytes .../dialog_top_dark_bottom_medium.png | Bin 1976 -> 0 bytes .../res/drawable-hdpi/ic_subtype_keyboard.png | Bin 0 -> 1068 bytes java/res/drawable-hdpi/ic_subtype_mic.png | Bin 0 -> 681 bytes .../keyboard_key_feedback_background.9.png | Bin 0 -> 1372 bytes ...eyboard_key_feedback_more_background.9.png | Bin 0 -> 1637 bytes .../keyboard_popup_panel_background.9.png | Bin 0 -> 1443 bytes ...eyboard_popup_panel_trans_background.9.png | Bin 0 -> 1677 bytes .../drawable-hdpi/sym_bkeyboard_123_mic.png | Bin 0 -> 2576 bytes .../drawable-hdpi/sym_bkeyboard_delete.png | Bin 0 -> 2314 bytes java/res/drawable-hdpi/sym_bkeyboard_done.png | Bin 0 -> 1588 bytes .../res/drawable-hdpi/sym_bkeyboard_globe.png | Bin 0 -> 2136 bytes java/res/drawable-hdpi/sym_bkeyboard_mic.png | Bin 0 -> 1410 bytes java/res/drawable-hdpi/sym_bkeyboard_num0.png | Bin 0 -> 1903 bytes java/res/drawable-hdpi/sym_bkeyboard_num1.png | Bin 0 -> 792 bytes java/res/drawable-hdpi/sym_bkeyboard_num2.png | Bin 0 -> 3241 bytes java/res/drawable-hdpi/sym_bkeyboard_num3.png | Bin 0 -> 2829 bytes java/res/drawable-hdpi/sym_bkeyboard_num4.png | Bin 0 -> 2638 bytes java/res/drawable-hdpi/sym_bkeyboard_num5.png | Bin 0 -> 2532 bytes java/res/drawable-hdpi/sym_bkeyboard_num6.png | Bin 0 -> 3568 bytes java/res/drawable-hdpi/sym_bkeyboard_num7.png | Bin 0 -> 3687 bytes java/res/drawable-hdpi/sym_bkeyboard_num8.png | Bin 0 -> 2952 bytes java/res/drawable-hdpi/sym_bkeyboard_num9.png | Bin 0 -> 3887 bytes .../drawable-hdpi/sym_bkeyboard_numalt.png | Bin 0 -> 2971 bytes .../drawable-hdpi/sym_bkeyboard_numpound.png | Bin 0 -> 1577 bytes .../drawable-hdpi/sym_bkeyboard_numstar.png | Bin 0 -> 1742 bytes .../drawable-hdpi/sym_bkeyboard_return.png | Bin 0 -> 1111 bytes .../drawable-hdpi/sym_bkeyboard_search.png | Bin 0 -> 1612 bytes .../res/drawable-hdpi/sym_bkeyboard_shift.png | Bin 0 -> 1474 bytes .../sym_bkeyboard_shift_locked.png | Bin 0 -> 1115 bytes .../res/drawable-hdpi/sym_bkeyboard_space.png | Bin 0 -> 358 bytes java/res/drawable-hdpi/sym_bkeyboard_tab.png | Bin 0 -> 1008 bytes .../drawable-hdpi/sym_bkeyboard_tabprev.png | Bin 0 -> 1026 bytes java/res/drawable-hdpi/voice_swipe_hint.png | Bin 0 -> 5965 bytes .../btn_keyboard_key_normal_off_stone.9.png | Bin 0 -> 2691 bytes .../btn_keyboard_key_normal_on_stone.9.png | Bin 0 -> 2720 bytes .../btn_keyboard_key_normal_stone.9.png | Bin 0 -> 2517 bytes .../btn_keyboard_key_normal_off.9.png | Bin 860 -> 0 bytes .../btn_keyboard_key_normal_off_stone.9.png | Bin 0 -> 2691 bytes .../btn_keyboard_key_normal_on.9.png | Bin 926 -> 0 bytes .../btn_keyboard_key_normal_on_stone.9.png | Bin 0 -> 2720 bytes .../btn_keyboard_key_normal_stone.9.png | Bin 0 -> 2517 bytes .../btn_keyboard_key_pressed_off.9.png | Bin 836 -> 0 bytes .../btn_keyboard_key_pressed_on.9.png | Bin 886 -> 0 bytes java/res/drawable-mdpi/btn_close_normal.png | Bin 0 -> 1259 bytes java/res/drawable-mdpi/btn_close_pressed.png | Bin 0 -> 1726 bytes java/res/drawable-mdpi/btn_close_selected.png | Bin 0 -> 1716 bytes .../btn_keyboard_key_normal_off_stone.9.png | Bin 0 -> 2348 bytes .../btn_keyboard_key_normal_on_stone.9.png | Bin 0 -> 2379 bytes .../btn_keyboard_key_normal_stone.9.png | Bin 0 -> 2211 bytes .../btn_keyboard_normal_metal.9.png | Bin 0 -> 5069 bytes .../drawable-mdpi/btn_keyboard_toggle_off.png | Bin 0 -> 693 bytes .../drawable-mdpi/btn_keyboard_toggle_on.png | Bin 0 -> 812 bytes java/res/drawable-mdpi/btn_led_off.9.png | Bin 0 -> 505 bytes java/res/drawable-mdpi/btn_led_on.9.png | Bin 0 -> 575 bytes .../{drawable => drawable-mdpi}/cancel.png | Bin .../{drawable => drawable-mdpi}/caution.png | Bin .../dialog_top_dark_bottom_medium.9.png | Bin .../ic_dialog_alert_large.png | Bin .../ic_dialog_voice_input.png | Bin .../ic_dialog_wave_0_0.png | Bin .../ic_dialog_wave_1_3.png | Bin .../ic_dialog_wave_2_3.png | Bin .../ic_dialog_wave_3_3.png | Bin .../ic_dialog_wave_4_3.png | Bin .../res/drawable-mdpi/ic_subtype_keyboard.png | Bin 0 -> 498 bytes java/res/drawable-mdpi/ic_subtype_mic.png | Bin 0 -> 483 bytes .../keyboard_key_feedback_background.9.png | Bin 0 -> 1182 bytes ...eyboard_key_feedback_more_background.9.png | Bin 0 -> 1385 bytes .../keyboard_popup_panel_background.9.png | Bin 0 -> 996 bytes ...eyboard_popup_panel_trans_background.9.png | Bin 0 -> 3734 bytes .../{drawable => drawable-mdpi}/mic_slash.png | Bin .../{drawable => drawable-mdpi}/ok_cancel.png | Bin .../speak_now_level0.png | Bin .../speak_now_level1.png | Bin .../speak_now_level2.png | Bin .../speak_now_level3.png | Bin .../speak_now_level4.png | Bin .../speak_now_level5.png | Bin .../speak_now_level6.png | Bin .../drawable-mdpi/sym_bkeyboard_123_mic.png | Bin 0 -> 1520 bytes .../drawable-mdpi/sym_bkeyboard_delete.png | Bin 0 -> 800 bytes java/res/drawable-mdpi/sym_bkeyboard_done.png | Bin 0 -> 775 bytes .../res/drawable-mdpi/sym_bkeyboard_globe.png | Bin 0 -> 1303 bytes java/res/drawable-mdpi/sym_bkeyboard_mic.png | Bin 0 -> 838 bytes java/res/drawable-mdpi/sym_bkeyboard_num0.png | Bin 0 -> 1148 bytes java/res/drawable-mdpi/sym_bkeyboard_num1.png | Bin 0 -> 493 bytes java/res/drawable-mdpi/sym_bkeyboard_num2.png | Bin 0 -> 1785 bytes java/res/drawable-mdpi/sym_bkeyboard_num3.png | Bin 0 -> 1675 bytes java/res/drawable-mdpi/sym_bkeyboard_num4.png | Bin 0 -> 1530 bytes java/res/drawable-mdpi/sym_bkeyboard_num5.png | Bin 0 -> 1411 bytes java/res/drawable-mdpi/sym_bkeyboard_num6.png | Bin 0 -> 1943 bytes java/res/drawable-mdpi/sym_bkeyboard_num7.png | Bin 0 -> 2040 bytes java/res/drawable-mdpi/sym_bkeyboard_num8.png | Bin 0 -> 1618 bytes java/res/drawable-mdpi/sym_bkeyboard_num9.png | Bin 0 -> 2167 bytes .../drawable-mdpi/sym_bkeyboard_numalt.png | Bin 0 -> 1670 bytes .../drawable-mdpi/sym_bkeyboard_numpound.png | Bin 0 -> 910 bytes .../drawable-mdpi/sym_bkeyboard_numstar.png | Bin 0 -> 943 bytes .../drawable-mdpi/sym_bkeyboard_return.png | Bin 0 -> 834 bytes .../drawable-mdpi/sym_bkeyboard_search.png | Bin 0 -> 1042 bytes .../res/drawable-mdpi/sym_bkeyboard_shift.png | Bin 0 -> 998 bytes .../sym_bkeyboard_shift_locked.png | Bin 0 -> 787 bytes .../res/drawable-mdpi/sym_bkeyboard_space.png | Bin 0 -> 411 bytes java/res/drawable-mdpi/sym_bkeyboard_tab.png | Bin 0 -> 627 bytes .../drawable-mdpi/sym_bkeyboard_tabprev.png | Bin 0 -> 605 bytes .../voice_ime_background.9.png | Bin .../voice_swipe_hint.png | Bin .../{drawable => drawable-mdpi}/working.png | Bin java/res/drawable/btn_close.xml | 27 + java/res/drawable/btn_keyboard_key2.xml | 36 + java/res/drawable/btn_keyboard_key3.xml | 36 + java/res/drawable/btn_keyboard_key_stone.xml | 36 + java/res/drawable/keyboard_key_feedback.xml | 22 + .../res/layout/{input.xml => input_basic.xml} | 6 +- java/res/layout/input_basic_highcontrast.xml | 32 + java/res/layout/input_stone_bold.xml | 37 + java/res/layout/input_stone_normal.xml | 35 + java/res/layout/input_stone_popup.xml | 50 + java/res/layout/input_trans.xml | 6 +- java/res/layout/keyboard_key_preview.xml | 29 + java/res/layout/keyboard_popup_keyboard.xml | 47 + java/res/values-cs/strings.xml | 12 + java/res/values-da/strings.xml | 12 + java/res/values-de/strings.xml | 12 + java/res/values-el/strings.xml | 12 + java/res/values-es-rUS/strings.xml | 12 + java/res/values-es/strings.xml | 12 + java/res/values-fr/strings.xml | 12 + java/res/values-it/strings.xml | 12 + java/res/values-ja/strings.xml | 12 + java/res/values-ko/strings.xml | 12 + java/res/values-nb/strings.xml | 12 + java/res/values-nl/strings.xml | 12 + java/res/values-pl/strings.xml | 12 + java/res/values-pt-rPT/strings.xml | 12 + java/res/values-pt/strings.xml | 12 + java/res/values-rm/strings.xml | 145 ++ java/res/values-ru/strings.xml | 12 + java/res/values-sr/strings.xml | 299 +++ java/res/values-sv/strings.xml | 12 + java/res/values-tr/strings.xml | 12 + java/res/values-zh-rCN/strings.xml | 12 + java/res/values-zh-rTW/strings.xml | 12 + java/res/values/attrs.xml | 69 + java/res/values/bools.xml | 3 + java/res/values/colors.xml | 10 +- java/res/values/dimens.xml | 7 +- java/res/values/donottranslate.xml | 4 +- java/res/values/strings.xml | 41 +- java/res/values/styles.xml | 35 + java/res/xml-da/kbd_qwerty.xml | 213 +++ java/res/xml-da/kbd_qwerty_black.xml | 213 +++ java/res/xml-de/kbd_qwerty_black.xml | 189 ++ java/res/xml-fr/kbd_qwerty_black.xml | 192 ++ java/res/xml-iw/kbd_qwerty.xml | 164 ++ java/res/xml-iw/kbd_qwerty_black.xml | 164 ++ java/res/xml-nb/kbd_qwerty.xml | 211 +++ java/res/xml-nb/kbd_qwerty_black.xml | 211 +++ java/res/xml-ru/kbd_qwerty_black.xml | 175 ++ java/res/xml-sr/kbd_qwerty.xml | 171 ++ java/res/xml-sr/kbd_qwerty_black.xml | 171 ++ java/res/xml-sv/kbd_qwerty_black.xml | 215 +++ java/res/xml/dictionary.xml | 23 + java/res/xml/kbd_alpha_black.xml | 106 ++ java/res/xml/kbd_phone_black.xml | 67 + java/res/xml/kbd_phone_symbols_black.xml | 70 + java/res/xml/kbd_qwerty_black.xml | 207 +++ java/res/xml/kbd_symbols_black.xml | 141 ++ java/res/xml/kbd_symbols_shift.xml | 5 +- java/res/xml/kbd_symbols_shift_black.xml | 107 ++ java/res/xml/prefs.xml | 41 +- .../inputmethod/latin/AutoDictionary.java | 14 +- .../inputmethod/latin/BinaryDictionary.java | 143 +- .../inputmethod/latin/CandidateView.java | 55 +- .../inputmethod/latin/ContactsDictionary.java | 36 +- .../android/inputmethod/latin/Dictionary.java | 27 +- .../{voice => latin}/EditingUtil.java | 99 +- .../latin/ExpandableDictionary.java | 249 ++- .../latin/InputLanguageSelection.java | 5 +- .../inputmethod/latin/KeyboardSwitcher.java | 220 ++- .../android/inputmethod/latin/LatinIME.java | 765 ++++++-- .../inputmethod/latin/LatinIMESettings.java | 38 +- .../inputmethod/latin/LatinIMEUtil.java | 78 + .../inputmethod/latin/LatinImeLogger.java | 847 +++++++++ .../inputmethod/latin/LatinKeyboard.java | 87 +- .../latin/LatinKeyboardBaseView.java | 1633 +++++++++++++++++ .../inputmethod/latin/LatinKeyboardView.java | 82 +- .../android/inputmethod/latin/Suggest.java | 239 ++- .../inputmethod/latin/TextEntryState.java | 179 +- .../latin/UserBigramDictionary.java | 402 ++++ .../inputmethod/latin/UserDictionary.java | 3 +- .../inputmethod/latin/WordComposer.java | 11 +- .../inputmethod/voice/LatinIMEWithVoice.java | 28 - .../voice/LatinIMEWithVoiceSettings.java | 21 - .../android/inputmethod/voice/VoiceInput.java | 74 +- .../voicesearch/LatinIMEWithVoice.java | 29 - .../LatinIMEWithVoiceSettings.java | 5 - native/Android.mk | 16 +- ...oid_inputmethod_latin_BinaryDictionary.cpp | 105 +- native/src/char_utils.cpp | 899 +++++++++ native/src/char_utils.h | 26 + native/src/dictionary.cpp | 319 +++- native/src/dictionary.h | 27 +- tests/Android.mk | 17 + tests/AndroidManifest.xml | 33 + tests/data/bigramlist.xml | 36 + tests/data/wordlist.xml | 244 +++ tests/res/raw/test.dict | Bin 0 -> 2829 bytes tests/res/raw/testtext.txt | 24 + .../inputmethod/latin/ImeLoggerTests.java | 59 + .../inputmethod/latin/SuggestHelper.java | 268 +++ .../latin/SuggestPerformanceTests.java | 126 ++ .../inputmethod/latin/SuggestTests.java | 172 ++ .../inputmethod/latin/UserBigramTests.java | 100 + 228 files changed, 11427 insertions(+), 728 deletions(-) delete mode 100755 java/res/drawable-en-hdpi/sym_keyboard_feedback_delete.png create mode 100644 java/res/drawable-hdpi/btn_close_normal.png create mode 100644 java/res/drawable-hdpi/btn_close_pressed.png create mode 100644 java/res/drawable-hdpi/btn_close_selected.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_key_normal_off_stone.9.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_key_normal_on_stone.9.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_key_normal_stone.9.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_normal_metal.9.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_toggle_off.png create mode 100644 java/res/drawable-hdpi/btn_keyboard_toggle_on.png create mode 100644 java/res/drawable-hdpi/btn_led_off.9.png create mode 100644 java/res/drawable-hdpi/btn_led_on.9.png create mode 100644 java/res/drawable-hdpi/dialog_top_dark_bottom_medium.9.png delete mode 100755 java/res/drawable-hdpi/dialog_top_dark_bottom_medium.png create mode 100755 java/res/drawable-hdpi/ic_subtype_keyboard.png create mode 100644 java/res/drawable-hdpi/ic_subtype_mic.png create mode 100644 java/res/drawable-hdpi/keyboard_key_feedback_background.9.png create mode 100644 java/res/drawable-hdpi/keyboard_key_feedback_more_background.9.png create mode 100644 java/res/drawable-hdpi/keyboard_popup_panel_background.9.png create mode 100644 java/res/drawable-hdpi/keyboard_popup_panel_trans_background.9.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_123_mic.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_delete.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_done.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_globe.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_mic.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num0.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num1.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num2.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num3.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num4.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num5.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num6.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num7.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num8.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_num9.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_numalt.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_numpound.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_numstar.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_return.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_search.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_shift.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_shift_locked.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_space.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_tab.png create mode 100644 java/res/drawable-hdpi/sym_bkeyboard_tabprev.png create mode 100644 java/res/drawable-hdpi/voice_swipe_hint.png create mode 100644 java/res/drawable-land-hdpi/btn_keyboard_key_normal_off_stone.9.png create mode 100644 java/res/drawable-land-hdpi/btn_keyboard_key_normal_on_stone.9.png create mode 100644 java/res/drawable-land-hdpi/btn_keyboard_key_normal_stone.9.png delete mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_normal_off.9.png create mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_normal_off_stone.9.png delete mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_normal_on.9.png create mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_normal_on_stone.9.png create mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_normal_stone.9.png delete mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_pressed_off.9.png delete mode 100644 java/res/drawable-land-mdpi/btn_keyboard_key_pressed_on.9.png create mode 100644 java/res/drawable-mdpi/btn_close_normal.png create mode 100644 java/res/drawable-mdpi/btn_close_pressed.png create mode 100644 java/res/drawable-mdpi/btn_close_selected.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_key_normal_off_stone.9.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_key_normal_on_stone.9.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_key_normal_stone.9.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_normal_metal.9.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_toggle_off.png create mode 100644 java/res/drawable-mdpi/btn_keyboard_toggle_on.png create mode 100644 java/res/drawable-mdpi/btn_led_off.9.png create mode 100644 java/res/drawable-mdpi/btn_led_on.9.png rename java/res/{drawable => drawable-mdpi}/cancel.png (100%) rename java/res/{drawable => drawable-mdpi}/caution.png (100%) rename java/res/{drawable => drawable-mdpi}/dialog_top_dark_bottom_medium.9.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_alert_large.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_voice_input.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_wave_0_0.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_wave_1_3.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_wave_2_3.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_wave_3_3.png (100%) rename java/res/{drawable => drawable-mdpi}/ic_dialog_wave_4_3.png (100%) create mode 100755 java/res/drawable-mdpi/ic_subtype_keyboard.png create mode 100644 java/res/drawable-mdpi/ic_subtype_mic.png create mode 100644 java/res/drawable-mdpi/keyboard_key_feedback_background.9.png create mode 100755 java/res/drawable-mdpi/keyboard_key_feedback_more_background.9.png create mode 100644 java/res/drawable-mdpi/keyboard_popup_panel_background.9.png create mode 100644 java/res/drawable-mdpi/keyboard_popup_panel_trans_background.9.png rename java/res/{drawable => drawable-mdpi}/mic_slash.png (100%) rename java/res/{drawable => drawable-mdpi}/ok_cancel.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level0.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level1.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level2.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level3.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level4.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level5.png (100%) rename java/res/{drawable => drawable-mdpi}/speak_now_level6.png (100%) create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_123_mic.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_delete.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_done.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_globe.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_mic.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num0.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num1.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num2.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num3.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num4.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num5.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num6.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num7.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num8.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_num9.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_numalt.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_numpound.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_numstar.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_return.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_search.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_shift.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_shift_locked.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_space.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_tab.png create mode 100644 java/res/drawable-mdpi/sym_bkeyboard_tabprev.png rename java/res/{drawable => drawable-mdpi}/voice_ime_background.9.png (100%) rename java/res/{drawable => drawable-mdpi}/voice_swipe_hint.png (100%) rename java/res/{drawable => drawable-mdpi}/working.png (100%) create mode 100644 java/res/drawable/btn_close.xml create mode 100644 java/res/drawable/btn_keyboard_key2.xml create mode 100644 java/res/drawable/btn_keyboard_key3.xml create mode 100644 java/res/drawable/btn_keyboard_key_stone.xml create mode 100644 java/res/drawable/keyboard_key_feedback.xml rename java/res/layout/{input.xml => input_basic.xml} (83%) create mode 100755 java/res/layout/input_basic_highcontrast.xml create mode 100755 java/res/layout/input_stone_bold.xml create mode 100755 java/res/layout/input_stone_normal.xml create mode 100755 java/res/layout/input_stone_popup.xml create mode 100644 java/res/layout/keyboard_key_preview.xml create mode 100644 java/res/layout/keyboard_popup_keyboard.xml create mode 100644 java/res/values-rm/strings.xml create mode 100644 java/res/values-sr/strings.xml create mode 100644 java/res/values/attrs.xml create mode 100644 java/res/values/styles.xml create mode 100644 java/res/xml-da/kbd_qwerty.xml create mode 100644 java/res/xml-da/kbd_qwerty_black.xml create mode 100755 java/res/xml-de/kbd_qwerty_black.xml create mode 100644 java/res/xml-fr/kbd_qwerty_black.xml create mode 100755 java/res/xml-iw/kbd_qwerty.xml create mode 100755 java/res/xml-iw/kbd_qwerty_black.xml create mode 100644 java/res/xml-nb/kbd_qwerty.xml create mode 100644 java/res/xml-nb/kbd_qwerty_black.xml create mode 100755 java/res/xml-ru/kbd_qwerty_black.xml create mode 100644 java/res/xml-sr/kbd_qwerty.xml create mode 100644 java/res/xml-sr/kbd_qwerty_black.xml create mode 100644 java/res/xml-sv/kbd_qwerty_black.xml create mode 100644 java/res/xml/dictionary.xml create mode 100644 java/res/xml/kbd_alpha_black.xml create mode 100755 java/res/xml/kbd_phone_black.xml create mode 100755 java/res/xml/kbd_phone_symbols_black.xml create mode 100755 java/res/xml/kbd_qwerty_black.xml create mode 100755 java/res/xml/kbd_symbols_black.xml create mode 100755 java/res/xml/kbd_symbols_shift_black.xml rename java/src/com/android/inputmethod/{voice => latin}/EditingUtil.java (59%) create mode 100644 java/src/com/android/inputmethod/latin/LatinIMEUtil.java create mode 100644 java/src/com/android/inputmethod/latin/LatinImeLogger.java create mode 100644 java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java create mode 100644 java/src/com/android/inputmethod/latin/UserBigramDictionary.java delete mode 100644 java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java delete mode 100644 java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java delete mode 100644 java/src/com/google/android/voicesearch/LatinIMEWithVoice.java delete mode 100644 java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java create mode 100644 native/src/char_utils.cpp create mode 100644 native/src/char_utils.h create mode 100644 tests/Android.mk create mode 100644 tests/AndroidManifest.xml create mode 100644 tests/data/bigramlist.xml create mode 100644 tests/data/wordlist.xml create mode 100644 tests/res/raw/test.dict create mode 100644 tests/res/raw/testtext.txt create mode 100644 tests/src/com/android/inputmethod/latin/ImeLoggerTests.java create mode 100644 tests/src/com/android/inputmethod/latin/SuggestHelper.java create mode 100644 tests/src/com/android/inputmethod/latin/SuggestPerformanceTests.java create mode 100644 tests/src/com/android/inputmethod/latin/SuggestTests.java create mode 100644 tests/src/com/android/inputmethod/latin/UserBigramTests.java diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index e229bc76a..642c717d3 100755 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -1,8 +1,6 @@ - - diff --git a/java/proguard.flags b/java/proguard.flags index 0a5d2dda9..829a096c0 100644 --- a/java/proguard.flags +++ b/java/proguard.flags @@ -1,3 +1,8 @@ -keep class com.android.inputmethod.latin.BinaryDictionary { int mDictLength; + (...); +} + +-keep class com.android.inputmethod.latin.Suggest { + (...); } diff --git a/java/res/drawable-en-hdpi/sym_keyboard_feedback_delete.png b/java/res/drawable-en-hdpi/sym_keyboard_feedback_delete.png deleted file mode 100755 index ca7637552b5e256f445fc8dd497811ab7518f489..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1278 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/btn_close_selected.png b/java/res/drawable-hdpi/btn_close_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..870c670f72364b0bb8a013590264b677c5218e34 GIT binary patch literal 2807 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 SOLA@i0000) z&lwIQM*QzT63LU#lh2dSWA^#cV@~)pdGh3UCrz4EmmrTRbwx!*?UvXlQ7V#>PhX=anm0-0yhI#fum7sIRZj1MQQ# zx;h63NW;O?VDOMJ$@)~nPrOW61Oc0xnsPKZH#=HdTIA~0t8(qyHMxHMy8E-WwblI& z;O|55e8P4yARH794B^08Fc4m3biy-UCM?3R3=G=Z+T_NK8*=mJO=)j$myV8(9QYaB zx^*iE&e5P9fHv7a2SpD|Xn-&PUgYNqW4uh>)oCOSLMIHtkOtT?3!q`kPs?1r>z++qGv-35M=Wr zzfYKu@N%jMFR{E@UBB5NN51%ANRGEnk6!Fn!mbf5z$^gSqDAs9znwRv?*3Fc{pBAs zY3SY%3>&&y=|vtYD|<;M$QCVv?+0GT*+APrYoz|0V!80mUxGA#`^XV6C)2sFKbH%cbYv)iUty8R_ZkO#Nmb>Kk8w(`yNa zC&+|s(IQ#*TJ0Te(tWQ{`tR3E-;?w1MT=mohr1g9qhN(XYbdl~ zk+$)mv1>pJ=$#gTY|$dvr_x(67zHcDT@B7q8g{27gx)o=0(M&n*`h_TafTDHhs2G# zG$Fh1RVaE-EF{2?9yui^u>Oe1DTL5T4eX`-LilGerFHR0L9QSXGoWy{pW#LblGqQ7@qwy z2Quy1vnO6;=gyt7ckf-U`*t@g03!e>W`%U= zg{rdG%U0v<$H*enTKqHi+kS;%6g0bjx*&feZ{`MOLg>;mp^qTbF}kT8L7FWl#t_SC2l!qfeaynUF17WZAN1&V0?9H961- z<*by2p|Y^&=+UPD`qLSZ3E84WmMmE!D_5>`B}UK)<+PA3K??(FLdWURrvUoX8ITFt zqD2-hS|rPtFLz)n3FY)`>6scXY{P~}f4YCy?V(36i>Dd|% zJl}>#f4YBUs;sPx7g?}if&2G!Oru82St(nJg%T1#f4YBUs;H=l7nwJ2o+~d*qXrF8 z&`O0$u@HE^7GQtuADQsxQnUzOB|>>&8Z~H$f_lc#vBs8U3_O;zY{J z%UyY48VMR9R!YxSs3g#kX@sP;gl-Fcpjx6urcImXrZ5YLT4Kc0 z211ML;6NX*M5cI=)R$^{i;Ih;q@*Mjh^B$r%wooj8F`?T0Yaj52!7V!n5k2z%EXBi z?Cgkk@=Ujq?(CG4rQsFZAnKdDg#<`n;c= x7kvXSKLTFzjP>O-#!Ec?|N7qzkL155{Rf;PP9H9-2x0&L002ovPDHLkV1nE#j2{31 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/btn_keyboard_key_normal_stone.9.png b/java/res/drawable-hdpi/btn_keyboard_key_normal_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fba10b8882d6ecb43458ed5535e20d77f65197c5 GIT binary patch literal 2211 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/btn_keyboard_toggle_off.png b/java/res/drawable-hdpi/btn_keyboard_toggle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe78402f4b99c6f1f39e702341f2204b765893d GIT binary patch literal 1425 zcmV;C1#bF@P)tFvr zYmc`v|Ksre8`kXXY^z_CJp@lRH#dLJ^<;g0eN#9buB)xB4FMeV`~5x`h4G2N zs{88dYV7jz^1N6qZk(T=uMoFJo+A<$s*`)n#7IE#c>EiZziMo3e2t~J?gl_b`KjE% z)S9l{J#z(?w}2fN3Wc`-E_1z}%jMqo_V!*nh8{CXM@2nNO--+&(dZBD?d=l{3>ay1 z3=GWm^|c|X2ZE%guCC6&*hf7fR2bmCmBOMFKgF&jbwe@Eh zeTzc^HvUqnWXk2T1(K4^1SpfF8Y)JNHF)laVJ&lz^%|bl zBsAmA1fmuYViaT2IVXnxVDL1ayjezF-y0uSte!z(zWuDyPd6Q^uTD4fKCT= zw(TW$+9L(Z)C3B4aw$;50jNOd>%g=Qm`{0$vt94H@eZIe7FJ>x>_7#g=Q0i53rIu0 zGWp5`rhECor~+UaU;AEIFk%WM%3={50)W~;2co3aZT}d|u~7#LQ1PTu%q^iG$$Oz3 z77XM()f{rKcPvczm`o<)RVWq?Oh9hvxmXBHKx%tES({{$vB0ums3Oh-qDq2(*0x1u8|oea2U4k& z+1c4K{O#CdNFa`#0yP9AR+_cWA#8?*h79qN^Mmr?wuM(Za zhI(fGTfZD09&TX&%--IfIXE~l$H&K(B?&AON!HcXW%~R3&EVjm;SiUUoX4f#kZ_q} zYn9@Pt~l5FW@TliJU%{tM1|`F=+Cf(q$xV98;JsRs80#CH%>Oz4Trc`Ran2n?*r?9 zpu#eJu+Fh{w7I!ic3_^2?!nU1lGyZMQ*81y-8BiYj+-!ygUm9;NzYpVU!Fb6VHqUM&?H~| fZhQjpUq1K?E0K1EnweR?00000NkvXXu0mjf!pNDb literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/btn_keyboard_toggle_on.png b/java/res/drawable-hdpi/btn_keyboard_toggle_on.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1221e97969ccd7d2c7294151920451e1c1ce72 GIT binary patch literal 1733 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/dialog_top_dark_bottom_medium.png b/java/res/drawable-hdpi/dialog_top_dark_bottom_medium.png deleted file mode 100755 index 7c79a4f9083f0ce4496ab2ec0ed99b8965df50aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1976 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 P00000NkvXXu0mjf%Rwia literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/keyboard_key_feedback_background.9.png b/java/res/drawable-hdpi/keyboard_key_feedback_background.9.png new file mode 100644 index 0000000000000000000000000000000000000000..6ba42db8237727d5d67b629bd60384fc014df3ab GIT binary patch literal 1372 zcmV-i1*7_jP);@F8{2|+b#-mc(%F;!iKC^Zi6gDRq zd1I+mdc3!{cUO0J_nyAKzP;sg`5C;Yr)Pf_&Rzm&UweD|(;XchyY;+Nacu`QfXWJ_ z6{c`H@@>Up@ks(xP%kKumlW750_au!d|E%B(a*1^?d*ehc6Pp|^`zG0S`RCZ=Q%8a zw*yuIQknqWXA2v`5Cw8j0iM$OuGWtg)K#sYX)Wq^B|VqZcJ?uE)4r`*FX{K^^!%p4 zIR;3u8ivPMl&o28DUd=?nG*sO`KJQv%Sxs4`|$Aaom#u%xH&jD_=`Xpumd}z=NH@B z+Ma^#X)vgPRDv-8L;47m3wj=r-5MPojdJMs6!XuD^_swW%UHe}29W`YA!E1EHcKTK zb3s)7iU4v7%zYP|{!+ZxNi%t~73)G$?4i~a_)yR%mGnHk7MSVj>1cj_K3ZH{tf|k= z&PL$9}dIQqjQ1UA(aJR!|b8~Z1-Ys5#NG+?G&2-RJ_dL*#TZ+z?s!uNw zHsfUSZm>JVtM4(b?{Jp7L#apv9D7CJm7rM9;HU_HukUc4*9$#Au>m+6|KW|7H+!+; z{iRgT;Pm(R|DZ$uQ03tW;B0W6KO8u8#r7guc5og5j=cq!>%8Xt7{IYk*}-ve92^J7 z!EtaL90$k2ac~@*{{>Fu;5ay*Rt}DXRn|4IEX!i%A#s;Y>r`{Da3EoIXK@U;MQRuLQh?_J_ltNGX;nx(e%c_Sy))eJveb) zpxE*aqDjUP5)m(O()!^G{(j^mYW0S3{E=VjUS7xyx(>vl=EF-dZ&{P){^$QjR{yb)^0}VjLaXZ z=6N_oH|yUNVHBh`q{cK~Q_^EaVGym6e6`6uI_6Kb`S{S#&>!(Slmk$1i`g?a7{|QB zaT}}LL9Iz+Ag#gDD*$wWIRdS3bH&@F+!(!tnyv5XMseO}((5b{?4c3GOa^n4naceDBZKrrR{Sz?*kPkW%>croQ6ITP!tiUFj9GV5*ivm}gqjnA zz5qt!i1Vj`v^?yAvnOEC`2vU?Ud?99{{vP4EDYjjyQ`# literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/keyboard_key_feedback_more_background.9.png b/java/res/drawable-hdpi/keyboard_key_feedback_more_background.9.png new file mode 100644 index 0000000000000000000000000000000000000000..4d0b60109987c866268d42f30d8148829a1af76a GIT binary patch literal 1637 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/keyboard_popup_panel_trans_background.9.png b/java/res/drawable-hdpi/keyboard_popup_panel_trans_background.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fd7366e204321ee317e1a55342d23b95f0a09202 GIT binary patch literal 1677 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_mic.png b/java/res/drawable-hdpi/sym_bkeyboard_mic.png new file mode 100644 index 0000000000000000000000000000000000000000..512f46080022eb99c3af40d970362a783264a78b GIT binary patch literal 1410 zcmV-|1%3L7P)zyT;`=CMM^ z4tRlLKH2E#=q;Iu3{G-oW#tw0y9VR~Zdy=U32A^2CVvuTqkWGqRne^2oS6BBcDTU00mX4E86iS8<16OKmYv1GD z{C@vWJcj{2z|CZNjH9%)^jGxj2VMkPL_JC+P(&&~GtO^MktQ5DIXMR>OGP0IX=Zed z%jJsFSI6w^tY2;-5f?X|C`m;638}KOGA^_-*4Njs(!)5hSY>79FUbfsHa7lD&(Y@R z=dbWORaGNWZZj>=7zwce@Ho&c%-!4D`x$=s@mI01uyD)ibnYd2T{nT(f!<&+_yeym zE-t=}-%kN$BxN+S@*^gT_44xa?|9|-_;`!hnGFpM8*_7WAJo^^FN;Ch+1cA&U0vU- zuC9JWW0kS7vBIULr6+iMb93|8(sPgE?J}0&I>t^~hU;ekV`8)7skl zDG9IZC@*6&7m*VEc4-1mChf4iI^#t*z}Vhr_We`u6nngwXaha23di;qzn+dw`6Tuo$xa}tP62|Hi%2~}E9G0alW*yX ziHQ&_^am$`9rq&-2dIAyXak<4EuKeYoJ5A5_SB)%E;t~^jgVtT#1Xl(vvb4kcArFL z5ZgvL_>&xVj~sYMa`?kk7Z7G-gwQWXL~LKm*ztBs%+Ob4_L!O55VN{Qmp=74B;|4_ z5!K_2P0QI{A!p=%P*7c6{cfsk&(YV{*G}8qLt%ua^W-Y6A@yxL#KBHMLBZcvZ`7E{ zn|x+af0qC+i7gk^+vs&gCFkEJr{0r%E^YTi@{Wtp@2Dhkcwa%>V!Z literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_num0.png b/java/res/drawable-hdpi/sym_bkeyboard_num0.png new file mode 100644 index 0000000000000000000000000000000000000000..678a790ded5d6dfccdb79fa5470e371ce893f498 GIT binary patch literal 1903 zcmV-#2ax!QP)DwKc{QIIQAWznQm$s(U!y9iQIBV|o@sVY@!7m1o~vO(&i3fn-Xs8uMX zqC|C7goz@4B?$%s{u|r9*SbR=4`T)+rA+d!bTp&!edC$)?z^9NM@pqqdxZQyA_?*Z z`GR~Qf_y=~5JA2mUx*-IkT2xLlP__Q1qUaY^XxZ~q!;>fyWOz1B40z$0`veKK_b#u z32{Jzt|bNeJ0o8r(vfH*zznd^F^?t#$&LX+fFK>CG#SZED?yOIHTg2}XzBR?J76!M z==SZ~N5;m+4$jTZ6_%8gEV|w9$DN&>f5UHn!gHF47)_2Kf2#-NpaGZw`v6A(ZC9>b z`OapuJtu2khoYjQ`Kwp2egk^%0S*I-0YJC;OEUFXP;~b3eei z7C;3cpSS)T$d`z0TuC;7oq*41px>mIh5S5n8*ksabLVr7Mw8?THk-{cr_(u~pPwJ) zf75EUs{;cApTPKffP?P06Y{m>0{7B{)&gn(<+Qpox;HhFj}~O6mppa!=+Pmb+=_~d zd7sbs1^oWmUjmF5#%uFW*UmI(gIgpR> zXFsmQ+S=Ot@_!sVb`0IyTeM2^)JndFE2uENG~c>)t3DhKn^=;Hi;I^oUApuuoK6BB z1ExAUI{qjtE1PHaB9VvzTvk2R1O~cqt|C8^1t!%XUj)}@qPj!JRoto)4VT}ii>lGl z(Q00?va)gp4vT;VCa+&`xm=HV-I0-zS~!@fAX7>{TiEGo;!HG&);thd(Evg2eS>a9rDqN3#h6_;;lsT0WkbE^1%h6UKx$E(Ig<20Lw&rOih+AFMDk~9@q1V zcDp^mZJwpi3knK?ye^DqiI)_TpSE&xmn+e0*&+4i67I+uPeO#$vH3 z3LIk)Yoce)ocX?|r{`zJ`c-53YZCy$)|e?VVUF^760)KQuSnxH+YGkp;0h_M%j@;F zXH$Ii=#d@4UvFq=_z4a}H1WIM?`w8?18@f^1y-$it0|dG=4`DncGvs)Y?d$Q>KpuJ zB9YK=jg|EI^78VARRRHVmCOH?2)eOIQ&ZD#z~R>xi^YrrE6WV$*1LD_y2r-Gicqn! zvGJdlmX<$Zg#|(+fj}Sx`Ts*^TRWvD^5w2n0<3uh`70|c7S4~S;{y<2;B|4Efr$@F zNJx^84qm%6cd9f2JeG|gM;NV`Bha_(>HG1=!4T4N_hj6@hsDpw(7{2gJ`_4 zuyB!A1Piw^8H}73>imAcJwtF_%GOlLV)OmQNcG)I;#|D zGxieHKz{lKprD%O6=!E>_fh_>XNnpl)C48GZfR-hQzk72mi)^yfs+xeCk_#2L+wJ2Or>3ULm>hf!KA*3G*R8Iuo+RHJS4jRkx3}lN zzHp3)4YK58YDw-T!u-#6UEQuCKPmTQFI>3r5Q649zYiAfxOeZ~8(8pQ!HY!>ZX-a- zSH|BI_k73|#I`~HX0aemH0JU0MToCqHTia>0T*8;1XZiQzP^5RXlUpiR{8q%>t|bA zTm3|=sjI826EYKt+lH8KYisinIXf+MQe@qW$B!THbvm8D&--{bN8<#M^6X7aP$-QC#FeHTzd-6lyiQ(izD5iD~Y#(9XO0>059+K9>< z=&uKZ!4D1{I^i{`_EbbMpl5BvYehWwR1L(??UU@`t)xeJ+?sd+Zk4TW4E?DI!_^TrfaIkz8Sl z&joWNe?xnx3-W~s@&)-q6v)2<3;-<9{-f0=)gu4^002ovPDHLkV1oFfkDvem literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_num1.png b/java/res/drawable-hdpi/sym_bkeyboard_num1.png new file mode 100644 index 0000000000000000000000000000000000000000..4e68e35b353a9279a835b5ba6d2d3f32f9e08077 GIT binary patch literal 792 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 bj{pMz=XRrFDd<8s00000NkvXXu0mjfw6-Mm literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_num3.png b/java/res/drawable-hdpi/sym_bkeyboard_num3.png new file mode 100644 index 0000000000000000000000000000000000000000..57f9a8d8ebe93657ad2c8c514d8ceffeae6dde1f GIT binary patch literal 2829 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%0AQQr(ouI!PFtX(#F2uwg@;dNEK`rPS9?B=~HA|S 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_num7.png b/java/res/drawable-hdpi/sym_bkeyboard_num7.png new file mode 100644 index 0000000000000000000000000000000000000000..fff6f27bfb23d3767ef3323209256a280c64ae00 GIT binary patch literal 3687 zcmV-t4w&(YP)n3!4VP&IcIXrOfs|I8(*`{I7uc1aBKJPs;-&L-+#aEuU~)fd;R}W+iW(KQhuGH zQ6NyjSHM?_0=@#iQWWqN@Rg!~uYj+V2Um1Xep5@I>Q3}+Cr)YBA`vEji?6H25CW*(gyhCd{vMY7bOTVMoFFKUL=>*y`Nf-SwT8Q7BAZZk-I=?FOy~RUWk%a7HIK ztJV6TD8l7n&7i8Kpf4*fc~@B-l$6{~<4{%HUasQaU)bU?INDM<5epjWetC{INnAJ^ zXPe-QU|HL>Yu6%Bha31_%U|7Aa;4TXc`7bgxQMcukV6R+XO4 zK_XJ)`uO({4$@^pvCGQHN?@O019R+1`Pe0wecQ1eAjWdEYU*)ba01e=u&^+8 z@#4iRu3ftp3CGj=`1laWDoAWC?zII41(|m5-kpq}Zu1zMT=9IH1HU<=2C>E6moHzQ z=m+%+9f3=B|YH`w-XW)4#mdC7PYjrXbTDoqO-HJhoeom9Xoa`3=Ivf z!4AshIr>n{BG8hOk}iWji%4t~7Z*pOeKhR$fSpBHgn77rk)6nY>(;G*z_lnE<1^1Z zlQm$#fb+1`oST~)b>+&H-yqlVykE7`9zY)K`GGrk?o4rP@buG9AH!g8G3U8TIbvbS z!`)~b>-G8;(8V=u{R=#4k1xLXqImoE?Gta@xY2k2{`~_}Qc?&+?Z+Q~JR4y3K>IRy zi64oJ7L+r=p^wux;D6{Sn|V<37oW)Lvd*WUFMsf(3h-Dz%xJnSIx- zTlWfV`S07eF9qOVWt#K3bm`Kwpf%0Nkt0v7T)A>5dpOaO!GU zfJ3Y5_x0Cb55nq4IB0ttDB&N>$Mw8TtsS&+mzOLSOCxXh3hHy_%$dh}^ypDRGz38V zkQV}o7JFJ_gn^mzk=Vdqn2>Jj$QB*T_Y;GdGqGuXU?37X5h6_W^U%)N;FwB zY}l}3*t!l|8r%~nDJgf1OONc|GGfFCu^SA0U-|Ii!!LaK<(G5#H&a{l?PIMEFPZ`0?ZSgZD+us%5}Utt4n224BM~Ox_l-V#SJd zY`uH##em9KUe@v;@Anw_uDSwXWG{sJgL;So)lGosx+8%rglaiQAVF<-0s!LeTJ~6r zLjra;Iiohz)?_ky3neGO=OdMnqeqYS1+W3&`QKuZe0%rqUH;;WFaC>Tn4QNV1h!+* zpP+bv*fShEDG@YOg_O(|cDc?K z*M}YVk3RZn+NYm>YL{`8OOPswiHW%h$@m#BvKO4do(CY_w6x)AHjYUx=$di{jTB$h z)bDrSeV6>>k3S|*JfUEUj9mvMbL!1E-`ooBFXMUH6Ep-%O!JsHv&x zf?ytsU6q13m^fDj~6@Rz?4q!tN zN36(V%NRtmWhJQ@A0MCl&O7g9B1?9A6$J{)Z{x;|X;_p3Bxs)8hlpb)-gyM7;e{7o zFrwyzvd+Q@=fFALKL7mlr)JHX^*5$#jWbL%E_yu~_g}qw^-u(%(0ih=R@(x3xw{kc zQsv}`!|o;EW&bfd7Mq`5XPZHhhD|5yh_vsI*M>| zG}B@5-9=LM)mLBr&auJ3fdel=UKTzed1*h~V~MH4Bn&JU%NX=Z=}?WTF)c0a94M)r z0x=S(v2^A_@t}PIBF9T5^@8ad_nLxc|O?!B{i`uy#}p*;`!&F-$q9T`T6;M z7cN}*3I~4bI~c-ZG4hNU{yR`){7^`qaDre`V-O%E?VadqTurLJ|Ni@a=g*&ydr0z9 zKH4xt$G7lViIKfrP>o4aGxXhviBanRSRbS*3_ ztYP%%(c3^hLKxJGg=WvM5{o@&&YTQT(NVf~?AWo9$f8r&^8;_)x-|~;vI?1O8vdrU zBw_>KEnBuElR|=|HT3V_KcAhk*_9*Zw$5<^R;^l`g zAlsdf&Scp0P3%BIr(4&pTeo`3lqucm90y8h0+d!kYHDf`J>JO8&Fu%N8I_TdVS%l3 zsIdTez7NF|WZC;cK?U5Or8P`6F7Uk`#)A$WI+WnpV9b~?2RN-ZFmRpW`W6Pd7OEyA zE-r2}04rdCoq3PQCJC(aB}(fNIHYB;~;+%9=-tA&voh2r37MwXsv1F$dRiD4<3AuPFX42#v=4Y zqNfL?)gdQXwS4*V4H8AR3ck9HXQJYPNmzP%`dfSU?3pQF#9$1NRWlHn@|cqiascb- zb5e{$E1jR{*)c4fur;+Z2`#G$FCHtMu=-*bgyiJp#QpHY56N(vBtJht%cf16R)Tge z;d&uci<#|t@?6afypYOb*Zc5z)RGwR;A@`1z&|rHGa@M|sW37!QaEC@#0Hl8NwVz8 z3%0Bc6G(Nu2tGUr;IFe0QzyM%B|WVY^rPn8(AH}w;&-F;#Htn^NVPgSIk^xDCxBwr z(xpqs!t;xvMuaW1w743{mYgAr*dksavr4$p0=Pkn-bBrlc5VarX=*RuY0vZZyz*T+ zs&`>f8ksjMIUqLjpjDmleEC$<*@JUiJC34}c7mRRV=#hm)O+u}w-g@QGc+``dhXo0 zQ%8&#k;~L#X+`E*r7_44$kh&FM1I(0b#NAIdk(2_Apq7h|Iv9i9oH7{*1gHVclz|w z!TX&}!sfz#;^B3J#M!0r^t`vYqs}7 zz^^$rfUkhB6a{<*e5EMhE8r{TA(ej%FaY~#8--pWQM3R6002ovPDHLk FV1h7%Dslh- literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_num8.png b/java/res/drawable-hdpi/sym_bkeyboard_num8.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc1a955ee5b2d91039f5af5292acbeb7971e45f GIT binary patch literal 2952 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 Rg0cVr002ovPDHLkV1h%v+3)}W literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_numpound.png b/java/res/drawable-hdpi/sym_bkeyboard_numpound.png new file mode 100644 index 0000000000000000000000000000000000000000..0a46122b2ba72f4b100efdd11c8add1eb8bc4de0 GIT binary patch literal 1577 zcmV+^2G;qBP);?5JgeJ+yB#)oB~chC!aS? zJ|~|yPCh4}H%>k$pEpkaThY{M@;LLN*mLUx@7R08|ZBPai!O$E>ZbO-@S zjRIoWM~nAVNcIhav^XdK{!vc|H3BjLW;~AoE&*ZelL5#A=#lIzyq}P*6t4OAAfH5Z z=Gxj?nNq1-5d>kmu&{6v$u=PIxwW;m&*9z+z%RqY!!0;r%m$b`O(cg%N~Xu-X%a(b3Tpye@FJ^RUP#u)DkKq!;`8`u34j@;~UOr>9-`^zggQU zsf2VBzP8WK&b~)GHa0e%6T)S(nR{?>AnCnMr;BxTbP$4VNH`hy9IaoTV6US(0%4V`ezEW1hp&s9aw$pfYbo7LD zvREuvb#-;uvRuHhyu9q7gVfj8ALBR_f-K8ruCC7#`8p)qg1Vmr$lu!9a$R0tO0rT` zRu)8mpTvl^?C?xJv*x1;I(P;Gj@LwiBY`5EA%+1YBVzL!ujeJ%- zz7~)Nr~${a_gMbk;c)x_W4ZuV%p+LJYnbx+(s#0t>G5GBW>V`}D=CX;Ghc(g_XGbV zBYg&NXJ=;<=c9nEHwfon1HJ%!4j3C58TpY8;C8#eg=<5Ar}+L4U_bbhHE++N<|j~> zukf5~mincorFS4{GGHZB?%zq!#1|G8?)HPWwzd^G_y8xk$wtw#MsQ-2ocw?G_mm*A z`C5pFryVLPDz@O@$8422Bje`gCdq&mX0tie+}up=A7XYJlKJ`stJ1>~`IICj%0$fT z^%l#@$iJd>d`%|F=kuu`vy~>NsHkWc?@?x1|)cn-XBz^a~v$InqMTlA*8yFZ^ zM+uSidunQ`oPL>JQc|*m+I}M|ODH(`52*9_{r*yNVzpZLa&mG47}3(In4h1&%WPdP z*9z_zHwuQovt_-ub4R?#;~|BzIf#07d3pIV)@*2|dMyaTk<;n?p1h&IzkeR(CauU9 z82l+HsoUtW)L_B@xkXu_T23}xGaf~}9+IsLkw^Z+Y$tu*l0KIiQ~dp0COucnoYF1J z301m~FSngHDRLyrUdL0j<8&gPW%9*;q1_2Znm7LA2u?mHpEpiEC!aS?J|~|yPX621 b{uE#US~VAG)0yuL00000NkvXXu0mjf=4k^% literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_numstar.png b/java/res/drawable-hdpi/sym_bkeyboard_numstar.png new file mode 100644 index 0000000000000000000000000000000000000000..ca22bd53509a2feddcc2a22fa872c14f249e7174 GIT binary patch literal 1742 zcmV;<1~K`GP)347+aJLCM)6}3N0;kzeCQ=ZU{OdS^B2;B)`y}-gjw#@7#0G`Ms+}qfv{p z*qBvOpvYI`D^uhv@|7v_75T~(`HFmHiu^~y;!XO!Gzr3@5*8XSUl*NsjUs;$#MAeR z19XYZFfao|<`Js#3Krw(@2oU-hTkjlA2JmZiB7=5uZhG-MwH*nGm6g@n-rl|r@0at< zGw;4elCp`Q0S1uP*x0z+;c)orE2gKX55Tt#cotX==sY$aNCdKg%_SuzU($5 z6}9j^4LAX-$$sY?%o-J1RaMm}NSsSABO@bg4jnr58b|*!z{U};^J+-x=;$cy?d^Sq z#z!I%yU*vlZTf1?gb_hey%H#BXlSSx`P$`j-MxJI@;>qdselXc06D?Z7j@6ToU<3s%V&VoY*!vVU-J z@Gbb)0xP5xrO01I-lyn(?!tu&hix`nh=|oRZKkcQtq%Sdcm_F0x#~`wd-3!ZbnANZ}3$CyMevy*RTIcd{$6U&<9@`uZ$!! zN4;6dw=&s8lnthAsIsrG z?@7848|(C~TeoPO1*vDazrVi&FNc7?fO{-QSh(AzJtF{6`FV#s-|1M(4zZROG$z`O zN~R_d2&^^?!xjg_<~kRd^cowk4SqXZy0}mfKldYGWo2bAg+ig=G;9VYKAVN1?vDh6 z!BAmg;WyZ@&+&~3KknL&h;BQ|!_A|kqrRm~a_Q2gmApD0XsM~GKjTjqr@NS^9W!)* zl=o&3p#ORPqgBAE#uOr3ya|Y>+&fuYTYDVbq@j~_>bh>z*znpwCF4#=NbsCLe|~Li zYisWON%rj7Ggw(!*^jj4pPHJw8xDskU_E z@DAQsoxB0ZOZx$B!s$0}-dvNLoBN*M?{{(v3BpHJlEvwChEQgM21Cabu!v-1NKJ>m&gw7I#t9sc*v zoH_HU>_^k}u_P_Hq^2rPnqN~L?t^cJ&bqyi6icL&Uj)G@7fC{R6DS8>FE20uUVN68 zmiEWwu<*KgXeVZ#qny6GW(pB2mHESN20O@G^t9Xq}gV^J0La_)ahJxo~~ z`IK@p8X6k*k$z8PBKpU!UcLIZ^c2hBbYkNbuojij?v#|28H4`=k4Wcos7xL6cs$pS9Xr-!wOS*FVT=cZ!D-W!l>f8S z3zVy~k>e}!^77g^e`j!-kt-Pa1Oug*+^t);9zT8h^iEDkD|kLrrkz-Kxc3v<4DNdE z+z5uaITD@e>;l(_@j9oT5Rac;;FOy>v35U^jfoSd)HEr@%r2n8i)*I%x;XZWV}dgl zlb+W!<8i@RiBJ*Q|90F@Rw^~=rdTZMpzM-XKy(S$n3`-e>iLhYDe@KhihN~?d_}%8 kMZO|mnIiwuv%dux0N#8ijHTuha{vGU07*qoM6N<$f~G=Oa{vGU literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_return.png b/java/res/drawable-hdpi/sym_bkeyboard_return.png new file mode 100644 index 0000000000000000000000000000000000000000..426e1599ee26dcb27c8129fbea70f6df334d0746 GIT binary patch literal 1111 zcmV-d1gQIoP)b`DT0 zwl7tM+I?**l{%W+u)4ZxLG?UXHXDOe;tgJd22X6@4>5R9*FvtAjAd0IxebX#LWlFK z%R&rEY0x%~purPk|4xh@+LHy6%V)T1k#>3FSQaUwI;O6!t_Lj3ey~_9Pa}~?6MW-n zHX^%7bRG;q3pfe5Y_r*X*~UIPI_iS&O#rrl_3Hqa3GJa>Z>o)z{Z|cV=ehnut1)HCWL5G{xK`KItJ|odCykY==8k ztJ4@pHsvJ7lOpci;B-3g1cSlBd;@Ag;7vC23yGBUAOq;xu6G#m~)FlMrH5jzAh zx3sh@?G>^9{{9!RhqC}ZCI(fP-R}^q)w(b_Ir)s3Es6UyE}xbdG1h2%dwZaqgftpW za%gDC6AFdy!Ot)Ff=*Knu0-cyG#WpF>b(NbJBaGnfOgK%=CKUrE0}lpb?EMI`+U9| zVuOZTo#Tikd44w-@6t}_rk^)xuvX002ovPDHLkV1lx1|FQr8 literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_search.png b/java/res/drawable-hdpi/sym_bkeyboard_search.png new file mode 100644 index 0000000000000000000000000000000000000000..1b6f884fa39a191cf08aca04df2bae8d77bb08de GIT binary patch literal 1612 zcmV-S2DABzP)-s{8k1Z^_wzkDrk(NfpYK4LdS}arx^-rvIRiveCi?;itR4i0WUw`~pQ0VH? zVuhkuUnpz+;i|>@enj2$oN@+kheVCZCRxeAhvPkW&fM=l&iU@0a5kGwauF}7f#S?QtN+5driS$RL36e*`!H>@heZ!Zu$dhNKh# z6+jCJ85|rm^z`&xpPikJvs$fKE>S2HyGEmNy1cynZgzI|eJEN3tOIrd)<;4PCDl10 z1*ia#fV8%@ws#E%!xS~rA!1@;X0R?kL(i(nm&BcLg_If)J3l}FVp3Alucr|b1E0ZX z(FggGdYc$Wy&O9*Fz{+*WaNEEjhD;ihX_Y@d=`BWITiW#8ZiYRVrgk9J32agUfftH z`>U?5?vt6Bnb)A+^9a1hGPK#LKaal9C;9dYF`O<>08%nDGk<0q$n*SBUS8f0+uPeO zfc+{U8W2vv`>SZn&(Hr6>$1kd-|iYUXV+FRAQ`TEY>%7G=3XKdN)6-eL|=|pj%62mESX=`iirc$Zg5gMe^>DE?OR&v1><%pCWa?$WiUT|>mmf#0{g6|vn z4tGpPXS5#!n=5?lVGt}K4g8MPbhzm z-obe~L8DN87G%*kY@;A_ua1q4y$&`bKqIIQQU1a*E!G(t8YQv=8(NX0 z9=%dqTiYg9>Zl}Om;5EF3aFC>NF`*guCC^nmX?0Sstp14_4VIDotFUykYf;E8}hf)i^=A{pD#((;Z> zCbK#s7Z@103+wTHsGCH}L~$B9w-a+x&hfN<;u{+qEhmvfTUAxn$0Pvs)L}kLOqjwr z#gZBt8gAJOJnW&5*=)Xz!HVRaZxR!c!%;}Csj2zsByu=%i;IgHv|fFhm_SZW>nEkM zvhuc6Dt&knh>6LiLVD=5N3N)-=nmMQc^syqy`+y?F%|OL7c8m5!osgfREL}vJX}rC zcnPs~ruPmH<#6<`3zwR7fLeJ+#$0000< KMNUMnLSTZ*I|x|- literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_shift.png b/java/res/drawable-hdpi/sym_bkeyboard_shift.png new file mode 100644 index 0000000000000000000000000000000000000000..5a22dd30970f4bd04822e15ad7721ac16df176d5 GIT binary patch literal 1474 zcmV;z1wHzSP)TYtC9r#%8xifRW@6MSyXXdUV7!1ao#-fZl zr6npXDl952D(px_{Q_SgK><*F3GnZU7Dj^fKrEmIynq|niln$>yP`s&2nik`5@j10 z7`S4$+kcpunQ`FS0_gaDWC~>-$H4*pDgZt3>fGGi?SzDcIWniEr9FbJ07wFKTrVcv zh=?#It7Gy7nVFe?u-omRu&}Tn_N%~oKpP2hVHd_EwM_adi6FCDt@c(`RsAN2tE{a2 z9``Q;RzMwYabXq4|GLaXAG&D4{!}d-|Nyz|RV-5!D>+8RQ?Gj+-cCrQ>jg5`n=U2V*k1`p7zl?bB9XkfxOj`m5lNLwd#9T3EY7jrE;JNtK`kD8jAZ^SR=MF|t$Ha#?_wzl?L zfe_D=8{%~7f6}?Vw2M~Cq@<(;!IfKDT0Uo2HXS7l97PyCqAXSu- zAClhL+4-?fr`zNe&@(hN^d0OU0JLo10&eg^kii19q7gdJ!J6XRO|DPv?hOtOK0x61 zQNQu=@k^tlqi@5WDy*!L5Vn`t4i69CHJi;(#ffbw;LSfpo7Z{ds1NhV$VkCq+7c*` z&(6-?l)6$%j#uO3;~999PYz4WXf!Ufdz!hAa}yI2cMjX0keZtMQ0k~j-a)suwIyd| zW&L8YSd4nTUZ+Ia+qI&=J#{XZ>q2jD?{&e(J7*!5so-ilai7=${22%YTz(YM!m?95 z9*@`Qbh^vS%X9ks`$r;mG~ff_NBDirTYc5OT`*nSdU|?_1YwHZt@K&I%J(hMZWMRg z)z$R?8&K+V4cOlQOg>6j2mFx|=I4iaLTu})KOf%kGw~?mCxt~hNhIc7vbfRK*48q9 z{sxwwa}E+@vHnO23yQ?~#b+MkREdPc72SeuWo5;PI`M zudlnLG;+J$E_OND9>0;xcq=L@9>d)}MM$t{wOW0s%3iN`b9Hrf1?kk16gPWWC&9s# zloW4pceD2LKE6fZ%p)GzLZOs6MfExN4BLTDBt}dFH#{Sy0oT{a-4&JC6>B9;^xgVO zxeNPG8567Eq6+@@c;J{4bEu=-J8UIFOv#?RL$Ul^8r1AmC1=b8BWEKqNF**^?h=8MUTVb_^Yw8@qMIJ2HLZQs)(%Kv$nS8r24#L z2wPD^;4epN-dM)*S4-6abptHO&7W}nau%i#x%pu}#Rf(5D=24SvMA9%utbGLg++xO csr)Oz08I+^|Dt*_O#lD@07*qoM6N<$f?sgP(EtDd literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_shift_locked.png b/java/res/drawable-hdpi/sym_bkeyboard_shift_locked.png new file mode 100644 index 0000000000000000000000000000000000000000..5664491267bfe10b0660f1b0b0474b99cc174e0d GIT binary patch literal 1115 zcmV-h1f=_kP)xl&P!d!$alsfC!a`h#izYZQnK0-?oE=CQG%>Kq1i^tsBa18=1_#8DFlZt~ zqEzS~dbFJ1pZtzAL`YxzPET_$`Q+NWcklf^cdz&UToql{Oa8~?DEUv5EoO_^VzyX8 zN&SNtkWc{iFInFH(_$p#0cF4iG>`-vX37=a6ct6;6TU+vRW&g&akj3m?%DkOd>G#Y zz|F_aNi^~(hJDIafCn63TwJ_fQ&Y1@yF;PSJM3!(M}V92mB_{vF%H(v!M8>tk++)^&i*Q*Ds@-qcXYy8B!F)vE}9EYZP1|QU)z8Ew52l3piYCH&CpUi-Wij zjYi)TBW2Lh(eWJRodZ>%v{1QXq)NH+a}U1j{t*=rsdt|Yz3w4Tb_#1;M_ zQs~CU#y4y}gO%R|t+Q-~k-nIt=g4Jp*F18kEhLVnm=qC9rBZYu))^@aJhImnDX7i0 zwY7xENy%i=XkwOKexoL%b#`{np@Y4LE+K%)q-U?pnx>^zS69DBB9Rr5X7;yMg7wNJS|f4u?PW_xInzkm>-<+4dRL zr(>~LobvOGL0VEw_{)*XuVlUa*3!98-Yg3x`4w))%3?}RGtcK$te*4x(yc6JKkjWY hTg(>wchgS+1_0s_B03J8U}fi7AzZCsS=07#KM`T^vIyZoQf1$aPpjz;*J32j_ehR?98C{`PEDm)>1Z zg|LIIEH0}vZhx@jVdk{hS6C8bA(h;woh7BGb%24H#{eJkhG~}Z0rBiZ)ywbB`u6X| zgddB3Xd0N$*s`ti{gO3%_gwEi*!6h%ftY1xnoo`YteCRvx8v5o;ikpbVWEF6*=}Q* z$)(zS>BYt*`Q!7AvSM6}Za#jX*=H}#Rw5Z~?6op1JLTvq|D!YV4!Y%ON*>ALPc#4g z_`dX)iw`~YC;EN8Aadj0!aH5NTINTGBtM!s!@|_#BeV9@s|$h?o@<}o{(hSKyQx`E iBIOerOs_^aFfwr6bvj~Sc{Ldra15TVelF{r5}E+NRg;GR literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/sym_bkeyboard_tab.png b/java/res/drawable-hdpi/sym_bkeyboard_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..3466e127101007166179df2ea81967b68ccd10ee GIT binary patch literal 1008 zcmV9C|8f55XR!;-T$Pi-C%V2)&CvNv}nE)LcSoOTh@0wx_0eC`Ay|vv?4p6)Z^6 zek5IOn$7xNcq7vw&4z9_5i{^7yP3`GZ{NK6&+LR*mL)Y)k)pB8Rnx^rHsUnA4Ks<*u0+BpW;KN60(-0t;IF!nb zp%A3pGDcU8kvv8cMv6uLWHkba2@Wg^o&ubf2~g!m04Zd2Dz#^l`yK++QN2)30O3@i z4-XID2nK`N{QUe|I5$9fbYg^ZGpRXHAQTE+%x1IcR4R2B&SRkS=*$#3MRen{p6#&> zJraooP195}narbNv3O}_X6A(ea!AK@0r3P?1j#mj9|pApP=rREJUcs^+}qphSX^9u zM(ylUo+);L7#(}b&UI?QTLa2N-EslCEGkwkEiK*b@9z()g>>4?=Eg*$J zu;Fv`FN29%jG6JJ3GH)TV*JAfGiF~k@^YRCR1Z40P#_lFBA&# z>dHhS@osf>^$Co!Eowhp50FU)c0@pSeF#UTyuZJ{BUV82zFJ;hehP=!{X1kMZFYe~ zD3hS<8f&H7cA?O+qnu18vvYHE&k5!mO}1fb`^={f?1}^6$%Dm|Mn^}drl+TK)aBbW z$uc`IP5@c_{R+BlYh+~P0l*xa?Wx|w7jA9=8IUiz>h%!TVHd(kw|yTcfDqspK``N6 zy&)?`OLz@#iZ!)(V@LLvTkmDf0>pEz1f}9fWwrFIhapONAE*eu9qe*uVHm*>KC~8!S zeNG=U<9ExsWSl^m+B&VFH~BJ;-gbU-?m6e4dn>4_YH6h=T3W2Nf!aWApd$xl)0_(= zk;KjwNYxxwvw_&PgJgxuPzfqP@_s;qAk+n*pDAp*;d4S>$OqX8BugNY**?Tx)ddAY zq0m#j`P=U9?uWQOPWMH#fkYZAugm3Pma=#}9+$>iLNkEa5W1{r?1t0nbkH!`ngO&2 zg;oIq7l#y&Y#wUq#8_(+D35U8QYkN~%>WW9aq`U0v$+C5r5-5TU&7ca`t0-@WlEyO zdZ!p$jYDK52p`EiIy!n$lBE3n{Cu21WV4*wN{Q1Coq$eHOibK}#bRNkiBO{wsI3NY zKIt-;(Ae16_1W3kTksh%7z{E<5A^GwMwzfXm9su>y4As8Mn*>NEG#Tsxp;YF2*^njYydjh*VlJzsv-QMsZVRQ}L|b2Pj^pd^&ptG8HzL~=uC zpsPJSJqh+90AAwy=ElaxcxPwlx+bLSmc!vlPft(Z#_xw9A7o`5q5%uphlYm60d=mC zQ$(>?^u^=xOBB7Lp|Drb*!aSMBe2lBMgj!_fsX?N1FvgxYIV2Z&3AQm?X0YIZRicdhP*D?Q5|6M$hm0&#ptsp_yWQ){%gc|4hlgKj`=wGT7Aao|XtRnw zL7$;-WSz8rMPmf0Ou2$VeStnKE-pU792P~IstnBz8V=jkCx!cmDFfx9l74w*bm~yZ z%o3kK=jP^~A#^t*k;oKy(t2MoW44#cAyWjH4~N6CTrRhc5YA9?E7iNYtp!Ncm^V!p zOGl&8=VY0p*+BZaNTDJ8qhf3ZkVW_4mZ+&42GCxY)eFRYql{K5)MZ&#D1ww`0P%-Q z0I2{K3I#srl+D;Z+Yg8VWU;Ja5EdjosZj~f@LEf=rN)c-{dUkYlyhV=xAYVO;N^jK wkFj`=g&O^DlGZ{ku?s7-|1bOBfPM=w0I|fwgfS3Df&c&j07*qoM6N<$f_ppV)Bpeg literal 0 HcmV?d00001 diff --git a/java/res/drawable-hdpi/voice_swipe_hint.png b/java/res/drawable-hdpi/voice_swipe_hint.png new file mode 100644 index 0000000000000000000000000000000000000000..130f83a9cfa12a280db1fd3db0c29e28c814ca88 GIT binary patch literal 5965 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-land-hdpi/btn_keyboard_key_normal_off_stone.9.png b/java/res/drawable-land-hdpi/btn_keyboard_key_normal_off_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..67a204f8565ca2f6a4f87852ae1919d7611e22d1 GIT binary patch literal 2691 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-land-hdpi/btn_keyboard_key_normal_on_stone.9.png b/java/res/drawable-land-hdpi/btn_keyboard_key_normal_on_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..63cbe60a3ab527002739db25418087ec7a1b674a GIT binary patch literal 2720 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 mE=D4vEApoaLbnY diff --git a/java/res/drawable-land-mdpi/btn_keyboard_key_normal_off_stone.9.png b/java/res/drawable-land-mdpi/btn_keyboard_key_normal_off_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..67a204f8565ca2f6a4f87852ae1919d7611e22d1 GIT binary patch literal 2691 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-land-mdpi/btn_keyboard_key_normal_on.9.png b/java/res/drawable-land-mdpi/btn_keyboard_key_normal_on.9.png deleted file mode 100644 index 0c16ed5093dfcac53807d9d9663c516e52bb07ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 926 zcmV;P17ZA$P)f`xPoyonGAWz zi+N}hP%}OV`G`E_GehzVSsoljn9(SVl35Z017=ki+}n;<;Rrj7g z=T=wsRV>RwSW^^b<;GG%`5+*LLLoew5Z3K>6%}{H?hFu}U64ex*$j)t0{h4tFN?)u zh{xlUU)w+?lL@NTDh$JbuIpGXmwN|9K~+^<_w9BYjYb2hRB9W@d_G61RKj#R#dti1 zX_}ERp=sKhjdHn+TCHXWLPpY1BT!KSA*PumZ252H2nDAcAs{3T*&#x;TjC2K59Y{K(IKD ztA-6=U=E-DK+Le|%3(1a8R0=tU#=K1^fmab<+pkJ@$1M4@;LQ~Qm%HAGCwLNjxyoqhFO-oo1*`MIL#hgv;|jPERgT|M=NgF35oJaiolpF#xO);*l8fdNSfBO&Aj& zz_&EKL{S(Y!}Ap{+m!ZYG7!>DPrQx-h%AO4MCUN8#__v`&WmC&Kg{r3xolgPjdhB% z2IBP1DmwKO&}*FAJYB|u)8t(x;@=^>JhE7--`oh2C%eqoy*4N>8T=-t>{#F`4S$m2 z@3G(FxF9DqSxjA=%r_^ky=Znj6v_PiX2dP=f&y}7#OVn{0J|`<*BlY$@XLs7tnVBN z^O_Fx+LuZr%aMHo@RUYeHDVnj?5->GFJ**kTI}@BzH9akgbFnjhnk}_thh!+lqf4k z6o<-J97tsYB#}s<*Xyl|dC6pQ!${Dib(%{+sG!&Bbk;F#jASwyG@DJ->-EUat2`hA zK{0JW=mw#s8ruIbR$bhx#Y+%JTJe<~#{LR007x($d)Rj$0{{R307*qoM6N<$f>02w ASpWb4 diff --git a/java/res/drawable-land-mdpi/btn_keyboard_key_normal_on_stone.9.png b/java/res/drawable-land-mdpi/btn_keyboard_key_normal_on_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..63cbe60a3ab527002739db25418087ec7a1b674a GIT binary patch literal 2720 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 O0000)JiSw6o*v#l5sSFszBQ=lu46 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 ULR7(SfdBvi07*qoM6N<$g69e{_5c6? literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/btn_close_selected.png b/java/res/drawable-mdpi/btn_close_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..f2bf91a2d716935b05298c513c69457691f93293 GIT binary patch literal 1716 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 SOLA@i0000) z&lwIQM*QzT63LU#lh2dSWA^#cV@~)pdGh3UCrz4EmmrTRbwx!*?UvXlQ7V#>PhX=anm0-0yhI#fum7sIRZj1MQQ# zx;h63NW;O?VDOMJ$@)~nPrOW61Oc0xnsPKZH#=HdTIA~0t8(qyHMxHMy8E-WwblI& z;O|55e8P4yARH794B^08Fc4m3biy-UCM?3R3=G=Z+T_NK8*=mJO=)j$myV8(9QYaB zx^*iE&e5P9fHv7a2SpD|Xn-&PUgYNqW4uh>)oCOSLMIHtkOtT?3!q`kPs?1r>z++qGv-35M=Wr zzfYKu@N%jMFR{E@UBB5NN51%ANRGEnk6!Fn!mbf5z$^gSqDAs9znwRv?*3Fc{pBAs zY3SY%3>&&y=|vtYD|<;M$QCVv?+0GT*+APrYoz|0V!80mUxGA#`^XV6C)2sFKbH%cbYv)iUty8R_ZkO#Nmb>Kk8w(`yNa zC&+|s(IQ#*TJ0Te(tWQ{`tR3E-;?w1MT=mohr1g9qhN(XYbdl~ zk+$)mv1>pJ=$#gTY|$dvr_x(67zHcDT@B7q8g{27gx)o=0(M&n*`h_TafTDHhs2G# zG$Fh1RVaE-EF{2?9yui^u>Oe1DTL5T4eX`-LilGerFHR0L9QSXGoWy{pW#LblGqQ7@qwy z2Quy1vnO6;=gyt7ckf-U`*t@g03!e>W`%U= zg{rdG%U0v<$H*enTKqHi+kS;%6g0bjx*&feZ{`MOLg>;mp^qTbF}kT8L7FWl#t_SC2l!qfeaynUF17WZAN1&V0?9H961- z<*by2p|Y^&=+UPD`qLSZ3E84WmMmE!D_5>`B}UK)<+PA3K??(FLdWURrvUoX8ITFt zqD2-hS|rPtFLz)n3FY)`>6scXY{P~}f4YCy?V(36i>Dd|% zJl}>#f4YBUs;sPx7g?}if&2G!Oru82St(nJg%T1#f4YBUs;H=l7nwJ2o+~d*qXrF8 z&`O0$u@HE^7GQtuADQsxQnUzOB|>>&8Z~H$f_lc#vBs8U3_O;zY{J z%UyY48VMR9R!YxSs3g#kX@sP;gl-Fcpjx6urcImXrZ5YLT4Kc0 z211ML;6NX*M5cI=)R$^{i;Ih;q@*Mjh^B$r%wooj8F`?T0Yaj52!7V!n5k2z%EXBi z?Cgkk@=Ujq?(CG4rQsFZAnKdDg#<`n;c= x7kvXSKLTFzjP>O-#!Ec?|N7qzkL155{Rf;PP9H9-2x0&L002ovPDHLkV1nE#j2{31 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/btn_keyboard_key_normal_stone.9.png b/java/res/drawable-mdpi/btn_keyboard_key_normal_stone.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fba10b8882d6ecb43458ed5535e20d77f65197c5 GIT binary patch literal 2211 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/btn_keyboard_toggle_off.png b/java/res/drawable-mdpi/btn_keyboard_toggle_off.png new file mode 100644 index 0000000000000000000000000000000000000000..21399a4f3618f0e6549016cd7b53e75767029de6 GIT binary patch literal 693 zcmV;m0!safP)FRt&i8pU@BJ4{)I%io;3~XK@}<=MrD+L_!F;P`+!` zIp>!4TK!bWycheGlK^aHwy$+qd<4T`m*~l|Krl(`m_K zu{^q5t_O$1ac4H0i3LTB1!z8>({MOk4hDm7)9Li3SS)sa6Ut_@5(|9s`~69;*Gs$I zj>hA$cw%X_TC&+};@Ru`l0Ki0n$0F1k4NIt$Y?Z* zIR5wheNw4Z6buGQuh%CP3dMj!pAc8#=jrJw76=3$*r?HH2oeNP788VF42yX>f4kilBu-hBb2^>gnP@7< zfB_a9aOECZt=4;_J)6zQWHJd7O29>2M6tgXT`U%&RB&Y>kkQ!velMF+sTB6B&@~1e zaAhF|*O~MIC!l`x1M9j>2qDYOW+O1b{y3;E_XraU5MRGYdAVHDYPAxCYl9ClV1NY& zTv>>FQ)b695{ZbDCX)&A0jYcEU1C)0e zNG6iqZl_c#CHy?$U&G<>&w<41cDpZp;w$DPm&;Kikq|?Od4XXMA7a1&3y%DvBBP5{ zDix{I={z%W!TmVjGQ@xZ_L_V1Gg7P7Vmxedel?;z&0>v{_+o<4i9=&*5J?KH^z=N{Ec93CP z@lRbh*R5@mCTV;xS%b16B7X45m-qX_`(EA`!M1Juk0l&sG(J8)8VZHdk|d3YqS((l z(=<19U0+sJb!lN?A@@h4OeUkWx3^EmK2j zoy%sk`9nq%6B7y<-0$t}9ZRKB(83?F?SBtl*miKq?n0M!8gi`%#bObgo12SdcxQfo z-Z2wfj)_EKa$sO!EY*|5J3N9G{0ur8{C>ZKYgQ58^%clCi8KATAj|TYAP6Obro4)# zr>92;2M3>#QRIc5fhy+_4u=s42H^L(Mpn~;(J)Y})ezDYT$Zv}Uth1Tt*u?1nVHEs z6(y6&w4x}Hg6}0hVFl5k2vMxT@`(_9KF7#L6Q*dQ5mZsuzJa-lG8`$F%V~mh4y(Jn zd!)0o6K^U{VF*A2CNz5=YV!wFYugRlbB;0QFqbu~^?KCN(b3=5)`qY82gni~gZgha zxe7k~K@)90o{Q1L;j)Ico)Pzh8*7<7FK%#;ZQo&|7S3GOu$Gr|H0u6_k9u)j>_bBb zHI>MQ8`eRz-Wg*Kb6LY$&xj}1vMgMP-+)@9PhF>+N}>Z(Cv-Q*7;~7*8rFJ7l}cra z64i^TYe)!ZQLO;AeV|@-MI5~69AnI3E^Ao(+hnhNv$I53Dfd_ZqkmaH1sDLhN2LT+(uVi|0000MLu#IeH9 zP1g-$c#K+Fc1?-Q%)Me5d&70bfjzFlz870Py*XBHU2WDl|I&IxTlI5`R5HSkSGCxu zx0D<1w=`aCcmI8jjl6yR??VDk9EvR<;=kM^m*|Y1xQpDoT%|Sk=m^x?$eEk^T6eoB ziT}9g$}AJFHu+@on%AcpE(Rz{@Yp5ob<*8wTw2o5wlLrY-)hD5t1S<*u1{z_9DKz^ zY}JXZ4Dq{1k~TUUuGUeR=&@tRqa^ihs*CnaxG~qZaCT>8s^X5N^EP~W!Wq7=AtaFb z=8W1fF(JNo`3+n4H?uG`>ZKh&OJeP(&yfB3YOVJXPqW!u)hBz#ozdd{ zXrpg1|9tY|iy4R9j(h z`!BikX9do;1nW&-z3#Qzv}gOL?YNa8x^|(>DMjYY6X)yqgToC>%vX>rNANINbqtSm cEBBety?I6NQ?BI;V3aa=y85}Sb4q9e0F9c|%m4rY literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/btn_led_on.9.png b/java/res/drawable-mdpi/btn_led_on.9.png new file mode 100644 index 0000000000000000000000000000000000000000..fe77abb08e8a87dc6f49872d4fa1214d04b7c5ff GIT binary patch literal 575 zcmeAS@N?(olHy`uVBq!ia0vp^MnLSs!3HD`yQJkZFfg%sx;TbZ+-(zy2%<+%x z$^|E#UJ9^k{c~LC7uRJ|A|NT!*pYF;F@QzYi-*TXf`8)DvnQ4u<7?_!;CRWXsA*Qf9eM=lH0#C<8gGN zqS*4pcYEJ^m8g{T3Vpqm&GBLFh6x)V^uBP}(e}-E@4my^4>{SbE>{kI-D|S)s_L1Q zrvD!Tb*)SIej>-E&nN%R5|^`UY-1h(#kyy0JtzIH`gP~s<@)=hzx#BSP88Z!0yIy+ z{^sU=$J_MR=R`f9wte1Yp?ZgT6~F(BJ?ejPd)myfI$oaEq;EC*M1P(4GbuRpwmR|| z-;A7ly~|g3-=4c=OYV}&wa3k0n}64|U-LZp+1jmMkC%rQnXIzrv(`SuwW3~qP4<_C zRxXPrEB-PuaVRt}KnM>bg%)X0aD&4fov@rD;Pr7yy;c8B=>^8IO2CA{;OXk;vd$@? F2>{eN1i1hJ literal 0 HcmV?d00001 diff --git a/java/res/drawable/cancel.png b/java/res/drawable-mdpi/cancel.png similarity index 100% rename from java/res/drawable/cancel.png rename to java/res/drawable-mdpi/cancel.png diff --git a/java/res/drawable/caution.png b/java/res/drawable-mdpi/caution.png similarity index 100% rename from java/res/drawable/caution.png rename to java/res/drawable-mdpi/caution.png diff --git a/java/res/drawable/dialog_top_dark_bottom_medium.9.png b/java/res/drawable-mdpi/dialog_top_dark_bottom_medium.9.png similarity index 100% rename from java/res/drawable/dialog_top_dark_bottom_medium.9.png rename to java/res/drawable-mdpi/dialog_top_dark_bottom_medium.9.png diff --git a/java/res/drawable/ic_dialog_alert_large.png b/java/res/drawable-mdpi/ic_dialog_alert_large.png similarity index 100% rename from java/res/drawable/ic_dialog_alert_large.png rename to java/res/drawable-mdpi/ic_dialog_alert_large.png diff --git a/java/res/drawable/ic_dialog_voice_input.png b/java/res/drawable-mdpi/ic_dialog_voice_input.png similarity index 100% rename from java/res/drawable/ic_dialog_voice_input.png rename to java/res/drawable-mdpi/ic_dialog_voice_input.png diff --git a/java/res/drawable/ic_dialog_wave_0_0.png b/java/res/drawable-mdpi/ic_dialog_wave_0_0.png similarity index 100% rename from java/res/drawable/ic_dialog_wave_0_0.png rename to java/res/drawable-mdpi/ic_dialog_wave_0_0.png diff --git a/java/res/drawable/ic_dialog_wave_1_3.png b/java/res/drawable-mdpi/ic_dialog_wave_1_3.png similarity index 100% rename from java/res/drawable/ic_dialog_wave_1_3.png rename to java/res/drawable-mdpi/ic_dialog_wave_1_3.png diff --git a/java/res/drawable/ic_dialog_wave_2_3.png b/java/res/drawable-mdpi/ic_dialog_wave_2_3.png similarity index 100% rename from java/res/drawable/ic_dialog_wave_2_3.png rename to java/res/drawable-mdpi/ic_dialog_wave_2_3.png diff --git a/java/res/drawable/ic_dialog_wave_3_3.png b/java/res/drawable-mdpi/ic_dialog_wave_3_3.png similarity index 100% rename from java/res/drawable/ic_dialog_wave_3_3.png rename to java/res/drawable-mdpi/ic_dialog_wave_3_3.png diff --git a/java/res/drawable/ic_dialog_wave_4_3.png b/java/res/drawable-mdpi/ic_dialog_wave_4_3.png similarity index 100% rename from java/res/drawable/ic_dialog_wave_4_3.png rename to java/res/drawable-mdpi/ic_dialog_wave_4_3.png diff --git a/java/res/drawable-mdpi/ic_subtype_keyboard.png b/java/res/drawable-mdpi/ic_subtype_keyboard.png new file mode 100755 index 0000000000000000000000000000000000000000..0d7ebd4e579c01416b40087f968ca65aa8537804 GIT binary patch literal 498 zcmVxt*`Q1UOu!1=0N!{^M__=WnhBZ>!d>NyPF8~5+O93?ldQB(lYTz`|6LR@3`3D} zhzytkGhhZhcfi~0x`N|7jEBV7)-c-3tE&1;5Due;agQ0KJ%0WEkuNEb0TXgOKm+K>U}oB(?! z+CI?SpM{!)(m=YSIhkt^n6NH)dYPHM(1#ms3^ipc;}*ke_M!%HXOsL-lbk|-6S5jj zQrd|3lnxHjNo*sUNe+2JiDDAlILahOKzNA#LIY`f8RtwI<@(aEwJ~8CQs6#Dbp&+#28Tx!vnuF?rfEFi z!?`vV;7%C}TUJAm3a%s5!Tizx~lS3GTy-i}O2b$M_Xs05(wrD8#Qi1poj507*qoM6N<$g0uY9y8r+H literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/ic_subtype_mic.png b/java/res/drawable-mdpi/ic_subtype_mic.png new file mode 100644 index 0000000000000000000000000000000000000000..247d5b3a9cec75d14aac825139c52ab6207f1c23 GIT binary patch literal 483 zcmV<90UZ8`P)%nybc3(~$^@kozy`2^xvLC1os@7dA&M&5(vuLxm*@LF<5EhiCD+Sp;Z+C} z0x$vRoF{AvM?%VI31JiiE<-D5dBP8G>%j+N@6RSkpc!~M<4mY#A&^g>45$$TNDf$I z{I+xz_|gXofkNQ_6UYOnCd4VmO?G!g4&nzO(G#5f2*!`V2~L{u0l!hssErY1s2t?ld9 zow{45+u7Gb|2#<8WYSG{yYrjb+4pv5%j)&IH;BR*YYeQ$XWoDc!|+OB$Nm>-S7S@U zC{FyYlLzaO@D6ZB)LDo2Zc4pFy&eVLWQGa>&WKOD&K&)^1;?LQJfhxaM1z}vcR_-1 z>odxbPT;mk>(gbF6W#{LuzY!W`TqF$_)-uAbDZ-h>UZ~gU35zSPbuZ+YPI^QSS)_n z+1dH74WNu^8sLJ*;70tlwY7C2#7l3$B@Dy28yg$1RJDeTuxFzUo&xtlR##WomA79& zzuDPYnNFu=DwUFBV`Du(XHQN}SfNnh`Fx%q92{`qQM2%Fb92)eP@@cR!DDc-xVWe| zQ#t4}KR;hvSXihMQ`ytw+UVWg-3|8k_IzMc20hATG6k3n$^huYT>}}fPfScKK?8VP zfmifkAsVz-X^scugC5YQ_4-=4^-yROPJm;0kw_%wpizYiR@m>?a@~xr^>|vZAmEz@ z2;%XE!zNIR^f7#?LMPPIHpn8i9-}wVI%}Fqp1-J>ba~!Pmr$MVLZe!oasVE0n*puI z(|QGBY4bc4^DPVZ0{f4Ch7gVnA2B&&o?NhAyya3078?*ZJa=qEJnnez;Kv|j1Tm{DQVmOm_) z%U_`3`1p7z2NR=5>y@t1ft`UX=9_7@v$L}-e3{SZ#Sk`XOzZKqUOV0AT(DHuwsv%M z^bs1f*{sN9G9!Z|hw(uV)V1DPl=p=rY)r1CmdoXS%*@PekB*K$-{0R)6pKZkPN%Dr zlarxIQ}h_)T!o5I9uEK<(s-w*r`w>{Yt2=*J#kE~5}%yFpoA9i^lc@{m6es(3ijC$ z7VP`2t*tkzT2k@{!e2>6i%EP*NLFHahTx3>x3n}2n4X?aO-)TbRYp8aCX)|@5O;co z4a4wvsZ{!=pua+>93CDP5cpp$I7ce~uh(;U`=l;(sx7b#7|~+BGkxs*O4M^(qHw5c zF_2{~7~J#;@dBn&+;+AMhY+5L1Oe`Q?&}@<9a19A^-pY07*qoM6N<$f{XA!Jpcdz literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/keyboard_key_feedback_more_background.9.png b/java/res/drawable-mdpi/keyboard_key_feedback_more_background.9.png new file mode 100755 index 0000000000000000000000000000000000000000..29aa285bd5446fa30710c24efe602ec948b44ea4 GIT binary patch literal 1385 zcmV-v1(y1WP)5 z5Nte%g=ke4${rN8T5zJgq%!0(vNTS$a{dfr z653z;Oy!f~cA=9CT~yUAPE+(Tj6pvTyahNh)JaHtl;Vff{UG29GqV6EhNoU9mVQxz z<0lqJtG77;c)(r8m2~yYHj}*cqb(&6r1FqqD3ev9Yo7k}>vOr&kcq^M2ag z-27ft8_0>a(P-jE;7!02AnWVvF9qA*Lcf`r8J^8%c_x$LeSLj?T(ISGnG}mfS|}9g z;o%_#JYp1HZEbBS2O1^^Y~V3)wz#+`aAuC7&;0y+V_{*Tt|^MWTr0czeBRyL+e-i@ z;n0`4T&@U%VR8WaP@5vf`rzQ;3upkw1@M|w<{_nV<)c2(4|c$ww40DZwMC)jC1VPk zCiBT;@)6UgJX_pzm6CFI!98%z2F-f1omm+jLH1j)ExU}O)yDkDx zTVK$`w=u7Vi-}GH&?qJ+?*QX%BOvWK(k??-tjuEvS|;o*_8<2dLijy|nYYHgal$&? zEq9u**nw_?r;7QOq zgQ_!VKq~3pA*7)1pf&B5&R?gu7EdXup*a>iwjmRms|8+US1|Z zw5$Pzz7h8JRxB(aM|kT4$icxu&&kP2N}NCa)z#H&I2E0rpS$^dJ`L^SfL$3K9jycK zot>ReYqc7gn3%Yko13dfIGD8Iqyee8W~xDy4=TSB2EJ4(HQ5DFS65e5v5rRop3nw; z@NaM)$NAUr2SdK5#UUCpOuSmH{ss+#!O(-r?CflHXlTd-Hiw6Yn+NHssi~Whk&!wG z)Zsg5gFes~euH%=JJK$;#-pPnmdoXOI=Om)ey{^|X}1x? zKC^@kS4p7b}lasG|dwajx-``J)7^T^4wk{@~r_vNLbCpzt3KNXG036bIVotmU zyIQWYZN*W!N_=txhmul&=}&~D*Vfj45MaOV^x7?)`FVSL`$tiQQ2QIgAJzaVD)Gf3 zS&6|D1YS9ilG1cwe0)4JHa7NLI58pS!556N&*FmhJnzp+rSiJ~eFC9!dU{$!z;C4B z94VKHRv%}Hsm8u51v}6qMSbU@lw87g$~Py(+aR@(ngxFL@Lo)t0Caba>@^S-75w?+20~0?y^{)J9y_3qgbDOr zuQE+e=zt?Juapwa0fLlJY2YF6VSU@K*Ty_?+AnYka^@j#z-`<0|M!0n;{NO7w(U_4 rIDzdgc;o)x200#C(S2p<{t;jRaYXaV8UYj(00000NkvXXu0mjfEPjx} literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/keyboard_popup_panel_background.9.png b/java/res/drawable-mdpi/keyboard_popup_panel_background.9.png new file mode 100644 index 0000000000000000000000000000000000000000..36d75df6f9d09d5fda2692891d8e8ac04119e6c6 GIT binary patch literal 996 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 Wi?!l27&M3g0000=hT|3J+u7g0#) zIF3_e-$VC>m7&e(VDt`r@V$HQx%Yd{efPX4*KW7RM#*GjBW98yNsuIH*v9uUhGW8c zhK#js)~En}1#9egy&pleupCSR4NwE+fHK|EUpO3|DHe;TcvyQA z<(-4cU|(RhTCH9#m+!DFt$xoN=I#Oq1h5;J6N$tPrBb=V(mYULGhmOnHs2#N2UDrk ztk36LY&M%po6Yvd<#Ij4bz>bi4wK8}0+J?>BZ6*$HA;dY2nuxbbPsD0v^oNtP^nZq z1A%~}QmIVAZ97|UtXqbD%1knuye($NYPG&X+XZkE90y0i2{2nKm9D4L>ASeTMEAf6 zu6R72NBF(SvM|kj?BJ@wU|3=qItTWP8u}7ru~=TBZ5|u~8VVFlfW5I;%!zC)D-?=G zCX>0xL|rzU%`eE;WHLQrerlj#i_h!z`gcsw**^)QhjkZ4-9^kC(1A%H>j;1rnKl}Y z&uJ|A6bgkXw)QpMlgLmgbd4=2upOa*9Xg%vZ7)F-wJsWsE{bAkVzjw{;TJKTJ_o>p zC_;qh$mMbmDBC~_#{X6jEqF|=R)3^{TCKK<`EA`&E6m6DceXLq6c=4#to_taa~?Zzg23l*E_UIjixhj zr7eE{&%DdTZxwn2e+El#xBF?M(WnpOZI7l8p!uV}$J|@~J;e19?3R8pa?)RENsuJy ee-rdgfB^t1-yZ!LiDf_l00008)<)t=s zU>=-4ZqnwS+;`r+_tlJ|$k?WlGq!Bp5V|3>?yvw8$O8qS$T^>p|0xgx0q3bj=eL0x zvB$_o#3qJG%z#Va7cnxBhf09cZnvM7%jLd8Bol~F@hu#Li;0gaZak<|DqnoxKWH|a z-&?JgJDE&E+?#DAJf@kmZM)FvbewLtdmp4O27|#v-1=%!Gy6iOUa$XjUH1cQ7wB(z z$s`a?qtSTQ>-Fvl2jLQHNM@r{D*eWE4txVVYN)Ih9k-rP<(r3KE@|H?_ zhJk4a3;>_I?iO&suLPWnDeXR=AMhoI^f|ZflZGv!#hQN-1DZOa2F_C2kGZRYq&&28 z)M_>Vf7+`OBQZ2%;->Ufr z_8aCKZ4Fr|m%6}AYcENJ6Xu-EH1v)gLedyzsv#7|VbX6B*4Gt2a`PoHyyWZnOWF&>Y_!66@5V^!WE426J%eN3$^ z+YIuZI}7=4hc6OQgNO8Kki%8SD6EIllSmujIL>jF{gC(+-z>V}R(6cmHucm(Xsb>g zW{N)>V)C|<1VbfZE?%E%B}NELiw#~NJ~mZJ;F2At!L7VUB7I(;>i9@gI<5%8$wv}= zuapVtE}!uFQJjZ##ibje0vmJ(&R_7_XKv?5v|sb&A_s*~Cqiim#Y@&8sgs5KXC(83 z5z<<+T*{p>qC=MimXgV0$>y}g<*O>k9B+|~PK5pnFaTA7#}cCJrUw83002ovPDHLk FV1mm!XCnXr literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_globe.png b/java/res/drawable-mdpi/sym_bkeyboard_globe.png new file mode 100644 index 0000000000000000000000000000000000000000..c6595cf623b8f87ac4a769f707582ad55eb0785b GIT binary patch literal 1303 zcmV+y1?c*TP)1ksDZdNG+|JA_)M8xph{ ztxg0@7)zWsOQ$xPI-5OjIiLGXyU8xfVD!Ml`OZ1td7t;Z-}^r2<2)XZ&+BX=pV!>p z5d8N9ewVCsfG>N`kBzCF0e`UmIpF!X1avYn5CBMlKtRIYCu7e57jOt10Vj;ma~T2e zF&Pd4BG%T{RAXaf@2#(|$BNKUHy)`}dX%4^zfxUYy@+@0z^}j)z|BtdSEE8>-~cfY z0%%rOS3iW2?;h-8s?VOu2l7BV2Zy}kYR$;pWX8=v&{_C7G1&C2fX?pdro z#{7xPZ_Dr<9*}{Bg@v^G`uZw7h6DcR8ONB&`;^++S|d%x$H(t9 zHa31kJ)I^oG0|L5Q1ER`Ow49#YU&TUxw-el!^3}}H98@kg!a)Gc8Y*^Sjf+Fn;7Mj zlao3kH5!d~5S}fI#o|JLR904IUR+!Zwb^VkcyCEdOS8_-&JKr#g?%LYCnrk~W-I0#l)568^H-6M5q7&hA~G^^7e;NEd+KyLuPBvDTVY|Lt*x!i41zkQ0fzrP=a7&kGaJ_iQ} zq07t5T7~+1uNTN=ZriBt1Rd3I~*|IPWsyI~5k1 zn3ebkadB~!4IZScotK22ot+M>)g$H;Jv}|Wc>a{+gNl*BYf@KN*XJaZKp?M2LM^?pgT$L{w<1YC(A>tF7Ng!`umA0GyZ&IRQO584ib^@A3H`5zJixM}R`ihTK=A28T+e z((%p-p69jcbb1<>I*L1l(@-7CK|SUbi5rOynx1lmN@p*5u?QdvIWN zb@eQZM?4;X)oI-0@st9AKxJ)h&0eWgEO?9e^!zWWTEtR*4ESv}+XgEK`F#Ek##gi1 z>=KN=b-7&SnVFfFxOW}gn3$M&!_4GzxjBqoKr2^^bl}*+!op4VpxJExw7k50j|>W2 z19MYTQ|oMv-|v5cv5z*VloL>aQE)06jouduo6qN4Po+|iA^ac~i#;=$Okda>T(f~{ zdlBpHw0llYfGyB~ad4WwiNoR8?5w6nqp=zag`Q%~dElhVtLU{{3nF7w^TXwG`Ig|4 zm6eqx+`9tK02>)scC#gxrEHf!K#h*e8eCCBJifBYNF?$ZT^;ODa`AtCJv|(mZOUv_ zFc`e7*XxfKi$yz|n4O&sRI61tzN#nl^YdF$)wGur&DH6D0k7A4mtr+2W<&1KXN7B* z9h>M6)9Jv5^4 QegFUf07*qoM6N<$f{;RzLI3~& literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num0.png b/java/res/drawable-mdpi/sym_bkeyboard_num0.png new file mode 100644 index 0000000000000000000000000000000000000000..7188f9ca54ce6fe689b4a23006ba736ae38eb8a0 GIT binary patch literal 1148 zcmV-?1cUpDP)X)SWVDK5HrUA!({7k_GsL8VXP z{$OANM($q*9ER^iv6vJyyi6o>)QAdJW6zyufvvtY@00*+mwS!N_) z0vEuos;a8z@$vC47U-pRU0vN%jOBqi5GUnv29~62qY-}t8?X&D$ z4?Wavc=>^W0jtO3F)4#s?%z@UAb}2t;|uCy>YtjLqG%Ao;_2Kqm1_qIA=AjwBQ6+4 zxjgvIVRMzE#|mFdlPDq!bx><-Yl=7s!6O{v=InO+PjL*sA)AHCJ6Ree0vbVCuc7!X48Qy=sORZDdOzEUuR+Yi!a@o}ehlYj{#NX4?(`KGb z&G$Ub^g7Cxy*oHK7?GNqYUFmLI7SxRWq;ECovo>BzB@`Z?Fn&h zQc_alQQFFZxw*OTNeG|gZy}Kr6%`c?ZnvBK#~4rEk$9`4y=w(Gb8>QewtKPRl(z`P z^S<^sy`}4ibABJg>8|mprBes=e9Kq+nIt;TMxI(K-1cUg9@910!qc+|-bK z9$e-epYrzeeSLklwzf9P!U9T5OF!eKgdEf;@7Dz}t@Ztqa~%E*kyIX1=F~Klzv7sL zJv+~$SA6sN)rp2rvNV3sRb4kR;Xs O0000L8FJA4%^LS1wB z9{f0x-157-ymyyKWm(ol4Y?+8kUYtgJjn|a%L|@M*k=V;#d{MV2gbmZS9uwQ>wtzW z+Z3e002l%VkMc{v8qfgt9mhHIeZM`0&@}Ba48u#DeXiss6yE@LfCJmMFS_0C$xjCe zXtsf8-rjj8z6tC_QFPSlbgugS{^76D`IcYDzFcK{CRF!4@6 z7dRG0{)3e;5P!ksCyBJf8zZx&o{PA*FA+M}kUYtgyb$s%;pIh9%xC

Sy`?$g{ft zo~G$=b|Z&?<_-9)y1caDrv>|hvxUdo1`ZYzPk`5|?*SIc%QyfEBH@gWu$}NMPuZU2 jNuK0Mp5!YYUjhsO_^ftO71TET00000NkvXXu0mjfy$9y; literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num2.png b/java/res/drawable-mdpi/sym_bkeyboard_num2.png new file mode 100644 index 0000000000000000000000000000000000000000..c1e9cc9b137f9f2cc255503e270b659482c90595 GIT binary patch literal 1785 zcmVjgE$DC)LgiWBU^O7(3-TTgc?|x?=keEy+i)ZzcT0Fy-nY@|2 znY@|&GjqviP>38*Fa#L`pl3V;CdHd163IVK=4s2en7oAN%K!&p1&`MJTmy^%w}DYW z2N?g;rYCJNWPl?O)YjIPmXwtAy_=ic5Qz}ze&^1ewRj!}ya1>GiLn1)#${d*Pn1OJ z@ZrPxN~KaOke&)3AD`E=x``H&Q_WJtwIdrC<79db6743|C6@W8f3s|udh{SSUCuMr@=)~)CmXq;2NbY&y zMfSdxfDJ!?Y9)xXJ;kr~^y$+_6bgklFfj1v?(Xh)J3BiI)M|Ax6fhG>cwHdcM^+_~sSnNOopaz&8Nqm)tt z^{A?etPPsE#4D%HrQO`E15(HgbTV_u<-ZyzYQyP|h$A(yv4$mZtRcDIFg9i`3COkyR8$NaF zR3+E?*~nW8Jp;TZ+}+*1FD@=lJuxw%fU9p2VpYQ35swr~awzDLqW%2*h}iPM!NE0- zj*deh-iUb@&;k4c{3arAS_<+KCQjDzNl8h`Zf$M#6CM^87M5V>W6M9B<=sTY{_54M z>qbXM-6A3))Cma*y*4&B_dqU!!$~ETN=Zo;$BrFKkBNyn3VehS#rwd418)jRjxNO4m%DfGCbt_Vx0^-V5)K)M zvuDpH(E>>8E?&Io1ro!zZr$1lm!?c@3-VGSB%-6E8wjMJ+S=L}c#Nx%$*rxewL)xz z_>y#0vT5cvfLBFD#r6{?PP`&K*t&IV3+9x4d)ey7Ib5C+nFGq@dRn0JBo?@E;X+hS zPEH-X{gLHSO|B!ET_=LD{I%@t?Dvq(!-9i@Ympap!^6X?xa8R)2Wj~GCLSjjVtwf} z2lhmQN!YVzPmb^)JUmMM^;sFGL!LqI+wRaq6;OLMXXlgz;6Jr0@-9)Wa=1F z4u~naF*0)O8gM_!Yc!5~dwc&hG&HpO#*G_+RO`79SPi(cv1E(2Jh_+C&6_tvV3D_U zI-OD=j#5*@ZtTt)G4JO* zp$QEQttRsDuplU+lZ5N)>f#?C3JMDg3oA`ePbZ~~qT#s>1$=-|%q=f3&z$m|q8Yqz z&PyWd%c-mdAqIb#&&IsGy!Q(V3O>i}An8N=d!BwIarUU5&<7cAp-!Ci$mSHYoY zjR*_w00000NkvXXu0mjfxZh^N literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num3.png b/java/res/drawable-mdpi/sym_bkeyboard_num3.png new file mode 100644 index 0000000000000000000000000000000000000000..e9987668cc7eb1776775e1da6611be282b2e9e26 GIT binary patch literal 1675 zcmV;626Xv}P)`9X(>A8V;)^shvOZ+PXoV=ya^zqpOO22;Zpz+$gny;@LMSXdty7dJ?^g;9Uw#*Gc_?d|LETpTQz z=c%4|p(OV3v?hLNU}0OfZ24TL(><1ho@z^$EV_?zwQ@%Zr&#pFUl@bLY=?6fEZRc0Kn7}I-7)bqJ^^pc z$jJDkt*z|~Sp-6(qM~#wSFY?hb?Q_$JscDiWaAgkblwT9p`f7PJCn(j_~eBF0oLWq zm;btJ*RHSQ7?B2b*6Ki~fHQtin3l0u$KYR9U4o82^ zsgA%r=BtA}J)ff%+}hfjARf%j%=`_PagO42jiR&=V6|akVGkgdXFwn6%9Sf=H*Vbc zaPQu|i46@6MdWkuT%##rz>Wq5cvf*#1q%IY|Bg&Q|6Wm1QFD2Dd0}&Nb6#a--F~h z{QOSz2VnP^GIn2r7ZD3Nc<|s?^1;y1(6Ne&ipH&5w{|nT@@KoScO5P-~+qP}nrsKzt zQ{?X_B_-WsQF6_Oo`2f;dE)43L`1|*yWKvi@BmMMREG{7+C(PiwzRao&wG?`)I$y& zIFN6%*(_$W`B7wKWJ^g&Nu#eguaa%jh&^ZF`ud3zC*DP2lK@fr2rt%y`>8m>U-tF& zZQ`TKz#!9es_Q2U<*O z2L$f-+}zw&=Cz6EIS~jCB0W@FTU&$vAXCW1QFlKpc=1)I;ncK{bF-ekPLchaUIZl` zmQU>L?0l2?C(VLgVopa=-9&{{Sy|b7r_-sAiHW&RRA5aYiik`j3KvOG$K&x(e~1e! za)6P+&sp#Yd@w{Vt+=?@!sB8S#LK0nrKRcei7QsDxT_#YQ+2~Z8wa=bNNevo9FE5q zE?mg!?d{Dp7!18oddeCu;xSF=>FIgng-#D@__6Hl?7qRlLBpj>mzEOO#Uv_wQGj%DT`(PZa@@Gf<#OPNM^sl= zZ|4HZl$MrOUsY9AjV_%gH8@UbOV!lWv>KPyvNd)9{56~s2lr>IEuN~0+MakA1y7fq zKY#v{nI<6L+G=WQN|CSY(fy0lo>FbE;{r*Pw^}|8c3F9A=2JIQdFx?%c{#U_^BO*j zJn_kzo}PXgkxm^L7>H2+*HvHC)PuQ+lK8(W6J-Mmb6Xs6zS~+q`-6b^ascf$W3g3Fj3e|G)n)Lx7jq0a_Ng zd93&WGXC0_9yafE0{60j_y2Y62k!^(2k!^}vbBE%7yysa VKp-sFe8vC(002ovPDHLkV1lSbLp%Tg literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num4.png b/java/res/drawable-mdpi/sym_bkeyboard_num4.png new file mode 100644 index 0000000000000000000000000000000000000000..7f0f3cccc18a707ed940e095341d14777c205fc9 GIT binary patch literal 1530 zcmVAb zEf}>LwbcY2XA+~%`*!c?KH+9M#>Ye|ItLba?z#7zbJyPM?7fe2f*?rFYLiONaPyG& zkoS=HkUuj`egNah015&&E5JloJA0Aixc`mBQPcIS$Risba2^muS6syaSO+`;&PYkw>}v0V3<`>x*7u@Gp5EQpSFbqcxN?#+zaX65a)DlY-c{*eRA+$w<6t z9gfDzX*`ZIlKSkgi##r-280(E7vGqho4a_#EWtijO)(>IetupvG&FSCZnxV~Qc`}8 zi;J5h1K@lrfQ4eob28X@GMJ6#J4f^3{RYJz+fm6oBr1Xwa1QX6R;#_^<>iIm5kf*j z*2vmIOiauoJkJ6IiO0H_Qw|6OyxZK|T$7!htxHc&7sMzpFaI9b0$j!Wl9H02;dd;+ z7ZBFa&~Ov`8yg#M!~J#fScpUSp)VAmbmDMe$X&=C_j6QfY3WU)(dgyp=Vt&f&2)Bl zY7S{v^ju&|OUs9CZEfH2JZ~&0DEP_S+k4Yuv6x3jM&5zBmq>jVaDb1GkBU62Ly!AH zqkiBA4GA(?L99eS%no96@A9!UQtn@otT(-XlUpR^gYE7hAnlE(k8|NFB}Ah!S&yyJs+S}s{{*{##4fNl}N^fjz?3O6k ziW@4yoR*e03&BnTWHl5|biKX3U%~YX821~S%_gJm;V@Ekbo63PO-(!e{0jxs*VmVd zy!ckmhPI|~`jl_Zh;Ze(QS8thyB)YR0~)z#G~NSPK<@xlrw zDXa2KCesdF76BL^0=Kuf<3)2S$WRyn6{3Pk>TZ#P`&J(w9{x}X&G{wtyk4(&cpqay zSXkI&@cf_Tbyo``86>ivm6g@p)6?@c*<2zWot? zKA?QO*Wm`s&-JphvfuCqQr?&C?d@LxUc3+y5%G|!G{?sSS&Zgel)`qOL8E?*EjyW+ znH_jPIy#z_mzSr8Z_0P)ZxS3;XEIR@&Wk3*eHsZ1-R#2H&f^wTen$R^IB!aL;Mnkv(rCn4F%Y zCwP#(?u9#{Q9v^6bEFmMxt^4z1H#Jyo}L*+2OB{oU#lp6TAdCyJVAW0vAOGc$tzBG znCdj-Jt>i1B~m6=f63WnPIjE-PRfyXKk_UxD+wIikItp#0mz@Y#(Dl-+dl#f03`gP>uxc1TL1t607*qoM6N<$f{lIO`~Uy| literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num5.png b/java/res/drawable-mdpi/sym_bkeyboard_num5.png new file mode 100644 index 0000000000000000000000000000000000000000..5f748b416e94cd2ac0a55f3aaade64043a0b2d7b GIT binary patch literal 1411 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 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num7.png b/java/res/drawable-mdpi/sym_bkeyboard_num7.png new file mode 100644 index 0000000000000000000000000000000000000000..5bb874c472ff7e3897f34286328007d227b7c874 GIT binary patch literal 2040 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 QivR!s07*qoM6N<$f+{HftN;K2 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_num9.png b/java/res/drawable-mdpi/sym_bkeyboard_num9.png new file mode 100644 index 0000000000000000000000000000000000000000..f348c92af468aa676ec39e4c0768100c08f97825 GIT binary patch literal 2167 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 Q;s5{u07*qoM6N<$f-K}G4gdfE literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_numpound.png b/java/res/drawable-mdpi/sym_bkeyboard_numpound.png new file mode 100644 index 0000000000000000000000000000000000000000..9126eed0df3672c0f2dc95a9405cabc83dd051e0 GIT binary patch literal 910 zcmV;919AL`P)LdtRMN26igbXQk(Lr=_7P>eDSI2a4)IgS2Eh4d+z_9`zp6uEvZ+VN$TNdh&RL=;tlaV)B69z z_nCnLeuDP#HRd=qgu7+h<-thbV zk1#$C+`wnCSkfaSBT0@a-In$T;!~;A3=M+8;AeCWU`-?vv-Ns?KvmT;XMw5P#M7Q0 zEEbDC8ca+~eB`}u+1%U=@r2~q3-&u*yom){Ngx;oo@_Sjr-$Kicn{q%TIo`$KGh9f))b&(Sx3%uBv+1Q3yqz8=I?e>JtW~***Z$HGiKtjDyUI9+v2N%H2SS*&JuUuJKd4ll`u%N{j7Z;ym z90Or62He2hi7PL2PiFO#iTzFB2 z*RQMQoInMqVRm=f?CxXyh0QL`W_JhUm*744uA6{XryX^4e%m9F$aM%i??f}BRjXAi ziH}C3SF~Rw;T*e+=NC~2>?|)YKbV`FdxOU{k>W+|Cks$Rq0r-hb$Vice*PNo`96_* zUZmN%*tllE(jl{30=LQJbY^!8%z|^k0|tPp6Ah1yt^&UCC1pr6yUEGP4K89F>+9<$ zD2AFo%f$`#d%V}{LQ`JwQ77@rsUU)5zG$^tEBHAI&Voy*0p4=#DmW=R%?~5`_C!Zs zUox2-hLO2&W!>VE^`K_QF{hGvj#Y&hHEq7nllaEk+8SL}Bvg9Oxt`yP$Kz_PR8(y4zb*_x7gNP2y#)B^2(p_jWB^T)Vn9+MV{Y@xMAl kydmBYZ;0=K_D6sL0NR>bF!`{hzW@LL07*qoM6N<$g6(RvvH$=8 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_numstar.png b/java/res/drawable-mdpi/sym_bkeyboard_numstar.png new file mode 100644 index 0000000000000000000000000000000000000000..9b9f1b9869ee3bdf98bb48df6ca3b01d1a72810d GIT binary patch literal 943 zcmV;g15o^lP)7xtrW*Nra6kQOC^LymL1?iStbGeG=T(trRaDqnA0{q~% z-|vr6?8wN-9HGxP#w4dj`Z(n?tXj=~MT19LZf!N7k*mD;T)pk!^xE2*7c2X)-EPk? zd3Sf$hDGjKAQ0fZ|HLNQ=C;8%kQ6`n-wO~8#8-h+0|Nu$jg5^#E6fZlPT1PoYRY6X zHu^T3Ez{cC`nfoshhRV0&1cOsEEcUUqnLOdS#{4Lhp z-0YTGKq;3BkEQ9((9qCx4DVPC%<{*q4u_*&Q55so*x1cfD%HSbEO_51CnsNE1m@G} z^p9LFmj$_PnA`3CAk|*JT*R|P-42Jt?{JLDG_Xn~cXo7ibdbV>!Qgu)udJ+e;m$W; z%j4n~2R~)b@5x+G%V(BNg~zSR0xD(CZUDYmEH;ce&IL_fU0r=b=(CM6=18th`dYNy z_DZqOQ$Q2wgT0|x9&vt)9BAv4T#H_coNCZHF){J5aF?Ue=v}$XKKg~xz#QgEA6+9=-jCFK$yk1yX2*`ttqL&97&f`vA1RUE$oLQjXF0u2HTUClXuaw{)n+n zFauhk34Q@-(t=q06W|OuO}}@*Ciq6ey6XuiNL2TvPN$`7LkSWns100>Mx)_m zGU;bIVKf@=d%fN_m=}8p((oR7lscrmCg#i~BaLJl)^o#8?7$^*Z_QnMWE# zkmAfT68LsF9DaybWK!Q$DwWvoxhsm|t|UqAE;WLngD;l|Y(GZX z*if;zQmNESe64}TKYi?wSH^nKS~{J6G76Vi43Ee2wp=dz5O)Pwczui^^f=@nZ1-F) zcPA2wJmw`elKr*I<=WyUHH(Wm2`+yR27_g)=-brg8&usj{Wzq!WT1|G4Wgd&oPzyX zT)zdbvQ403joi2h>|g;HdpYyJ<|@;+WI6Q7@AtnVM;>)L)o3(+;^Q-@g1sJbBtquq z%m9^IEouX`MF@pL2Q{f)ukW&tjuYV6&(vL_d_La>`dOM?Kw}kg*!n<%q>-I@`~2ss zG+8ShM=d1u3;0S&Dos+5`oKOz?(Na5YERms=^w?ZpsAo?#~%R(00fM{sqz{yw*UYD M07*qoM6N<$f{*2V{Qv*} literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_search.png b/java/res/drawable-mdpi/sym_bkeyboard_search.png new file mode 100644 index 0000000000000000000000000000000000000000..1f180155de4ec28736757f86cb34aa322e1a6cb9 GIT binary patch literal 1042 zcmV+t1nv8YP)_S}H zht(+Z!nAU^{!ct7PRH4%XAJ`n{5b46@6P*wpZ9(K@4E?+NMzk=x>V~{*|jDp)}>J} z5S3&=v=D#rJ(4Uz0=L(JbVkeZFqi|ge4l55|4fjYh3J717H8wWh#&>rEJr7K=AbLp_IBd7upGZ5as;YWCHa1p|XAW=zR5v#_cTjeunWC)H zfPgj;g6Jg$SQF=^V%%=`L&Q&k5|9r})D9dOA0PKfML8UfM~Ls`X;&;L1C-X()cC~? zXxb2S@&ZriEDF3%jNoWPL&JSp`(q$GUO|6z>J*THQEwIl6%`dPaC^h$O_^BW&Z)eo z!P};$rUzpCsi~@f-Mp%)FO*osbAYH2Sd7 zXq*-U!C)|lM;omZH9t$^%t+r8NrWJphFOcn@=6T&{eCNMdFyrL;N%-r{~~ge}Dfqxp@u@4BW)CgTTOBkf7A5)2PJBi=_S{mNXe8Ar&k_?mam< zd4tWf7;IvD5R?c~^XN)_KHn9&s92CF65H0+c8RxFVmbPU|d|C?Sqt9pb?(17racgcWvocOM7@0tGk^2@n#3M+ICi*K=w+ zF)>kMHk+?XUjk8I+uPf3Ih{^&{vaD^c#$P6<7lAb-JjON7O;b-VGpo^Ldm{nk9IoX zOnLddL|NCYWXj|S{30Fick6laYkAGl9#@E+-nEpd&d$!uXtSN$Di#z^A#9}60H*ZZ zQli)n^vMtuQxsKSvIYGi3Ps=+MNxKO0~eDyWzj?x4-O7GDl02}Jv}`xeBQ~$p;<}U zE4}Cxkj9&DDsS2|e2iIC_Ck{W{SbfU3M^oOS!{Z1f5-f9f_@4x04GMJTf2sK^VBXl9jaV%jhPQBBG*l3QB>55M*8Em4pVVAR1I4L>JL*c)b;c;0VRYoZIpy z%cfh+t^Z@sVjHIIY?>E7@Z+5Keb0H{|DNZ0KhA`3IGnbTOqRC6Y*Wx`CZ^~|349GP z;r}Hl9jL$-?#mD6K|rb~S(2V1AsV~gZmg-PX~(@D30u%xZg(o6re0=-{>Mk@lH$R~|64lq& zcMEmpU)?d9YR5<-JDG&IB~uPiFrPnb-mB~g_0 zD4zsn+%6;|hz8k4Zg)DJS1Dg;kk;1L=cqda_ORy^>_rt%c{8{YdERU`zef2ePlgqj zgVhAlKqYYdxk%?#RaI|Mcb5jFnZ`Mi%XvZxzYm-O*C!_@??v8gX=!;%`3G`VN^->1mO}AU} z>U25{E{gROMDHo-Mz7a@92*-O{Np;zJcLnXL9+D{6yiKe^Z9(r-rnArqz;8bVabPm ziAUHxa47nu7>(6xea8D-W}Gc&3R=BzYsb6J!$3BVqI3k4-^I+AjeH0OY}t UMiorL=>Px#07*qoM6N<$g7NjrSpWb4 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_shift_locked.png b/java/res/drawable-mdpi/sym_bkeyboard_shift_locked.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cebd060d38b74b2c504e83008194da63fca8ec GIT binary patch literal 787 zcmV+u1MK{XP)jytrO@cq zAOF;*GkxFkgh9p~XS8V#KKSq(o%#Ld`+L9N`@N@vs;YA*$sBVh%w~d&%r&J+5du(v z%I$v<|7%MBVT zkw`qn=N%ve2?%D#wjzdJ;sqPvb}$(Hz{_()GMRja?{9%M;5IADiWm<#4|XGw$UChV zkH^y`ipAn1+}{Q(!1b>L2|Qz3j-7Nm{X#2dSXiM@*vE5QV9AUqvtm}k&1^RNWR#cz zKA*2ytJNOh`K#bGa86T@i`~DC4ogL`66ljy(>H1X1S;l}hEFRWSonsnm0fbptFPx1Wpz zk&CZjhOZ_OGa!@6Jj8hGKpgeXaZ-oUY&OqKBa=584YyH_6AUUw8i2&eU==KKLR|sp z1VPx->lBN{-j+(G*SP!yx}0pk0b2I2pauF?vV9Ny0EgfUKTk<>4O}zmODW|eznsdE zKj0UmSS>#k%^+H%!IwsLYPDKF(R^lFcGN@U?ruJxe=LfkXZWVy?|;u;RqQIzXxYSE zF1M?9GZYH-qS5FZD>ad8f=bg{uh%#9ZUzE@bq)x@u8Psg)uMiAy4?~Ba@ZwE2#>j@7TGi=@@9n(J~ig@yI3>?2|J@5fBgg!Wf}0RYS{?%)ZY R>IDD*002ovPDHLkV1l1CZ1Dg9 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_space.png b/java/res/drawable-mdpi/sym_bkeyboard_space.png new file mode 100644 index 0000000000000000000000000000000000000000..4da7ee86eb85c43ec31a1ef0b24c91b73df0c0af GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8n!3HGFBdc41lw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6H#21Z*?7srr_TW@AKdL4F;B}KmL7Sa$RPUyy?ZG`VTXn@b9TP`9%K2jAfZ>%a^=-a%71E z7b-ez^7#tS>*obWW;O4BkZ158CT3qy}8)% znWxV2pvPMSSf9#PId_|05uK#6^XP_$mv=ZQ%v{E69A#L2u=*?0rs5UaM+)~GDqxR4 zc-uii`}~ofuru3g8(pje%O^em=^QBX-FesYRjYp1?ck~1y=LyTUF%k_3z+uw?2NT% zbDn1l$WNGX{l|~EOD45_i%#@$KAv;iYulZ}->3DYe@{NepLaGa_T5_f=E?{B+y3n4 zKgFJMqi>qRX2I|;vf|FipZ)l0o$L0!z3)YM10%x>uf%_GSNP8XLz}_V)z4*}Q$iB} DhOWNL literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_tab.png b/java/res/drawable-mdpi/sym_bkeyboard_tab.png new file mode 100644 index 0000000000000000000000000000000000000000..2cb991cbf1918f6ec7604af8c6881a19e98ab6cf GIT binary patch literal 627 zcmV-(0*w8MP)yK?#-O?XP znjC~uED1RWDipB^xg{4tO3pb-#X>JRi=;O}z2?|Jh*|=<$G;=qEHQ^9hnSz6=x)Cg zMp%oh5uT(e9r)mTE6n%3c{8(bg=JZ;QJUf!vHcf#d;yOnKW^Y0xAds z9o&L*&?Es_AsPddLWqNCH2MwWm%JlsXW#?RX`t(R06+eYZ$;u6JiCvTs`^+^`RlSs zVca}#GnOm?H^(->LRa7d7_thDQt@>x7F$mw5??V6gK>_%@(=-GD;Tv0wDPH1t+re$ zmC|2en9qfk%ilRvYCi#$SMoNK$t-G` z=10+VtqAF-dA;5)CzVQNA(cuU=kxiWPGR!c3gs1i?hl)Wg+k%8X_{^9X#Y{WDK)55 zyq`Y}3Q;zjU8>jX)1Buy>urQ+*H$0hMlXHf27d4cyaTgf?hihKwNNN@$i-!))9D>N zTL2%x3O$R*<3BK-2SM`fu~j7%&JFFvA#{{Qq6MQ#GMU`U<#L-C9&iTggDTmwm47dz zR6GiY!+ViP#K58MVEzl7fC;CTF-)tVmT*$7RyTUI3R6ZeYO}-ZQ8hc0Y5kMemNsXg z%P@>vt}9cf069FPL9^MUbGqyZ2uZhV&EeY8k|p4H@e)t|`{1_#0|2h1)~3BmGjad` N002ovPDHLkV1k4b6GQ+2 literal 0 HcmV?d00001 diff --git a/java/res/drawable-mdpi/sym_bkeyboard_tabprev.png b/java/res/drawable-mdpi/sym_bkeyboard_tabprev.png new file mode 100644 index 0000000000000000000000000000000000000000..5298291d5f7127aed9b30267f96c04f856184339 GIT binary patch literal 605 zcmV-j0;2tiP)Mihb;h4>p39|Z1$79zBcR{e@12wIfkL}V(CV@}_Zb72Y}#f((Y zfscFd+&kyH=W);6k#t?R4N_#=faRay=Lv=kMARP!B%ldsz?+o>PGAxk0m{HDP_xQK zwDTFS*LxsI(lz??z?hXWpmxILayfZC#gN5;<<>M! zTUAx{!zssyORv>x)p$I9f#EGsYF7gKq5+e@ir?=);kA3=aQK9G`fWg#foanHW9Oin z$z;xCS+=Ls=`Hcl + + + + + + + + + + diff --git a/java/res/drawable/btn_keyboard_key2.xml b/java/res/drawable/btn_keyboard_key2.xml new file mode 100644 index 000000000..bd745b76e --- /dev/null +++ b/java/res/drawable/btn_keyboard_key2.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/java/res/drawable/btn_keyboard_key3.xml b/java/res/drawable/btn_keyboard_key3.xml new file mode 100644 index 000000000..dbe82d5fd --- /dev/null +++ b/java/res/drawable/btn_keyboard_key3.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/java/res/drawable/btn_keyboard_key_stone.xml b/java/res/drawable/btn_keyboard_key_stone.xml new file mode 100644 index 000000000..a6040a04e --- /dev/null +++ b/java/res/drawable/btn_keyboard_key_stone.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + diff --git a/java/res/drawable/keyboard_key_feedback.xml b/java/res/drawable/keyboard_key_feedback.xml new file mode 100644 index 000000000..159ba8686 --- /dev/null +++ b/java/res/drawable/keyboard_key_feedback.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/java/res/layout/input.xml b/java/res/layout/input_basic.xml similarity index 83% rename from java/res/layout/input.xml rename to java/res/layout/input_basic.xml index 1d7c6f746..168eba691 100755 --- a/java/res/layout/input.xml +++ b/java/res/layout/input_basic.xml @@ -20,10 +20,12 @@ diff --git a/java/res/layout/input_basic_highcontrast.xml b/java/res/layout/input_basic_highcontrast.xml new file mode 100755 index 000000000..19ff1db11 --- /dev/null +++ b/java/res/layout/input_basic_highcontrast.xml @@ -0,0 +1,32 @@ + + + + diff --git a/java/res/layout/input_stone_bold.xml b/java/res/layout/input_stone_bold.xml new file mode 100755 index 000000000..e3588bb22 --- /dev/null +++ b/java/res/layout/input_stone_bold.xml @@ -0,0 +1,37 @@ + + + + diff --git a/java/res/layout/input_stone_normal.xml b/java/res/layout/input_stone_normal.xml new file mode 100755 index 000000000..fd7bf85fc --- /dev/null +++ b/java/res/layout/input_stone_normal.xml @@ -0,0 +1,35 @@ + + + + diff --git a/java/res/layout/input_stone_popup.xml b/java/res/layout/input_stone_popup.xml new file mode 100755 index 000000000..1efa56c5e --- /dev/null +++ b/java/res/layout/input_stone_popup.xml @@ -0,0 +1,50 @@ + + + + + + + diff --git a/java/res/layout/input_trans.xml b/java/res/layout/input_trans.xml index 94806f7e3..4c0979c04 100755 --- a/java/res/layout/input_trans.xml +++ b/java/res/layout/input_trans.xml @@ -20,11 +20,13 @@ diff --git a/java/res/layout/keyboard_key_preview.xml b/java/res/layout/keyboard_key_preview.xml new file mode 100644 index 000000000..64eaa6579 --- /dev/null +++ b/java/res/layout/keyboard_key_preview.xml @@ -0,0 +1,29 @@ + + + + diff --git a/java/res/layout/keyboard_popup_keyboard.xml b/java/res/layout/keyboard_popup_keyboard.xml new file mode 100644 index 000000000..a1b571ace --- /dev/null +++ b/java/res/layout/keyboard_popup_keyboard.xml @@ -0,0 +1,47 @@ + + + + + + diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml index 4687406f3..798d0807e 100644 --- a/java/res/values-cs/strings.xml +++ b/java/res/values-cs/strings.xml @@ -24,6 +24,8 @@ "Nastavení klávesnice Android" "Při stisku klávesy vibrovat" "Zvuk při stisku klávesy" + + "Opravovat překlepy" "Povolit opravu chyb vstupu" "Chyby vstupu v zobrazení na šířku" @@ -46,6 +48,8 @@ "Zobrazovat navržená slova během psaní" "Automatické dokončování" "Stisknutím mezerníku nebo interpunkčního znaménka automaticky vložíte zvýrazněné slovo." + "Návrh Bigram" + "Použít předchozí slovo ke zlepšení návrhu" "Žádný" "Základní" @@ -128,4 +132,12 @@ "Jazyk můžete změnit posunutím prstu po mezerníku." "← Uložte slovo opětovným klepnutím" "K dispozici je slovník" + + + + + + + + diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml index 7a2c59f76..9c525a3e0 100644 --- a/java/res/values-da/strings.xml +++ b/java/res/values-da/strings.xml @@ -24,6 +24,8 @@ "Indstillinger for Android-tastatur" "Vibration ved tastetryk" "Lyd ved tastetryk" + + "Ret stavefejl" "Aktiver fejlretning af input" "Inputfejl i landskab" @@ -46,6 +48,8 @@ "Vis ordforslag under indtastning" "Udfyld automatisk" "Mellemrumstast og tegnsætning indsætter automatisk fremhævet ord" + "Bigram-forslag" + "Brug forrige ord for at forbedre forslag" "Ingen" "Grundlæggende" @@ -128,4 +132,12 @@ "Træk fingeren på mellemrumstasten for at skifte sprog" "← Tast igen for at gemme" "Ordbog er tilgængelig" + + + + + + + + diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml index 9295f2332..047b11420 100644 --- a/java/res/values-de/strings.xml +++ b/java/res/values-de/strings.xml @@ -24,6 +24,8 @@ "Android-Tastatureinstellungen" "Vibrieren b. Tastendruck" "Ton bei Tastendruck" + + "Eingabefehler korrigieren" "Korrektur von Eingabefehlern aktivieren" "Eingabefehler im Querformat" @@ -46,6 +48,8 @@ "Vorgeschlagene Wörter während des Tippens anzeigen" "Autom. vervollständigen" "Leertaste und Interpunktion fügen autom. ein markiertes Wort ein" + "Bigramm-Vorschläge" + "Zur Verbesserung des Vorschlags vorheriges Wort verwenden" "Kein" "Standard" @@ -128,4 +132,12 @@ "Finger über die Leertaste bewegen, um die Eingabesprache zu wechseln" "← Zum Speichern erneut tippen" "Wörterbuch verfügbar" + + + + + + + + diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml index 8c78e5cf1..ef79ea097 100644 --- a/java/res/values-el/strings.xml +++ b/java/res/values-el/strings.xml @@ -24,6 +24,8 @@ "Ρυθμίσεις πληκτρολογίου Android" "Δόνηση κατά το πάτημα πλήκτρων" "Ήχος κατά το πάτημα πλήκτρων" + + "Διόρθωση σφαλμάτων πληκτρολόγησης" "Ενεργοποίηση διόρθωσης σφαλμάτων εισόδου" "Σφάλματα οριζόντιας εισαγωγής" @@ -46,6 +48,8 @@ "Προβολή προτεινόμενων λέξεων κατά την πληκτρολόγηση" "Αυτόματη συμπλήρωση" "Τα πλήκ.διαστήμ.και τονισμού εισάγ.αυτόμ.την επιλ.λέξη" + "Προτάσεις bigram" + "Χρήση προηγούμενης λέξης για τη βελτίωση πρότασης" "Καμία" "Βασική" @@ -128,4 +132,12 @@ "Σύρετε το δάχτυλο στο πλήκτρο διαστήματος για να αλλάξετε γλώσσα" "← Πατήστε ξανά για αποθήκευση" "Λεξικό διαθέσιμο" + + + + + + + + diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml index 4fd2e10ce..8cf11dfa2 100644 --- a/java/res/values-es-rUS/strings.xml +++ b/java/res/values-es-rUS/strings.xml @@ -24,6 +24,8 @@ "Configuración de teclado de Android" "Vibrar al pulsar teclas" "Sonar al pulsar las teclas" + + "Corregir errores de escritura" "Habilitar corrección de error de entrada" "Errores de entrada apaisada" @@ -46,6 +48,8 @@ "Mostrar palabras sugeridas mientras escribe" "Completar automát." "La barra espaciadora o la puntuación insertan automáticamente la palabra resaltada." + "Sugerencias de Vigoran" + "Utiliza la palabra anterior para mejorar la sugerencia" "Ninguno" "Básico" @@ -128,4 +132,12 @@ "Deslizarse manualmente por la barra espaciadora para cambiar el idioma" "← Presionar nuevamente para guardar" "Diccionario disponible" + + + + + + + + diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml index 7789a39d3..6d3823918 100644 --- a/java/res/values-es/strings.xml +++ b/java/res/values-es/strings.xml @@ -24,6 +24,8 @@ "Ajustes del teclado de Android" "Vibrar al pulsar tecla" "Sonido al pulsar tecla" + + "Corregir errores de escritura" "Habilitar la introducción de corrección de errores" "Errores de introducción de datos en vista horizontal" @@ -46,6 +48,8 @@ "Muestra las palabras sugeridas mientras se escribe." "Autocompletar" "La barra espaciadora y los signos de puntuación insertan automáticamente la palabra resaltada." + "Sugerencias de bigramas" + "Usar palabra anterior para mejorar sugerencias" "Ninguno" "Básico" @@ -128,4 +132,12 @@ "Deslizar el dedo por la barra espaciadora para cambiar el idioma" "← Volver a tocar para guardar" "Hay un diccionario disponible." + + + + + + + + diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml index 5589c677f..544789b2d 100644 --- a/java/res/values-fr/strings.xml +++ b/java/res/values-fr/strings.xml @@ -24,6 +24,8 @@ "Paramètres du clavier Android" "Vibrer à chaque touche" "Son à chaque touche" + + "Corriger les fautes de frappe" "Activer la correction des erreurs de saisie" "Erreurs de saisie en mode paysage" @@ -46,6 +48,8 @@ "Afficher les suggestions de terme lors de la saisie" "Saisie semi-automatique" "Insérer auto. le terme surligné avec barre espace/ponctuation" + "Suggestions de type bigramme" + "Améliorer la suggestion en fonction du mot précédent" "Aucun" "Simple" @@ -128,4 +132,12 @@ "Faites glisser votre doigt sur la barre d\'espacement pour changer la langue." "← Appuyer de nouveau pour enregistrer" "Dictionnaire disponible" + + + + + + + + diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml index 7c4ffbeb0..486a60ef8 100644 --- a/java/res/values-it/strings.xml +++ b/java/res/values-it/strings.xml @@ -24,6 +24,8 @@ "Impostazioni tastiera Android" "Vibrazione tasti" "Suono tasti" + + "Correggi errori di digitazione" "Attiva la correzione degli errori di inserimento" "Errori di inserimento in visualizzazione orizzontale" @@ -46,6 +48,8 @@ "Visualizza le parole suggerite durante la digitazione" "Completamento autom." "Barra spaziatrice e punteggiatura inseriscono la parola evidenziata" + "Suggerimenti sui bigrammi" + "Utilizza parola precedente per migliorare il suggerimento" "Nessuna" "Base" @@ -128,4 +132,12 @@ "Scorri il dito sulla barra spaziatrice per cambiare la lingua" "← Tocca di nuovo per salvare" "Dizionario disponibile" + + + + + + + + diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml index 1412c854f..cfa5a9746 100644 --- a/java/res/values-ja/strings.xml +++ b/java/res/values-ja/strings.xml @@ -24,6 +24,8 @@ "Androidキーボードの設定" "キー操作バイブ" "キー操作音" + + "入力ミス補正" "入力間違いを自動修正する" "横表示での入力修正" @@ -46,6 +48,8 @@ "入力時に入力候補を表示する" "オートコンプリート" "反転表示されている変換候補をスペースまたは句読点キーで挿入する" + "バイグラム入力候補表示" + "直前の単語から入力候補を予測します" "なし" "基本" @@ -128,4 +132,12 @@ "スペースバーで指をスライドさせて言語を変更する" "←保存するにはもう一度タップ" "辞書を利用できます" + + + + + + + + diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml index a574a6b8d..8fd4e63e8 100644 --- a/java/res/values-ko/strings.xml +++ b/java/res/values-ko/strings.xml @@ -24,6 +24,8 @@ "Android 키보드 설정" "키를 누를 때 진동 발생" "키를 누를 때 소리 발생" + + "입력 오류 수정" "입력 오류 수정 사용" "가로 입력 오류" @@ -46,6 +48,8 @@ "글자를 입력하는 동안 추천 단어를 표시" "자동 완성" "스페이스바와 문장부호 키로 강조 표시된 단어를 자동 삽입" + "Bigram 추천" + "이전 단어를 사용하여 추천 기능 개선" "없음" "기본" @@ -128,4 +132,12 @@ "손가락을 스페이스바에서 미끄러지듯 움직여 언어 변경" "← 저장하려면 다시 누르세요." "사전 사용 가능" + + + + + + + + diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml index 60089941b..7fbac9bdd 100644 --- a/java/res/values-nb/strings.xml +++ b/java/res/values-nb/strings.xml @@ -24,6 +24,8 @@ "Innstillinger for skjermtastatur" "Vibrer ved tastetrykk" "Lyd ved tastetrykk" + + "Rett opp skrivefeil" "Slå på retting av skrivefeil" "Rett opp skrivefeil i breddeformat" @@ -46,6 +48,8 @@ "Vis foreslåtte ord under skriving" "Autofullføring" "Mellomrom og punktum setter automatisk inn valgt ord" + "Bigram-forslag" + "Bruk forrige ord til å forbedre forslaget" "Ingen" "Grunnleggende" @@ -128,4 +132,12 @@ "Dra fingeren på mellomromstasten for å endre språk" "← Trykk på nytt for å lagre" "Ordbok tilgjengelig" + + + + + + + + diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml index 27147b8fe..b4b0ab29f 100644 --- a/java/res/values-nl/strings.xml +++ b/java/res/values-nl/strings.xml @@ -24,6 +24,8 @@ "Instellingen voor Android-toetsenbord" "Trillen bij druk op toets" "Geluid bij druk op een toets" + + "Typefouten corrigeren" "Foutcorrectie tijdens invoer inschakelen" "Invoerfouten in liggende weergave" @@ -46,6 +48,8 @@ "Voorgestelde woorden weergeven tijdens typen" "Auto-aanvullen" "Gemarkeerd woord automatisch invoegen met spatiebalk en interpunctie" + "Digram-suggesties" + "Vorig woord gebruiken om suggestie te verbeteren" "Geen" "Basis" @@ -128,4 +132,12 @@ "Schuif uw vinger over de spatiebalk om de taal te wijzigen" "← Tik nogmaals om op te slaan" "Woordenboek beschikbaar" + + + + + + + + diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml index 1d42efd98..8ca1650cd 100644 --- a/java/res/values-pl/strings.xml +++ b/java/res/values-pl/strings.xml @@ -24,6 +24,8 @@ "Ustawienia klawiatury Android" "Wibracja przy naciśnięciu" "Dźwięk przy naciśnięciu" + + "Popraw błędy pisowni" "Włącz poprawianie błędów wprowadzania" "Błędy wprowadzania w orientacji poziomej" @@ -46,6 +48,8 @@ "Wyświetl sugerowane słowa podczas wpisywania" "Autouzupełnianie" "Spacja i znaki przestankowe wstawiają wyróżnione słowo" + "Sugestie dla bigramów" + "Używaj poprzedniego wyrazu, aby polepszyć sugestię" "Brak" "Podstawowy" @@ -128,4 +132,12 @@ "Przesuń palcem po spacji, aby zmienić język" "← Dotknij ponownie, aby zapisać" "Słownik dostępny" + + + + + + + + diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml index fac0e2dc0..5fbff0fe7 100644 --- a/java/res/values-pt-rPT/strings.xml +++ b/java/res/values-pt-rPT/strings.xml @@ -24,6 +24,8 @@ "Definições de teclado do Android" "Vibrar ao primir as teclas" "Som ao premir as teclas" + + "Corrigir erros de escrita" "Activar a correcção de erros de entrada" "Erros de entrada na horizontal" @@ -46,6 +48,8 @@ "Apresentar sugestões de palavras ao escrever" "Conclusão automática" "A barra de espaços e a pontuação inserem automaticamente uma palavra realçada" + "Sugestões Bigram" + "Utilizar a palavra anterior para melhorar a sugestão" "Nenhum" "Básico" @@ -128,4 +132,12 @@ "Deslize o dedo pela barra de espaço para alterar o idioma" "← Toque novamente para guardar" "Dicionário disponível" + + + + + + + + diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml index ceab82a31..70288efd4 100644 --- a/java/res/values-pt/strings.xml +++ b/java/res/values-pt/strings.xml @@ -24,6 +24,8 @@ "Configurações de teclado Android" "Vibrar ao tocar a tecla" "Som ao tocar a tecla" + + "Corrigir erros de digitação" "Ativar a correção de erro de entrada" "Erros de entrada de paisagem" @@ -46,6 +48,8 @@ "Exibir sugestões de palavras durante a digitação" "Conclusão automática" "Barra de espaço e pontuação inserem a palavra destacada" + "Sugestões de bigrama" + "Usar palavra anterior para melhorar a sugestão" "Nenhum" "Básico" @@ -128,4 +132,12 @@ "Deslize o dedo na barra de espaços para alterar o idioma" "← Toque novamente para salvar" "Dicionário disponível" + + + + + + + + diff --git a/java/res/values-rm/strings.xml b/java/res/values-rm/strings.xml new file mode 100644 index 000000000..c32a2efa9 --- /dev/null +++ b/java/res/values-rm/strings.xml @@ -0,0 +1,145 @@ + + + + + "Tastatura Android" + "Parameters da la tastatura Android" + "Vibrar cun smatgar in buttun" + "Tun cun smatgar in buttun" + + + "Curreger sbagls d\'endataziun" + "Activar la correctura da sbagls d\'endataziun" + "Sbagls d\'endataziun en il format orizontal" + "Activar la correctura da sbagls d\'endataziun" + "Propostas da pleds" + "Curreger automaticamain il pled precedent" + "Propostas da pleds" + "Parameters da las propostas per pleds" + "Activar la cumplettaziun automatica durant l\'endataziun" + "Cumplettaziun automatica" + "Engrondir il champ da text" + "Zuppentar propostas da pleds en il format orizontal" + "Maiusclas automaticas" + "Scriver grond l\'entschatta da mintga frasa" + "Interpuncziun automatica" + + "Correcturas sveltas" + "Curregia sbagls da tippar currents" + "Mussar las propostas" + "Mussar pleds proponids durant l\'endataziun" + "Cumplettaziun automatica" + "Inserir auto. il pled marcà cun la tasta da vid/interpuncziun" + + + + + + "Nagin" + "Simpel" + "Avanzà" + + "%s : Memorisà" + "àáâãäåæ" + "èéêë" + "ìíîï" + "òóöôõœø" + "ùúûü" + "§ß" + "ñ" + "ç" + "ýÿ" + "Tegnair smatgà per mussar ils accents (à, é, etc.)" + "Smatgar ↶ per serrar la tastatura" + "Acceder a cifras e simbols" + "Smatgar ditg sin il pled dal tut a sanestra per l\'agiuntar al dicziunari" + "Tutgar quest commentari per cuntinuar »" + "Tutgar qua, per serrar quest commentari e cumenzar a tippar!" + "La tastatura vegn adina averta sche Vus tutgais in champ da text." + "Tegnai smatgà ina tasta per mussar ils segns spezials"\n"(ø, ö, ô, ó etc.)." + "Midai a numers e simbols cun tutgar quest buttun." + "Turnai a letras cun smatgar danovamain quest buttun." + "Tegnai smatgà quest buttun per midar ils parameters da tastatura, sco p. ex. la cumplettaziun automatica." + "Empruvai!" + "Dai" + "Vinavant" + "Finì" + "Trametter" + "?123" + "123" + "ABC" + "ALT" + "Cumonds vocals" + "Cumonds vocals en Vossa lingua na vegnan actualmain betg sustegnids, ma la funcziun è disponibla per englais." + "Ils cumonds vocals èn ina funcziunalitad experimentala che utilisescha la renconuschientscha vocala da rait da Google." + "Per deactivar ils cumonds vocals, avri ils parameters da tastatura." + "Per utilisar ils cumonds vocals, smatgai il buttun dal microfon u stritgai cun il det sur la tastatura dal visur." + "Ussa discurrer" + "Operaziun en progress" + + "Errur. Empruvai anc ina giada." + "Impussibel da connectar." + "Errur - discurrì memia ditg." + "Problem audio" + "Errur dal server" + "Betg udì ina frasa vocala" + "Betg chattà correspundenzas" + "Betg installà la tschertga vocala" + "Commentari:"" Stritgai cun il det sur la tastatura per discurrer." + "Commentari:"" Empruvai la proxima giada d\'agiuntar segns d\'interpuncziun sco \"punct\", \"comma\" u \"segn da dumonda\" cun cumonds vocals." + "Interrumper" + "OK" + "Cumonds vocals" + + "Sin la tastatura principala" + "Sin la tastatura da simbols" + "Deactivà" + + + "Microfon sin la tastatura principala" + "Microfon sin la tastatura da simbols" + "Ils cumonds vocals èn deactivads" + + "Trametter automaticamain suenter il cumond vocal" + "Smatgai sin la tasta enter sche Vus exequis ina tschertga u siglis al proxim champ." + "Avrir la tastatura"\n\n"Tutgai inqual champ da text." + "Serrar la tastatura"\n\n"Smatgai il buttun \"Enavos\"." + "Tutgar e tegnair smatgà in buttun per acceder a las opziuns"\n\n"Accedi a segns d\'interpuncziun ed accents." + "Parameters da tastatura"\n\n"Tutgai e tegnai smatgà il buttun ""?123""." + ".com" + ".net" + ".org" + ".gov" + ".edu" + "Metoda d\'endataziun" + "Linguas da cumonds vocals" + "Stritgar cun il det sur la tasta da vid per midar la lingua" + "← Tippar danovamain per memorisar" + "Dicziunari disponibel" + + + + + + + + + diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml index 9e5c59b58..1898d27eb 100644 --- a/java/res/values-ru/strings.xml +++ b/java/res/values-ru/strings.xml @@ -24,6 +24,8 @@ "Настройки клавиатуры Android" "Виброотклик клавиш" "Звук клавиш" + + "Исправлять опечатки" "Включить исправление ошибок при вводе" "Ошибки при вводе в горизонтальной ориентации" @@ -46,6 +48,8 @@ "Предлагать варианты слов во время ввода" "Автозавершение" "При нажатии пробела вставлять предложенное слово" + "Биграммные подсказки " + "Используйте предыдущее слово, чтобы исправить подсказку" "Нет" "Основной" @@ -128,4 +132,12 @@ "Для изменения языка проведите пальцем по пробелу" "← Нажмите повторно, чтобы сохранить" "Доступен словарь" + + + + + + + + diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml new file mode 100644 index 000000000..f706ebc3d --- /dev/null +++ b/java/res/values-sr/strings.xml @@ -0,0 +1,299 @@ + + + + + Андроидова тастатура + + Подешавања андроидове тастатуре + + + Вибрације при притиску + + Звук при притиску + + + Исправљање грешака + + + Исправљање грешака при уносу + + + Грешке при водоравној оријентацији + + + Исправљање грешака при уносу при + водоравном положају + + + Предлози речи + + + Аутоматска исправка претходно унесене речи + + + Предлози речи + + Подешавања за предлоге речи + + Укључи аутоматске наставке при уносу + + + Аутоматски наставци + + + Увећан поље за унос текста + + Сакриј предлоге речи при водоравном положају + + + Аутоматска величина слова + + Велико слово на почетку реченице + + Аутоматска интерпункција + + Аутоматско постављање интерпункцијских знака при уносу. + + + Брзе исправке + + Аутоматска исправка честих грешака + + + Приказ предлога + + Приказује предлоге речи током уноса + + + Аутоматска допуна + + Размакница и интерпункција аутоматски убацују означену реч. + + + + Искључено + Основно + Напредно + + + + @string/prediction_none + @string/prediction_basic + @string/prediction_full + + + + %s : Saved + + Дуги притисак на тастере открива проширене знаке (ø, ö, итд.) + + Притисните тастер за назад \u21B6 како бисте затворили тастатуру + + Приступ бројевима и симболима + + Притисните и држите притиснуту реч са крајње леве стране + како бисте је додали у речник + + + Притисните овај подсетник да наставите » + + + Притисните овде да бисте затворили подсетник и наставили унос! + + + Тастатура се отвара кад год је потребно да унесете текст + + + Притисните и држите тастер како бисте видели проширене + знаке\n(„, ‟, итд.) + + + + Пребаците се на бројеве и симболе притиском на овај тастер + + + + Вратите се назад на слова притиском на овај тастер + + + Притисните и држите притиснут овај тастер да бисте променили + подешавања тастатуре, попут аутоматског настављања + + + Пробајте сами! + + + + Иди + + Даље + + Крај + + Шаљи + + \?123 + + 123 + + АБВ + + ALT + + + + + Говорни унос + + + Говорни унос није тренутно подржан на Вашем језику, + али ради на енглеском. + + + Говорни унос је експериментална могућност која користи + Google-ово мрежно препознавање говора. + + + Како бисте искључили говорни унос, изаберите подешавања + тастатуре. + + + Како бисте укључили говорни унос, притисните дугме са сличицом + микрофона или превуците прстом преко целе дужине тастатуре. + + + Говорите сада + + + Обрада је у току + + + + + + Грешка. Молимо пробајте поново. + + + Повезивање није успело + + + Грешка, говор је предугачак. + + + Проблем са звуком + + + Грешка на серверу + + + Говор није снимљен + + + Нема погодака + + + Говорна претрага није инсталирана + + + Савет: Превуците прстом преко тастатуре а онда говорите. + + + Савет: Следећи пут, изговорите назив интерпункције, + попут „тачка“, „запета“ или „знак питања“. + + + Откажи + + + У реду + + + Говорни унос + + + + На главној тастатури + На симболичкој тастатури + Искључен + + + + @string/voice_mode_main + @string/voice_mode_symbols + @string/voice_mode_off + + + + + Микрофон на главној тастатури + Микрофон на симболичкој тастатури + Говорни унос је искључен + + + + Аутоматско слање по говорном уносу + + + Дугме за претрагу се аутоматски притиска при претрази или преласку + на следеће поље за унос. + + + + Отварање тастатуре\n\nTouch any text field. + + + Затварање тастатуре\n\nPress the Back key. + + + Притисните \u0026 и држите пристиснут тастер за опције\n\nПриступ акцентима и интерпункцији. + + + Подешавање тастатуре\n\nПритисните \u0026 и држите тастер \?123\. + + + ".rs" + + ".com" + + ".net" + + ".org" + + ".edu" + + + Метод за унос + + + Језици за унос + + Превуците прстом по размакници за промену језика + + + \u2190 Притисните опет да бисте сачували + diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml index bb1d47cf6..49359e8b9 100644 --- a/java/res/values-sv/strings.xml +++ b/java/res/values-sv/strings.xml @@ -24,6 +24,8 @@ "Inställningar för Androids tangentbord" "Vibrera vid tangenttryck" "Knappljud" + + "Rätta skrivfel" "Aktivera rättning av felaktiga inmatningar" "Inmatningsfel i liggande vy" @@ -46,6 +48,8 @@ "Visar ordförslag när du skriver" "Komplettera automatiskt" "Blanksteg och punkt infogar automatiskt markerat ord" + "Bigramförslag" + "Förbättra förslaget med föregående ord" "Inget" "Grundinställningar" @@ -128,4 +132,12 @@ "Dra med fingret på blanksteg om du vill ändra språk" "← Peka igen för att spara" "En ordlista är tillgänglig" + + + + + + + + diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml index 9baa1bfe2..73ad111f3 100644 --- a/java/res/values-tr/strings.xml +++ b/java/res/values-tr/strings.xml @@ -24,6 +24,8 @@ "Android klavye ayarları" "Tuşa basıldığında titret" "Tuşa basıldığında ses çıkar" + + "Yazım hatalarını düzelt" "Giriş hatası düzeltmeyi etkinleştir" "Yatay giriş hataları" @@ -46,6 +48,8 @@ "Yazarken önerilen kelimeleri görüntüle" "Otomatik tamamla" "Boşluk tuşu ve noktalama vurgulanan kelimeyi otomatik ekler" + "Bigram Önerileri" + "Öneriyi geliştirmek için önceki kelimeyi kullanın" "Yok" "Temel" @@ -128,4 +132,12 @@ "Dili değiştirmek için parmağınızı boşluk çubuğu üzerinde kaydırın" "← Kaydetmek için tekrar dokunun" "Sözlük kullanılabilir" + + + + + + + + diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml index dd64bed97..046d1d664 100644 --- a/java/res/values-zh-rCN/strings.xml +++ b/java/res/values-zh-rCN/strings.xml @@ -24,6 +24,8 @@ "Android 键盘设置" "按键时振动" "按键时播放音效" + + "纠正输入错误" "启用输入错误纠正功能" "横向输入错误" @@ -46,6 +48,8 @@ "输入时启用联想提示" "自动填写" "按空格键和标点符号时自动插入突出显示的字词" + "双连词建议" + "使用以前的字词改进建议" "无" "基本模式" @@ -128,4 +132,12 @@ "在空格键上滑动手指可更改语言" "← 再次点按即可保存" "提供字典" + + + + + + + + diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml index 5c54cc884..182f71192 100644 --- a/java/res/values-zh-rTW/strings.xml +++ b/java/res/values-zh-rTW/strings.xml @@ -24,6 +24,8 @@ "Android 鍵盤設定" "按鍵時震動" "按鍵時播放音效" + + "修正輸入錯誤" "啟用輸入錯誤修正功能" "橫向輸入錯誤" @@ -46,6 +48,8 @@ "打字時顯示建議字詞" "自動完成" "在反白顯示的字詞處自動插入空白鍵和標點符號鍵盤" + "雙連詞建議" + "根據前一個字詞自動找出更適合的建議" "無" "基本模式" @@ -128,4 +132,12 @@ "以手指在空白鍵上滑動可變更語言" "← 再次輕按可儲存" "可使用字典" + + + + + + + + diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml new file mode 100644 index 000000000..e3171eb33 --- /dev/null +++ b/java/res/values/attrs.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml index ebe2f04e5..f5f2c3d0e 100644 --- a/java/res/values/bools.xml +++ b/java/res/values/bools.xml @@ -25,4 +25,7 @@ false true + true + + true diff --git a/java/res/values/colors.xml b/java/res/values/colors.xml index c90d9f6af..343a9405d 100644 --- a/java/res/values/colors.xml +++ b/java/res/values/colors.xml @@ -21,4 +21,12 @@ #FF000000 #FFE35900 #ff808080 - \ No newline at end of file + #00000000 + #80000000 + #80FFFFFF + #FF808080 + #A0000000 + #FF000000 + #FFFFFFFF + #FF000000 + diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index 5b2095c0e..39dce9db0 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -23,4 +23,9 @@ 22dip 42dip 4dip - \ No newline at end of file + + 2.5in + 22sp + 0.05in + diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml index d5017353d..b7bfd9c3a 100644 --- a/java/res/values/donottranslate.xml +++ b/java/res/values/donottranslate.xml @@ -21,9 +21,9 @@ .\u0009\u0020,;:!?\n()[]*&@{}/<>_+=|\u0022 - .,!? + .,!?) - !?,@_ + !?,\u0022\u0027:()-/@_ diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 35dd3e089..c72cba7e6 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -25,9 +25,13 @@ Vibrate on keypress + Sound on keypress - + + + Popup on keypress + Correct typing errors @@ -85,6 +89,11 @@ Spacebar and punctuation automatically insert highlighted word + + Bigram Suggestions + + Use previous word to improve suggestion + None @@ -322,4 +331,34 @@ Dictionary available + + + Enable user feedback + + Help improve this input method editor by automatically sending usage statistics and crash reports to Google. + + Keyboard Theme + Basic + Basic (High Contrast) + Default (bold) + Default (normal) + + + @string/layout_basic + @string/layout_high_contrast + @string/layout_stone_normal + @string/layout_stone_bold + + + + 0 + 1 + 2 + 3 + + + Debug (Temporary) + + keyboard + voice diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml new file mode 100644 index 000000000..24fee02d8 --- /dev/null +++ b/java/res/values/styles.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/java/res/xml-da/kbd_qwerty.xml b/java/res/xml-da/kbd_qwerty.xml new file mode 100644 index 000000000..472f8be55 --- /dev/null +++ b/java/res/xml-da/kbd_qwerty.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-da/kbd_qwerty_black.xml b/java/res/xml-da/kbd_qwerty_black.xml new file mode 100644 index 000000000..2b41cf1fb --- /dev/null +++ b/java/res/xml-da/kbd_qwerty_black.xml @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-de/kbd_qwerty_black.xml b/java/res/xml-de/kbd_qwerty_black.xml new file mode 100755 index 000000000..366f87134 --- /dev/null +++ b/java/res/xml-de/kbd_qwerty_black.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-fr/kbd_qwerty_black.xml b/java/res/xml-fr/kbd_qwerty_black.xml new file mode 100644 index 000000000..1b799a51d --- /dev/null +++ b/java/res/xml-fr/kbd_qwerty_black.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-iw/kbd_qwerty.xml b/java/res/xml-iw/kbd_qwerty.xml new file mode 100755 index 000000000..b893f1a62 --- /dev/null +++ b/java/res/xml-iw/kbd_qwerty.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-iw/kbd_qwerty_black.xml b/java/res/xml-iw/kbd_qwerty_black.xml new file mode 100755 index 000000000..0dcf513e3 --- /dev/null +++ b/java/res/xml-iw/kbd_qwerty_black.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-nb/kbd_qwerty.xml b/java/res/xml-nb/kbd_qwerty.xml new file mode 100644 index 000000000..d2f0258c1 --- /dev/null +++ b/java/res/xml-nb/kbd_qwerty.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-nb/kbd_qwerty_black.xml b/java/res/xml-nb/kbd_qwerty_black.xml new file mode 100644 index 000000000..150ff7fc7 --- /dev/null +++ b/java/res/xml-nb/kbd_qwerty_black.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-ru/kbd_qwerty_black.xml b/java/res/xml-ru/kbd_qwerty_black.xml new file mode 100755 index 000000000..4923be01a --- /dev/null +++ b/java/res/xml-ru/kbd_qwerty_black.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-sr/kbd_qwerty.xml b/java/res/xml-sr/kbd_qwerty.xml new file mode 100644 index 000000000..e4884a8a6 --- /dev/null +++ b/java/res/xml-sr/kbd_qwerty.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-sr/kbd_qwerty_black.xml b/java/res/xml-sr/kbd_qwerty_black.xml new file mode 100644 index 000000000..30d094a88 --- /dev/null +++ b/java/res/xml-sr/kbd_qwerty_black.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml-sv/kbd_qwerty_black.xml b/java/res/xml-sv/kbd_qwerty_black.xml new file mode 100644 index 000000000..6604bc87c --- /dev/null +++ b/java/res/xml-sv/kbd_qwerty_black.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/dictionary.xml b/java/res/xml/dictionary.xml new file mode 100644 index 000000000..7b770a8b4 --- /dev/null +++ b/java/res/xml/dictionary.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/java/res/xml/kbd_alpha_black.xml b/java/res/xml/kbd_alpha_black.xml new file mode 100644 index 000000000..108e466b8 --- /dev/null +++ b/java/res/xml/kbd_alpha_black.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/kbd_phone_black.xml b/java/res/xml/kbd_phone_black.xml new file mode 100755 index 000000000..b7f9096bd --- /dev/null +++ b/java/res/xml/kbd_phone_black.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/kbd_phone_symbols_black.xml b/java/res/xml/kbd_phone_symbols_black.xml new file mode 100755 index 000000000..c73e5faa4 --- /dev/null +++ b/java/res/xml/kbd_phone_symbols_black.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/kbd_qwerty_black.xml b/java/res/xml/kbd_qwerty_black.xml new file mode 100755 index 000000000..d013ae01b --- /dev/null +++ b/java/res/xml/kbd_qwerty_black.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/kbd_symbols_black.xml b/java/res/xml/kbd_symbols_black.xml new file mode 100755 index 000000000..5652f7fca --- /dev/null +++ b/java/res/xml/kbd_symbols_black.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/kbd_symbols_shift.xml b/java/res/xml/kbd_symbols_shift.xml index 09b5c3f9d..ca431fc8c 100755 --- a/java/res/xml/kbd_symbols_shift.xml +++ b/java/res/xml/kbd_symbols_shift.xml @@ -34,7 +34,10 @@ android:popupCharacters="♪♥♠♦♣" /> - + diff --git a/java/res/xml/kbd_symbols_shift_black.xml b/java/res/xml/kbd_symbols_shift_black.xml new file mode 100755 index 000000000..a8acb9d00 --- /dev/null +++ b/java/res/xml/kbd_symbols_shift_black.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml index 535b63f3b..11cc3ac42 100644 --- a/java/res/xml/prefs.xml +++ b/java/res/xml/prefs.xml @@ -30,6 +30,13 @@ android:persistent="true" /> + + + + + + @@ -81,6 +105,21 @@ android:defaultValue="@bool/enable_autocorrect" android:dependency="show_suggestions" /> - + + + + diff --git a/java/src/com/android/inputmethod/latin/AutoDictionary.java b/java/src/com/android/inputmethod/latin/AutoDictionary.java index 93f1985ca..4fbb5b012 100644 --- a/java/src/com/android/inputmethod/latin/AutoDictionary.java +++ b/java/src/com/android/inputmethod/latin/AutoDictionary.java @@ -83,14 +83,14 @@ public class AutoDictionary extends ExpandableDictionary { sDictProjectionMap.put(COLUMN_LOCALE, COLUMN_LOCALE); } - private static DatabaseHelper mOpenHelper = null; + private static DatabaseHelper sOpenHelper = null; - public AutoDictionary(Context context, LatinIME ime, String locale) { - super(context); + public AutoDictionary(Context context, LatinIME ime, String locale, int dicTypeId) { + super(context, dicTypeId); mIme = ime; mLocale = locale; - if (mOpenHelper == null) { - mOpenHelper = new DatabaseHelper(getContext()); + if (sOpenHelper == null) { + sOpenHelper = new DatabaseHelper(getContext()); } if (mLocale != null && mLocale.length() > 1) { loadDictionary(); @@ -169,7 +169,7 @@ public class AutoDictionary extends ExpandableDictionary { // Nothing pending? Return if (mPendingWrites.isEmpty()) return; // Create a background thread to write the pending entries - new UpdateDbTask(getContext(), mOpenHelper, mPendingWrites, mLocale).execute(); + 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(); } @@ -209,7 +209,7 @@ public class AutoDictionary extends ExpandableDictionary { qb.setProjectionMap(sDictProjectionMap); // Get the database and run the query - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + SQLiteDatabase db = sOpenHelper.getReadableDatabase(); Cursor c = qb.query(db, null, selection, selectionArgs, null, null, DEFAULT_SORT_ORDER); return c; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 87de94b76..69c2b94f2 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -16,10 +16,15 @@ package com.android.inputmethod.latin; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.Channels; import java.util.Arrays; import android.content.Context; -import android.content.res.AssetManager; import android.util.Log; /** @@ -27,18 +32,33 @@ import android.util.Log; */ public class BinaryDictionary extends Dictionary { - public static final int MAX_WORD_LENGTH = 48; + /** + * There is difference between what java and native code can handle. + * This value should only be used in BinaryDictionary.java + * It is necessary to keep it at this value because some languages e.g. German have + * really long words. + */ + protected static final int MAX_WORD_LENGTH = 48; + + private static final String TAG = "BinaryDictionary"; private static final int MAX_ALTERNATIVES = 16; - private static final int MAX_WORDS = 16; + private static final int MAX_WORDS = 18; + private static final int MAX_BIGRAMS = 60; private static final int TYPED_LETTER_MULTIPLIER = 2; private static final boolean ENABLE_MISSED_CHARACTERS = true; + private int mDicTypeId; private int mNativeDict; - private int mDictLength; // This value is set from native code, don't change the name!!!! + private int mDictLength; private int[] mInputCodes = new int[MAX_WORD_LENGTH * MAX_ALTERNATIVES]; private char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS]; + private char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS]; private int[] mFrequencies = new int[MAX_WORDS]; + private int[] mFrequencies_bigrams = new int[MAX_BIGRAMS]; + // Keep a reference to the native dict direct buffer in Java to avoid + // unexpected deallocation of the direct buffer. + private ByteBuffer mNativeDictDirectBuffer; static { try { @@ -53,32 +73,122 @@ public class BinaryDictionary extends Dictionary { * @param context application context for reading resources * @param resId the resource containing the raw binary dictionary */ - public BinaryDictionary(Context context, int resId) { - if (resId != 0) { + public BinaryDictionary(Context context, int[] resId, int dicTypeId) { + if (resId != null && resId.length > 0 && resId[0] != 0) { loadDictionary(context, resId); } + mDicTypeId = dicTypeId; } - private native int openNative(AssetManager am, String resourcePath, int typedLetterMultiplier, + /** + * Create a dictionary from a byte buffer. This is used for testing. + * @param context application context for reading resources + * @param byteBuffer a ByteBuffer containing the binary dictionary + */ + public BinaryDictionary(Context context, ByteBuffer byteBuffer, int dicTypeId) { + if (byteBuffer != null) { + if (byteBuffer.isDirect()) { + mNativeDictDirectBuffer = byteBuffer; + } else { + mNativeDictDirectBuffer = ByteBuffer.allocateDirect(byteBuffer.capacity()); + byteBuffer.rewind(); + mNativeDictDirectBuffer.put(byteBuffer); + } + mDictLength = byteBuffer.capacity(); + mNativeDict = openNative(mNativeDictDirectBuffer, + TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + } + mDicTypeId = dicTypeId; + } + + private native int openNative(ByteBuffer bb, int typedLetterMultiplier, int fullWordMultiplier); private native void closeNative(int dict); private native boolean isValidWordNative(int nativeData, char[] word, int wordLength); private native int getSuggestionsNative(int dict, int[] inputCodes, int codesSize, - char[] outputChars, int[] frequencies, - int maxWordLength, int maxWords, int maxAlternatives, int skipPos, - int[] nextLettersFrequencies, int nextLettersSize); + char[] outputChars, int[] frequencies, int maxWordLength, int maxWords, + int maxAlternatives, int skipPos, int[] nextLettersFrequencies, int nextLettersSize); + private native int getBigramsNative(int dict, char[] prevWord, int prevWordLength, + int[] inputCodes, int inputCodesLength, char[] outputChars, int[] frequencies, + int maxWordLength, int maxBigrams, int maxAlternatives); - private final void loadDictionary(Context context, int resId) { - AssetManager am = context.getResources().getAssets(); - String assetName = context.getResources().getString(resId); - mNativeDict = openNative(am, assetName, TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + private final void loadDictionary(Context context, int[] resId) { + InputStream[] is = null; + try { + // merging separated dictionary into one if dictionary is separated + int total = 0; + is = new InputStream[resId.length]; + for (int i = 0; i < resId.length; i++) { + is[i] = context.getResources().openRawResource(resId[i]); + total += is[i].available(); + } + + mNativeDictDirectBuffer = + ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder()); + int got = 0; + for (int i = 0; i < resId.length; i++) { + got += Channels.newChannel(is[i]).read(mNativeDictDirectBuffer); + } + if (got != total) { + Log.e(TAG, "Read " + got + " bytes, expected " + total); + } else { + mNativeDict = openNative(mNativeDictDirectBuffer, + TYPED_LETTER_MULTIPLIER, FULL_WORD_FREQ_MULTIPLIER); + mDictLength = total; + } + } catch (IOException e) { + Log.w(TAG, "No available memory for binary dictionary"); + } finally { + try { + if (is != null) { + for (int i = 0; i < is.length; i++) { + is[i].close(); + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to close input stream"); + } + } + } + + + @Override + public void getBigrams(final WordComposer codes, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + + char[] chars = previousWord.toString().toCharArray(); + Arrays.fill(mOutputChars_bigrams, (char) 0); + Arrays.fill(mFrequencies_bigrams, 0); + + int codesSize = codes.size(); + Arrays.fill(mInputCodes, -1); + int[] alternatives = codes.getCodesAt(0); + System.arraycopy(alternatives, 0, mInputCodes, 0, + Math.min(alternatives.length, MAX_ALTERNATIVES)); + + int count = getBigramsNative(mNativeDict, chars, chars.length, mInputCodes, codesSize, + mOutputChars_bigrams, mFrequencies_bigrams, MAX_WORD_LENGTH, MAX_BIGRAMS, + MAX_ALTERNATIVES); + + for (int j = 0; j < count; j++) { + if (mFrequencies_bigrams[j] < 1) break; + int start = j * MAX_WORD_LENGTH; + int len = 0; + while (mOutputChars_bigrams[start + len] != 0) { + len++; + } + if (len > 0) { + callback.addWord(mOutputChars_bigrams, start, len, mFrequencies_bigrams[j], + mDicTypeId, DataType.BIGRAM); + } + } } @Override public void getWords(final WordComposer codes, final WordCallback callback, int[] nextLettersFrequencies) { final int codesSize = codes.size(); - // Wont deal with really long words. + // Won't deal with really long words. if (codesSize > MAX_WORD_LENGTH - 1) return; Arrays.fill(mInputCodes, -1); @@ -119,7 +229,8 @@ public class BinaryDictionary extends Dictionary { len++; } if (len > 0) { - callback.addWord(mOutputChars, start, len, mFrequencies[j]); + callback.addWord(mOutputChars, start, len, mFrequencies[j], mDicTypeId, + DataType.UNIGRAM); } } } diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index 4dc61d4a4..7fcc3d532 100755 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -83,7 +83,6 @@ public class CandidateView extends View { private int mDescent; private boolean mScrolled; private boolean mShowingAddToDictionary; - private CharSequence mWordToAddToDictionary; private CharSequence mAddToDictionaryHint; private int mTargetScrollX; @@ -144,9 +143,13 @@ public class CandidateView extends View { mPaint.setStrokeWidth(0); mPaint.setTextAlign(Align.CENTER); mDescent = (int) mPaint.descent(); - // 80 pixels for a 160dpi device would mean half an inch + // 50 pixels for a 160dpi device would mean about 0.3 inch mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50); + // Slightly reluctant to scroll to be able to easily choose the suggestion + // 50 pixels for a 160dpi device would mean about 0.3 inch + final int touchSlop = (int) (getResources().getDisplayMetrics().density * 50); + final int touchSlopSquare = touchSlop * touchSlop; mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent me) { @@ -160,6 +163,13 @@ public class CandidateView extends View { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + final int deltaX = (int) (e2.getX() - e1.getX()); + final int deltaY = (int) (e2.getY() - e1.getY()); + final int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance < touchSlopSquare) { + return false; + } + final int width = getWidth(); mScrolled = true; int scrollX = getScrollX(); @@ -167,7 +177,7 @@ public class CandidateView extends View { if (scrollX < 0) { scrollX = 0; } - if (distanceX > 0 && scrollX + width > mTotalWidth) { + if (distanceX > 0 && scrollX + width > mTotalWidth) { scrollX -= (int) distanceX; } mTargetScrollX = scrollX; @@ -219,8 +229,7 @@ public class CandidateView extends View { mDivider.getIntrinsicHeight()); } int x = 0; - final int count = mSuggestions.size(); - final int width = getWidth(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); final Rect bgPadding = mBgPadding; final Paint paint = mPaint; final int touchX = mTouchX; @@ -325,7 +334,6 @@ public class CandidateView extends View { } public void showAddToDictionaryHint(CharSequence word) { - mWordToAddToDictionary = word; ArrayList suggestions = new ArrayList(); suggestions.add(word); suggestions.add(mAddToDictionaryHint); @@ -341,7 +349,7 @@ public class CandidateView extends View { public void scrollPrev() { int i = 0; - final int count = mSuggestions.size(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int firstItem = 0; // Actually just before the first item, if at the boundary while (i < count) { if (mWordX[i] < getScrollX() @@ -360,7 +368,7 @@ public class CandidateView extends View { int i = 0; int scrollX = getScrollX(); int targetX = scrollX; - final int count = mSuggestions.size(); + final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); int rightEdge = scrollX + getWidth(); while (i < count) { if (mWordX[i] <= rightEdge && @@ -382,8 +390,14 @@ public class CandidateView extends View { mScrolled = true; } } - + + /* package */ List getSuggestions() { + return mSuggestions; + } + public void clear() { + // Don't call mSuggestions.clear() because it's being used for logging + // in LatinIME.pickSuggestionManually(). mSuggestions = EMPTY_LIST; mTouchX = OUT_OF_BOUNDS; mSelectedString = null; @@ -418,7 +432,11 @@ public class CandidateView extends View { if (y <= 0) { // Fling up!? if (mSelectedString != null) { + // If there are completions from the application, we don't change the state to + // STATE_PICKED_SUGGESTION if (!mShowingCompletions) { + // This "acceptedSuggestion" will not be counted as a word because + // it will be counted in pickSuggestion instead. TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); } @@ -453,25 +471,6 @@ public class CandidateView extends View { } return true; } - - /** - * For flick through from keyboard, call this method with the x coordinate of the flick - * gesture. - * @param x - */ - public void takeSuggestionAt(float x) { - mTouchX = (int) x; - // To detect candidate - onDraw(null); - if (mSelectedString != null) { - if (!mShowingCompletions) { - TextEntryState.acceptedSuggestion(mSuggestions.get(0), mSelectedString); - } - mService.pickSuggestionManually(mSelectedIndex, mSelectedString); - } - invalidate(); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_REMOVE_THROUGH_PREVIEW), 200); - } private void hidePreview() { mCurrentWordIndex = OUT_OF_BOUNDS; diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java index 15edb706a..ab75868cf 100644 --- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java @@ -20,9 +20,10 @@ import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; -import android.os.AsyncTask; import android.os.SystemClock; import android.provider.ContactsContract.Contacts; +import android.text.TextUtils; +import android.util.Log; public class ContactsDictionary extends ExpandableDictionary { @@ -31,27 +32,35 @@ public class ContactsDictionary extends ExpandableDictionary { Contacts.DISPLAY_NAME, }; + /** + * Frequency for contacts information into the dictionary + */ + private static final int FREQUENCY_FOR_CONTACTS = 128; + 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) { - super(context); + 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); - } - }); + 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); @@ -89,6 +98,7 @@ public class ContactsDictionary extends ExpandableDictionary { if (name != null) { int len = name.length(); + String prevWord = null; // TODO: Better tokenization for non-Latin writing systems for (int i = 0; i < len; i++) { @@ -112,7 +122,13 @@ public class ContactsDictionary extends ExpandableDictionary { // capitalization of i. final int wordLen = word.length(); if (wordLen < maxWordLength && wordLen > 1) { - super.addWord(word, 128); + super.addWord(word, FREQUENCY_FOR_CONTACTS); + if (!TextUtils.isEmpty(prevWord)) { + // TODO Do not add email address + // Not so critical + super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM); + } + prevWord = word; } } } diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index e7b526663..d04bf57a7 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -21,7 +21,6 @@ package com.android.inputmethod.latin; * strokes. */ abstract public class Dictionary { - /** * Whether or not to replicate the typed word in the suggested list, even if it's valid. */ @@ -31,7 +30,11 @@ abstract public class Dictionary { * 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_FREQ_MULTIPLIER = 2; - + + public static enum DataType { + UNIGRAM, BIGRAM + } + /** * Interface to be implemented by classes requesting words to be fetched from the dictionary. * @see #getWords(WordComposer, WordCallback) @@ -45,9 +48,12 @@ abstract public class Dictionary { * @param wordLength length of valid characters in the character array * @param frequency the frequency of occurence. 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 * @return true if the word was added, false if no more words are required */ - boolean addWord(char[] word, int wordOffset, int wordLength, int frequency); + boolean addWord(char[] word, int wordOffset, int wordLength, int frequency, int dicTypeId, + DataType dataType); } /** @@ -64,6 +70,21 @@ abstract public class Dictionary { abstract public void getWords(final WordComposer composer, final WordCallback callback, int[] nextLettersFrequencies); + /** + * Searches for pairs in the bigram dictionary that matches the previous word and all the + * possible words following are added through the callback object. + * @param composer the key sequence to match + * @param callback the callback object to send possible word following previous word + * @param nextLettersFrequencies array of frequencies of next letters that could follow the + * word so far. For instance, "bracke" can be followed by "t", so array['t'] will have + * a non-zero value on returning from this method. + * Pass in null if you don't want the dictionary to look up next letters. + */ + public void getBigrams(final WordComposer composer, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + // empty base implementation + } + /** * Checks if the given word occurs in the dictionary * @param word the word to search for. The search should be case-insensitive. diff --git a/java/src/com/android/inputmethod/voice/EditingUtil.java b/java/src/com/android/inputmethod/latin/EditingUtil.java similarity index 59% rename from java/src/com/android/inputmethod/voice/EditingUtil.java rename to java/src/com/android/inputmethod/latin/EditingUtil.java index 6316d8ccf..be31cb787 100644 --- a/java/src/com/android/inputmethod/voice/EditingUtil.java +++ b/java/src/com/android/inputmethod/latin/EditingUtil.java @@ -14,16 +14,23 @@ * the License. */ -package com.android.inputmethod.voice; +package com.android.inputmethod.latin; 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 EditingUtil { + /** + * Number of characters we want to look back in order to identify the previous word + */ + private static final int LOOKBACK_CHARACTER_NUM = 15; + private EditingUtil() {}; /** @@ -75,9 +82,21 @@ public class EditingUtil { * represents the cursor, then "hello " will be returned. */ public static String getWordAtCursor( - InputConnection connection, String separators) { - Range range = getWordRangeAtCursor(connection, separators); - return (range == null) ? null : range.word; + InputConnection connection, String separators) { + return getWordAtCursor(connection, separators, null); + } + + /** + * @param connection connection to the current text field. + * @param sep 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 range) { + Range r = getWordRangeAtCursor(connection, separators, range); + return (r == null) ? null : r.word; } /** @@ -87,7 +106,7 @@ public class EditingUtil { public static void deleteWordAtCursor( InputConnection connection, String separators) { - Range range = getWordRangeAtCursor(connection, separators); + Range range = getWordRangeAtCursor(connection, separators, null); if (range == null) return; connection.finishComposingText(); @@ -101,18 +120,20 @@ public class EditingUtil { /** * Represents a range of text, relative to the current cursor position. */ - private static class Range { + public static class Range { /** Characters before selection start */ - int charsBefore; + public int charsBefore; /** * Characters after selection start, including one trailing word * separator. */ - int charsAfter; + public int charsAfter; /** The actual characters that make up a word */ - String word; + public String word; + + public Range() {} public Range(int charsBefore, int charsAfter, String word) { if (charsBefore < 0 || charsAfter < 0) { @@ -125,7 +146,7 @@ public class EditingUtil { } private static Range getWordRangeAtCursor( - InputConnection connection, String sep) { + InputConnection connection, String sep, Range range) { if (connection == null || sep == null) { return null; } @@ -137,20 +158,22 @@ public class EditingUtil { // Find first word separator before the cursor int start = before.length(); - while (--start > 0 && !isWhitespace(before.charAt(start - 1), sep)); + 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)); - if (end < after.length() - 1) { - end++; // Include trailing space, if it exists, in word - } 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); + + after.toString().substring(0, end); + + Range returnRange = range != null? range : new Range(); + returnRange.charsBefore = before.length() - start; + returnRange.charsAfter = end; + returnRange.word = word; + return returnRange; } return null; @@ -159,4 +182,48 @@ public class EditingUtil { 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); + if (prev == null) { + return null; + } + String[] w = spaceRegex.split(prev); + if (w.length >= 2 && w[w.length-2].length() > 0) { + 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]; + } else { + return null; + } + } + + /** + * Checks if the cursor is touching/inside a word or the selection is for a whole + * word and no more and no less. + * @param range the Range object that contains the bounds of the word around the cursor + * @param start the start of the selection + * @param end the end of the selection, which could be the same as the start, if text is not + * in selection mode + * @return false if the selection is a partial word or straddling multiple words, true if + * the selection is a full word or there is no selection. + */ + public static boolean isFullWordOrInside(Range range, int start, int end) { + // Is the cursor inside or touching a word? + if (start == end) return true; + + // Is it a selection? Then is the start of the selection the start of the word and + // the size of the selection the size of the word? Then return true + if (start < end + && (range.charsBefore == 0 && range.charsAfter == end - start)) { + return true; + } + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java index 46bc41c42..e954c0818 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java @@ -16,24 +16,30 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.Dictionary.WordCallback; +import java.util.LinkedList; import android.content.Context; import android.os.AsyncTask; -import android.os.SystemClock; /** * Base class for an in-memory dictionary that can grow dynamically and can * 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; + private Context mContext; private char[] mWordBuilder = new char[MAX_WORD_LENGTH]; + private int mDicTypeId; private int mMaxDepth; private int mInputLength; private int[] mNextLettersFrequencies; + private StringBuilder sb = new StringBuilder(MAX_WORD_LENGTH); - public static final int MAX_WORD_LENGTH = 32; private static final char QUOTE = '\''; private boolean mRequiresReload; @@ -47,7 +53,9 @@ public class ExpandableDictionary extends Dictionary { char code; int frequency; boolean terminal; + Node parent; NodeArray children; + LinkedList ngrams; // Supports ngram } static class NodeArray { @@ -71,14 +79,27 @@ public class ExpandableDictionary extends Dictionary { } } + static class NextWord { + Node word; + NextWord nextWord; + int frequency; + + NextWord(Node word, int frequency) { + this.word = word; + this.frequency = frequency; + } + } + + private NodeArray mRoots; private int[][] mCodes; - ExpandableDictionary(Context context) { + ExpandableDictionary(Context context, int dicTypeId) { mContext = context; clearDictionary(); mCodes = new int[MAX_WORD_LENGTH][]; + mDicTypeId = dicTypeId; } public void loadDictionary() { @@ -118,12 +139,11 @@ public class ExpandableDictionary extends Dictionary { } public void addWord(String word, int frequency) { - addWordRec(mRoots, word, 0, frequency); + addWordRec(mRoots, word, 0, frequency, null); } - private void addWordRec(NodeArray children, final String word, - final int depth, final int frequency) { - + private void addWordRec(NodeArray children, final String word, final int depth, + final int frequency, Node parentNode) { final int wordLength = word.length(); final char c = word.charAt(depth); // Does children have the current character? @@ -140,6 +160,7 @@ public class ExpandableDictionary extends Dictionary { if (!found) { childNode = new Node(); childNode.code = c; + childNode.parent = parentNode; children.add(childNode); } if (wordLength == depth + 1) { @@ -152,7 +173,7 @@ public class ExpandableDictionary extends Dictionary { if (childNode.children == null) { childNode.children = new NodeArray(); } - addWordRec(childNode.children, word, depth + 1, frequency); + addWordRec(childNode.children, word, depth + 1, frequency, childNode); } @Override @@ -186,7 +207,7 @@ public class ExpandableDictionary extends Dictionary { if (mRequiresReload) startDictionaryLoadingTaskLocked(); if (mUpdatingDictionary) return false; } - final int freq = getWordFrequencyRec(mRoots, word, 0, word.length()); + final int freq = getWordFrequency(word); return freq > -1; } @@ -194,32 +215,8 @@ public class ExpandableDictionary extends Dictionary { * Returns the word's frequency or -1 if not found */ public int getWordFrequency(CharSequence word) { - return getWordFrequencyRec(mRoots, word, 0, word.length()); - } - - /** - * Returns the word's frequency or -1 if not found - */ - private int getWordFrequencyRec(final NodeArray children, final CharSequence word, - final int offset, final int length) { - final int count = children.length; - char currentChar = word.charAt(offset); - for (int j = 0; j < count; j++) { - final Node node = children.data[j]; - if (node.code == currentChar) { - if (offset == length - 1) { - if (node.terminal) { - return node.frequency; - } - } else { - if (node.children != null) { - int freq = getWordFrequencyRec(node.children, word, offset + 1, length); - if (freq > -1) return freq; - } - } - } - } - return -1; + Node node = searchNode(mRoots, word, 0, word.length()); + return (node == null) ? -1 : node.frequency; } /** @@ -267,7 +264,8 @@ public class ExpandableDictionary extends Dictionary { if (completion) { word[depth] = c; if (terminal) { - if (!callback.addWord(word, 0, depth + 1, freq * snr)) { + if (!callback.addWord(word, 0, depth + 1, freq * snr, mDicTypeId, + DataType.UNIGRAM)) { return; } // Add to frequency of next letters for predictive correction @@ -305,7 +303,8 @@ public class ExpandableDictionary extends Dictionary { || !same(word, depth + 1, codes.getTypedWord())) { int finalFreq = freq * snr * addedAttenuation; if (skipPos < 0) finalFreq *= FULL_WORD_FREQ_MULTIPLIER; - callback.addWord(word, 0, depth + 1, finalFreq); + callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, + DataType.UNIGRAM); } } if (children != null) { @@ -324,6 +323,171 @@ public class ExpandableDictionary extends Dictionary { } } + protected int setBigram(String word1, String word2, int frequency) { + return addOrSetBigram(word1, word2, frequency, false); + } + + protected int addBigram(String word1, String word2, int frequency) { + return addOrSetBigram(word1, word2, frequency, true); + } + + /** + * Adds bigrams to the in-memory trie structure that is being used to retrieve any word + * @param frequency frequency for this bigrams + * @param addFrequency if true, it adds to current frequency + * @return returns the final frequency + */ + private int addOrSetBigram(String word1, String word2, int frequency, boolean addFrequency) { + Node firstWord = searchWord(mRoots, word1, 0, null); + Node secondWord = searchWord(mRoots, word2, 0, null); + LinkedList bigram = firstWord.ngrams; + if (bigram == null || bigram.size() == 0) { + firstWord.ngrams = new LinkedList(); + bigram = firstWord.ngrams; + } else { + for (NextWord nw : bigram) { + if (nw.word == secondWord) { + if (addFrequency) { + nw.frequency += frequency; + } else { + nw.frequency = frequency; + } + return nw.frequency; + } + } + } + NextWord nw = new NextWord(secondWord, frequency); + firstWord.ngrams.add(nw); + return frequency; + } + + /** + * Searches for the word and add the word if it does not exist. + * @return Returns the terminal node of the word we are searching for. + */ + private Node searchWord(NodeArray children, String word, int depth, Node parentNode) { + final int wordLength = word.length(); + final char c = word.charAt(depth); + // Does children have the current character? + final int childrenLength = children.length; + Node childNode = null; + boolean found = false; + for (int i = 0; i < childrenLength; i++) { + childNode = children.data[i]; + if (childNode.code == c) { + found = true; + break; + } + } + if (!found) { + childNode = new Node(); + childNode.code = c; + childNode.parent = parentNode; + children.add(childNode); + } + if (wordLength == depth + 1) { + // Terminate this word + childNode.terminal = true; + return childNode; + } + if (childNode.children == null) { + childNode.children = new NodeArray(); + } + return searchWord(childNode.children, word, depth + 1, childNode); + } + + // @VisibleForTesting + boolean reloadDictionaryIfRequired() { + synchronized (mUpdatingLock) { + // If we need to update, start off a background task + if (mRequiresReload) startDictionaryLoadingTaskLocked(); + // Currently updating contacts, don't return any results. + return mUpdatingDictionary; + } + } + + private void runReverseLookUp(final CharSequence previousWord, final WordCallback callback) { + Node prevWord = searchNode(mRoots, previousWord, 0, previousWord.length()); + if (prevWord != null && prevWord.ngrams != null) { + reverseLookUp(prevWord.ngrams, callback); + } + } + + @Override + public void getBigrams(final WordComposer codes, final CharSequence previousWord, + final WordCallback callback, int[] nextLettersFrequencies) { + if (!reloadDictionaryIfRequired()) { + runReverseLookUp(previousWord, callback); + } + } + + /** + * Used only for testing purposes + * This function will wait for loading from database to be done + */ + void waitForDictionaryLoading() { + while (mUpdatingDictionary) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + } + + /** + * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words + * through callback. + * @param terminalNodes list of terminal nodes we want to add + */ + private void reverseLookUp(LinkedList terminalNodes, + final WordCallback callback) { + Node node; + int freq; + for (NextWord nextWord : terminalNodes) { + node = nextWord.word; + freq = nextWord.frequency; + // TODO Not the best way to limit suggestion threshold + if (freq >= UserBigramDictionary.SUGGEST_THRESHOLD) { + sb.setLength(0); + do { + sb.insert(0, node.code); + node = node.parent; + } while(node != null); + + // TODO better way to feed char array? + callback.addWord(sb.toString().toCharArray(), 0, sb.length(), freq, mDicTypeId, + DataType.BIGRAM); + } + } + } + + /** + * Search for the terminal node of the word + * @return Returns the terminal node of the word if the word exists + */ + private Node searchNode(final NodeArray children, final CharSequence word, final int offset, + final int length) { + // TODO Consider combining with addWordRec + final int count = children.length; + char currentChar = word.charAt(offset); + for (int j = 0; j < count; j++) { + final Node node = children.data[j]; + if (node.code == currentChar) { + if (offset == length - 1) { + if (node.terminal) { + return node; + } + } else { + if (node.children != null) { + Node returnNode = searchNode(node.children, word, offset + 1, length); + if (returnNode != null) return returnNode; + } + } + } + } + return null; + } + protected void clearDictionary() { mRoots = new NodeArray(); } @@ -332,18 +496,11 @@ public class ExpandableDictionary extends Dictionary { @Override protected Void doInBackground(Void... v) { loadDictionaryAsync(); - return null; - } - - @Override - protected void onPostExecute(Void result) { - // TODO Auto-generated method stub synchronized (mUpdatingLock) { mUpdatingDictionary = false; } - super.onPostExecute(result); + return null; } - } static char toLowerCase(char c) { diff --git a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java index 5e835e543..4f672271a 100644 --- a/java/src/com/android/inputmethod/latin/InputLanguageSelection.java +++ b/java/src/com/android/inputmethod/latin/InputLanguageSelection.java @@ -99,7 +99,10 @@ public class InputLanguageSelection extends PreferenceActivity { boolean haveDictionary = false; conf.locale = locale; res.updateConfiguration(conf, res.getDisplayMetrics()); - BinaryDictionary bd = new BinaryDictionary(this, R.raw.main); + + int[] dictionaries = LatinIME.getDictionary(res); + BinaryDictionary bd = new BinaryDictionary(this, dictionaries, Suggest.DIC_MAIN); + // Is the dictionary larger than a placeholder? Arbitrarily chose a lower limit of // 4000-5000 words, whereas the LARGE_DICTIONARY is about 20000+ words. if (bd.getSize() > Suggest.LARGE_DICTIONARY_THRESHOLD / 4) { diff --git a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java index 1a196448f..d04930303 100644 --- a/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/latin/KeyboardSwitcher.java @@ -21,12 +21,16 @@ import java.util.Locale; import java.util.Map; import android.content.Context; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.inputmethodservice.InputMethodService; +import android.inputmethodservice.Keyboard; +import android.preference.PreferenceManager; +import android.view.InflateException; -public class KeyboardSwitcher { +public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener { + public static final int MODE_NONE = 0; public static final int MODE_TEXT = 1; public static final int MODE_SYMBOLS = 2; public static final int MODE_PHONE = 3; @@ -45,6 +49,27 @@ public class KeyboardSwitcher { public static final int KEYBOARDMODE_IM = R.id.mode_im; public static final int KEYBOARDMODE_WEB = R.id.mode_webentry; + public static final String DEFAULT_LAYOUT_ID = "3"; + public static final String PREF_KEYBOARD_LAYOUT = "keyboard_layout"; + private static final int[] THEMES = new int [] { + R.layout.input_basic, R.layout.input_basic_highcontrast, R.layout.input_stone_normal, + R.layout.input_stone_bold}; + + // Ids for each characters' color in the keyboard + private static final int CHAR_THEME_COLOR_WHITE = 0; + private static final int CHAR_THEME_COLOR_BLACK = 1; + + // Tables which contains resource ids for each character theme color + private static final int[] KBD_ALPHA = new int[] {R.xml.kbd_alpha, R.xml.kbd_alpha_black}; + private static final int[] KBD_PHONE = new int[] {R.xml.kbd_phone, R.xml.kbd_phone_black}; + private static final int[] KBD_PHONE_SYMBOLS = new int[] { + R.xml.kbd_phone_symbols, R.xml.kbd_phone_symbols_black}; + private static final int[] KBD_SYMBOLS = new int[] { + R.xml.kbd_symbols, R.xml.kbd_symbols_black}; + private static final int[] KBD_SYMBOLS_SHIFT = new int[] { + R.xml.kbd_symbols_shift, R.xml.kbd_symbols_shift_black}; + private static final int[] KBD_QWERTY = new int[] {R.xml.kbd_qwerty, R.xml.kbd_qwerty_black}; + private static final int SYMBOLS_MODE_STATE_NONE = 0; private static final int SYMBOLS_MODE_STATE_BEGIN = 1; private static final int SYMBOLS_MODE_STATE_SYMBOL = 2; @@ -57,9 +82,8 @@ public class KeyboardSwitcher { KEYBOARDMODE_IM, KEYBOARDMODE_WEB}; - //LatinIME mContext; Context mContext; - InputMethodService mInputMethodService; + LatinIME mInputMethodService; private KeyboardId mSymbolsId; private KeyboardId mSymbolsShiftedId; @@ -67,7 +91,7 @@ public class KeyboardSwitcher { private KeyboardId mCurrentId; private Map mKeyboards; - private int mMode; /** One of the MODE_XXX values */ + private int mMode = MODE_NONE; /** One of the MODE_XXX values */ private int mImeOptions; private int mTextMode = MODE_TEXT_QWERTY; private boolean mIsSymbols; @@ -79,13 +103,19 @@ public class KeyboardSwitcher { private int mLastDisplayWidth; private LanguageSwitcher mLanguageSwitcher; private Locale mInputLocale; - private boolean mEnableMultipleLanguages; - KeyboardSwitcher(Context context, InputMethodService ims) { + private int mLayoutId; + + KeyboardSwitcher(Context context, LatinIME ims) { mContext = context; + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ims); + mLayoutId = Integer.valueOf(prefs.getString(PREF_KEYBOARD_LAYOUT, DEFAULT_LAYOUT_ID)); + prefs.registerOnSharedPreferenceChangeListener(this); + mKeyboards = new HashMap(); - mSymbolsId = new KeyboardId(R.xml.kbd_symbols, false); - mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, false); + mSymbolsId = makeSymbolsId(false); + mSymbolsShiftedId = makeSymbolsShiftedId(false); mInputMethodService = ims; } @@ -98,14 +128,24 @@ public class KeyboardSwitcher { void setLanguageSwitcher(LanguageSwitcher languageSwitcher) { mLanguageSwitcher = languageSwitcher; mInputLocale = mLanguageSwitcher.getInputLocale(); - mEnableMultipleLanguages = mLanguageSwitcher.getLocaleCount() > 1; } void setInputView(LatinKeyboardView inputView) { mInputView = inputView; } - + + private KeyboardId makeSymbolsId(boolean hasVoice) { + return new KeyboardId(KBD_SYMBOLS[getCharColorId()], hasVoice); + } + + private KeyboardId makeSymbolsShiftedId(boolean hasVoice) { + return new KeyboardId(KBD_SYMBOLS_SHIFT[getCharColorId()], hasVoice); + } + void makeKeyboards(boolean forceCreate) { + mSymbolsId = makeSymbolsId(mHasVoice && !mVoiceOnPrimary); + mSymbolsShiftedId = makeSymbolsShiftedId(mHasVoice && !mVoiceOnPrimary); + if (forceCreate) mKeyboards.clear(); // Configuration change is coming after the keyboard gets recreated. So don't rely on that. // If keyboards have already been made, check if we have a screen width change and @@ -114,9 +154,6 @@ public class KeyboardSwitcher { if (displayWidth == mLastDisplayWidth) return; mLastDisplayWidth = displayWidth; if (!forceCreate) mKeyboards.clear(); - mSymbolsId = new KeyboardId(R.xml.kbd_symbols, mHasVoice && !mVoiceOnPrimary); - mSymbolsShiftedId = new KeyboardId(R.xml.kbd_symbols_shift, - mHasVoice && !mVoiceOnPrimary); } /** @@ -140,6 +177,7 @@ public class KeyboardSwitcher { this(xml, 0, false, hasVoice); } + @Override public boolean equals(Object other) { return other instanceof KeyboardId && equals((KeyboardId) other); } @@ -150,6 +188,7 @@ public class KeyboardSwitcher { && other.mEnableShiftLock == this.mEnableShiftLock; } + @Override public int hashCode() { return (mXml + 1) * (mKeyboardMode + 1) * (mEnableShiftLock ? 2 : 1) * (mHasVoice ? 4 : 8); @@ -173,8 +212,14 @@ public class KeyboardSwitcher { void setKeyboardMode(int mode, int imeOptions, boolean enableVoice) { mSymbolsModeState = SYMBOLS_MODE_STATE_NONE; mPreferSymbols = mode == MODE_SYMBOLS; - setKeyboardMode(mode == MODE_SYMBOLS ? MODE_TEXT : mode, imeOptions, enableVoice, - mPreferSymbols); + if (mode == MODE_SYMBOLS) { + mode = MODE_TEXT; + } + try { + setKeyboardMode(mode, imeOptions, enableVoice, mPreferSymbols); + } catch (RuntimeException e) { + LatinImeLogger.logOnException(mode + "," + imeOptions + "," + mPreferSymbols, e); + } } void setKeyboardMode(int mode, int imeOptions, boolean enableVoice, boolean isSymbols) { @@ -186,10 +231,10 @@ public class KeyboardSwitcher { } mIsSymbols = isSymbols; - mInputView.setPreviewEnabled(true); + mInputView.setPreviewEnabled(mInputMethodService.getPopupOn()); KeyboardId id = getKeyboardId(mode, imeOptions, isSymbols); - - LatinKeyboard keyboard = getKeyboard(id); + LatinKeyboard keyboard = null; + keyboard = getKeyboard(id); if (mode == MODE_PHONE) { mInputView.setPhoneKeyboard(keyboard); @@ -201,6 +246,7 @@ public class KeyboardSwitcher { keyboard.setShifted(false); keyboard.setShiftLocked(keyboard.isShiftLocked()); keyboard.setImeOptions(mContext.getResources(), mMode, imeOptions); + keyboard.setBlackFlag(isBlackSym()); } private LatinKeyboard getKeyboard(KeyboardId id) { @@ -212,8 +258,10 @@ public class KeyboardSwitcher { orig.updateConfiguration(conf, null); LatinKeyboard keyboard = new LatinKeyboard( mContext, id.mXml, id.mKeyboardMode); - keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols), mHasVoice); + keyboard.setVoiceMode(hasVoiceButton(id.mXml == R.xml.kbd_symbols + || id.mXml == R.xml.kbd_symbols_black), mHasVoice); keyboard.setLanguageSwitcher(mLanguageSwitcher); + keyboard.setBlackFlag(isBlackSym()); if (id.mKeyboardMode == KEYBOARDMODE_NORMAL || id.mKeyboardMode == KEYBOARDMODE_URL || id.mKeyboardMode == KEYBOARDMODE_IM @@ -236,31 +284,40 @@ public class KeyboardSwitcher { private KeyboardId getKeyboardId(int mode, int imeOptions, boolean isSymbols) { boolean hasVoice = hasVoiceButton(isSymbols); + int charColorId = getCharColorId(); + // TODO: generalize for any KeyboardId + int keyboardRowsResId = KBD_QWERTY[charColorId]; if (isSymbols) { - return (mode == MODE_PHONE) - ? new KeyboardId(R.xml.kbd_phone_symbols, hasVoice) - : new KeyboardId(R.xml.kbd_symbols, hasVoice); + if (mode == MODE_PHONE) { + return new KeyboardId(KBD_PHONE_SYMBOLS[charColorId], hasVoice); + } else { + return new KeyboardId(KBD_SYMBOLS[charColorId], hasVoice); + } } switch (mode) { + case MODE_NONE: + LatinImeLogger.logOnWarning( + "getKeyboardId:" + mode + "," + imeOptions + "," + isSymbols); + /* fall through */ case MODE_TEXT: - if (mTextMode == MODE_TEXT_QWERTY) { - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_NORMAL, true, hasVoice); - } else if (mTextMode == MODE_TEXT_ALPHA) { - return new KeyboardId(R.xml.kbd_alpha, KEYBOARDMODE_NORMAL, true, hasVoice); + if (mTextMode == MODE_TEXT_ALPHA) { + return new KeyboardId( + KBD_ALPHA[charColorId], KEYBOARDMODE_NORMAL, true, hasVoice); } - break; + // Normally mTextMode should be MODE_TEXT_QWERTY. + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_NORMAL, true, hasVoice); case MODE_SYMBOLS: - return new KeyboardId(R.xml.kbd_symbols, hasVoice); + return new KeyboardId(KBD_SYMBOLS[charColorId], hasVoice); case MODE_PHONE: - return new KeyboardId(R.xml.kbd_phone, hasVoice); + return new KeyboardId(KBD_PHONE[charColorId], hasVoice); case MODE_URL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_URL, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_URL, true, hasVoice); case MODE_EMAIL: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_EMAIL, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_EMAIL, true, hasVoice); case MODE_IM: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_IM, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_IM, true, hasVoice); case MODE_WEB: - return new KeyboardId(R.xml.kbd_qwerty, KEYBOARDMODE_WEB, true, hasVoice); + return new KeyboardId(keyboardRowsResId, KEYBOARDMODE_WEB, true, hasVoice); } return null; } @@ -273,19 +330,6 @@ public class KeyboardSwitcher { return mMode == MODE_TEXT; } - int getTextMode() { - return mTextMode; - } - - void setTextMode(int position) { - if (position < MODE_TEXT_COUNT && position >= 0) { - mTextMode = position; - } - if (isTextMode()) { - setKeyboardMode(MODE_TEXT, mImeOptions, mHasVoice); - } - } - int getTextModeCount() { return MODE_TEXT_COUNT; } @@ -300,6 +344,18 @@ public class KeyboardSwitcher { return false; } + void setShifted(boolean shifted) { + if (mInputView != null) { + mInputView.setShifted(shifted); + } + } + + void setShiftLocked(boolean shiftLocked) { + if (mInputView != null) { + mInputView.setShiftLocked(shiftLocked); + } + } + void toggleShift() { if (mCurrentId.equals(mSymbolsId)) { LatinKeyboard symbolsKeyboard = getKeyboard(mSymbolsId); @@ -314,7 +370,7 @@ public class KeyboardSwitcher { LatinKeyboard symbolsShiftedKeyboard = getKeyboard(mSymbolsShiftedId); symbolsShiftedKeyboard.setShifted(false); mCurrentId = mSymbolsId; - mInputView.setKeyboard(getKeyboard(mSymbolsId)); + mInputView.setKeyboard(symbolsKeyboard); symbolsKeyboard.setShifted(false); symbolsKeyboard.setImeOptions(mContext.getResources(), mMode, mImeOptions); } @@ -348,4 +404,72 @@ public class KeyboardSwitcher { } return false; } + + public LatinKeyboardView getInputView() { + return mInputView; + } + + public void recreateInputView() { + changeLatinKeyboardView(mLayoutId, true); + } + + private void changeLatinKeyboardView(int newLayout, boolean forceReset) { + if (mLayoutId != newLayout || mInputView == null || forceReset) { + if (mInputView != null) { + mInputView.closing(); + } + if (THEMES.length <= newLayout) { + newLayout = Integer.valueOf(DEFAULT_LAYOUT_ID); + } + + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + mInputView = (LatinKeyboardView) mInputMethodService.getLayoutInflater( + ).inflate(THEMES[newLayout], null); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( + mLayoutId + "," + newLayout, e); + } catch (InflateException e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait( + mLayoutId + "," + newLayout, e); + } + } + mInputView.setExtentionLayoutResId(THEMES[newLayout]); + mInputView.setOnKeyboardActionListener(mInputMethodService); + mLayoutId = newLayout; + } + mInputMethodService.mHandler.post(new Runnable() { + public void run() { + if (mInputView != null) { + mInputMethodService.setInputView(mInputView); + } + mInputMethodService.updateInputViewShown(); + }}); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PREF_KEYBOARD_LAYOUT.equals(key)) { + changeLatinKeyboardView( + Integer.valueOf(sharedPreferences.getString(key, DEFAULT_LAYOUT_ID)), false); + } + } + + public boolean isBlackSym () { + if (mInputView != null && mInputView.getSymbolColorSheme() == 1) { + return true; + } + return false; + } + + private int getCharColorId () { + if (isBlackSym()) { + return CHAR_THEME_COLOR_BLACK; + } else { + return CHAR_THEME_COLOR_WHITE; + } + } + } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 15b537f94..9bd16adb2 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -16,11 +16,12 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.voice.EditingUtil; import com.android.inputmethod.voice.FieldContext; import com.android.inputmethod.voice.SettingsUtil; import com.android.inputmethod.voice.VoiceInput; +import org.xmlpull.v1.XmlPullParserException; + import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,9 +31,9 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.content.res.XmlResourceParser; import android.inputmethodservice.InputMethodService; import android.inputmethodservice.Keyboard; -import android.inputmethodservice.KeyboardView; import android.media.AudioManager; import android.os.Debug; import android.os.Handler; @@ -40,9 +41,9 @@ import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.speech.SpeechRecognizer; -import android.text.AutoText; import android.text.ClipboardManager; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -50,8 +51,8 @@ import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewParent; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; @@ -62,6 +63,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -74,21 +76,25 @@ import java.util.Map; * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService - implements KeyboardView.OnKeyboardActionListener, + implements LatinKeyboardBaseView.OnKeyboardActionListener, VoiceInput.UiListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = "LatinIME"; + private static final boolean PERF_DEBUG = false; static final boolean DEBUG = false; static final boolean TRACE = false; static final boolean VOICE_INSTALLED = true; static final boolean ENABLE_VOICE_BUTTON = true; + private static final boolean MODIFY_TEXT_FOR_CORRECTION = false; private static final String PREF_VIBRATE_ON = "vibrate_on"; private static final String PREF_SOUND_ON = "sound_on"; + private static final String PREF_POPUP_ON = "popup_on"; private static final String PREF_AUTO_CAP = "auto_cap"; private static final String PREF_QUICK_FIXES = "quick_fixes"; private static final String PREF_SHOW_SUGGESTIONS = "show_suggestions"; private static final String PREF_AUTO_COMPLETE = "auto_complete"; + private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion"; 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 @@ -127,6 +133,7 @@ public class LatinIME extends InputMethodService private static final int MSG_UPDATE_SHIFT_STATE = 2; private static final int MSG_VOICE_RESULTS = 3; private static final int MSG_START_LISTENING_AFTER_SWIPE = 4; + private static final int MSG_UPDATE_OLD_SUGGESTIONS = 5; // If we detect a swipe gesture within N ms of typing, then swipe is // ignored, since it may in fact be two key presses in quick succession. @@ -145,7 +152,7 @@ public class LatinIME extends InputMethodService private static final int POS_SETTINGS = 0; private static final int POS_METHOD = 1; - private LatinKeyboardView mInputView; + //private LatinKeyboardView mInputView; private CandidateViewContainer mCandidateViewContainer; private CandidateView mCandidateView; private Suggest mSuggest; @@ -157,6 +164,7 @@ public class LatinIME extends InputMethodService KeyboardSwitcher mKeyboardSwitcher; private UserDictionary mUserDictionary; + private UserBigramDictionary mUserBigramDictionary; private ContactsDictionary mContactsDictionary; private AutoDictionary mAutoDictionary; @@ -176,7 +184,6 @@ public class LatinIME extends InputMethodService private boolean mAfterVoiceInput; private boolean mImmediatelyAfterVoiceInput; private boolean mShowingVoiceSuggestions; - private boolean mImmediatelyAfterVoiceSuggestions; private boolean mVoiceInputHighlighted; private boolean mEnableVoiceButton; private CharSequence mBestWord; @@ -186,25 +193,32 @@ public class LatinIME extends InputMethodService private boolean mAutoSpace; private boolean mJustAddedAutoSpace; private boolean mAutoCorrectEnabled; + private boolean mBigramSuggestionEnabled; private boolean mAutoCorrectOn; + // TODO move this state variable outside LatinIME private boolean mCapsLock; private boolean mPasswordText; - private boolean mEmailText; private boolean mVibrateOn; private boolean mSoundOn; + private boolean mPopupOn; private boolean mAutoCap; private boolean mQuickFixes; private boolean mHasUsedVoiceInput; private boolean mHasUsedVoiceInputUnsupportedLocale; private boolean mLocaleSupportedForVoiceInput; private boolean mShowSuggestions; - private boolean mSuggestionShouldReplaceCurrentWord; private boolean mIsShowingHint; private int mCorrectionMode; private boolean mEnableVoice = true; private boolean mVoiceOnPrimary; private int mOrientation; private List mSuggestPuncList; + // Keep track of the last selection range to decide if we need to show word alternatives + private int mLastSelectionStart; + private int mLastSelectionEnd; + + // Input type is such that we should not auto-correct + private boolean mInputTypeNoAutoCorrect; // Indicates whether the suggestion strip is to be on in landscape private boolean mJustAccepted; @@ -219,8 +233,9 @@ public class LatinIME extends InputMethodService private final float FX_VOLUME = -1.0f; private boolean mSilentMode; - private String mWordSeparators; + /* package */ String mWordSeparators; private String mSentenceSeparators; + private String mSuggestPuncs; private VoiceInput mVoiceInput; private VoiceResults mVoiceResults = new VoiceResults(); private long mSwipeTriggerTimeMillis; @@ -228,17 +243,66 @@ public class LatinIME extends InputMethodService // Keeps track of most recently inserted text (multi-character key) for reverting private CharSequence mEnteredText; + private boolean mRefreshKeyboardRequired; // For each word, a list of potential replacements, usually from voice. private Map> mWordToSuggestions = new HashMap>(); + private ArrayList mWordHistory = new ArrayList(); + private class VoiceResults { List candidates; Map> alternatives; } + + public abstract static class WordAlternatives { + protected CharSequence mChosenWord; - private boolean mRefreshKeyboardRequired; + public WordAlternatives() { + // Nothing + } + + public WordAlternatives(CharSequence chosenWord) { + mChosenWord = chosenWord; + } + + @Override + public int hashCode() { + return mChosenWord.hashCode(); + } + + public abstract CharSequence getOriginalWord(); + + public CharSequence getChosenWord() { + return mChosenWord; + } + + public abstract List getAlternatives(); + } + + public class TypedWordAlternatives extends WordAlternatives { + private WordComposer word; + + public TypedWordAlternatives() { + // Nothing + } + + public TypedWordAlternatives(CharSequence chosenWord, WordComposer wordComposer) { + super(chosenWord); + word = wordComposer; + } + + @Override + public CharSequence getOriginalWord() { + return word.getTypedWord(); + } + + @Override + public List getAlternatives() { + return getTypedSuggestions(word); + } + } Handler mHandler = new Handler() { @Override @@ -247,10 +311,14 @@ public class LatinIME extends InputMethodService case MSG_UPDATE_SUGGESTIONS: updateSuggestions(); break; + case MSG_UPDATE_OLD_SUGGESTIONS: + setOldSuggestions(); + break; case MSG_START_TUTORIAL: if (mTutorial == null) { - if (mInputView.isShown()) { - mTutorial = new Tutorial(LatinIME.this, mInputView); + if (mKeyboardSwitcher.getInputView().isShown()) { + mTutorial = new Tutorial( + LatinIME.this, mKeyboardSwitcher.getInputView()); mTutorial.start(); } else { // Try again soon if the view is not yet showing @@ -273,6 +341,7 @@ public class LatinIME extends InputMethodService }; @Override public void onCreate() { + LatinImeLogger.init(this); super.onCreate(); //setStatusIcon(R.drawable.ime_qwerty); mResources = getResources(); @@ -288,7 +357,18 @@ public class LatinIME extends InputMethodService if (inputLanguage == null) { inputLanguage = conf.locale.toString(); } - initSuggest(inputLanguage); + + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + initSuggest(inputLanguage); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait(inputLanguage, e); + } + } + mOrientation = conf.orientation; initSuggestPuncList(); @@ -311,6 +391,46 @@ public class LatinIME extends InputMethodService prefs.registerOnSharedPreferenceChangeListener(this); } + /** + * Loads a dictionary or multiple separated dictionary + * @return returns array of dictionary resource ids + */ + static int[] getDictionary(Resources res) { + String packageName = LatinIME.class.getPackage().getName(); + XmlResourceParser xrp = res.getXml(R.xml.dictionary); + int dictionaryCount = 0; + ArrayList dictionaries = new ArrayList(); + + try { + int current = xrp.getEventType(); + while (current != XmlResourceParser.END_DOCUMENT) { + if (current == XmlResourceParser.START_TAG) { + String tag = xrp.getName(); + if (tag != null) { + if (tag.equals("part")) { + String dictFileName = xrp.getAttributeValue(null, "name"); + dictionaries.add(res.getIdentifier(dictFileName, "raw", packageName)); + } + } + } + xrp.next(); + current = xrp.getEventType(); + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Dictionary XML parsing failure"); + } catch (IOException e) { + Log.e(TAG, "Dictionary XML IOException"); + } + + int count = dictionaries.size(); + int[] dict = new int[count]; + for (int i = 0; i < count; i++) { + dict[i] = dictionaries.get(i); + } + + return dict; + } + private void initSuggest(String locale) { mInputLocale = locale; @@ -324,17 +444,25 @@ public class LatinIME extends InputMethodService } SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); - mSuggest = new Suggest(this, R.raw.main); + + int[] dictionaries = getDictionary(orig); + mSuggest = new Suggest(this, dictionaries); updateAutoTextEnabled(saveLocale); if (mUserDictionary != null) mUserDictionary.close(); mUserDictionary = new UserDictionary(this, mInputLocale); if (mContactsDictionary == null) { - mContactsDictionary = new ContactsDictionary(this); + mContactsDictionary = new ContactsDictionary(this, Suggest.DIC_CONTACTS); } if (mAutoDictionary != null) { mAutoDictionary.close(); } - mAutoDictionary = new AutoDictionary(this, this, mInputLocale); + mAutoDictionary = new AutoDictionary(this, this, mInputLocale, Suggest.DIC_AUTO); + if (mUserBigramDictionary != null) { + mUserBigramDictionary.close(); + } + mUserBigramDictionary = new UserBigramDictionary(this, this, mInputLocale, + Suggest.DIC_USER); + mSuggest.setUserBigramDictionary(mUserBigramDictionary); mSuggest.setUserDictionary(mUserDictionary); mSuggest.setContactsDictionary(mContactsDictionary); mSuggest.setAutoDictionary(mAutoDictionary); @@ -348,12 +476,18 @@ public class LatinIME extends InputMethodService @Override public void onDestroy() { - mUserDictionary.close(); - mContactsDictionary.close(); + if (mUserDictionary != null) { + mUserDictionary.close(); + } + if (mContactsDictionary != null) { + mContactsDictionary.close(); + } unregisterReceiver(mReceiver); - if (VOICE_INSTALLED) { + if (VOICE_INSTALLED && mVoiceInput != null) { mVoiceInput.destroy(); } + LatinImeLogger.commit(); + LatinImeLogger.onDestroy(); super.onDestroy(); } @@ -393,15 +527,12 @@ public class LatinIME extends InputMethodService @Override public View onCreateInputView() { - mInputView = (LatinKeyboardView) getLayoutInflater().inflate( - R.layout.input, null); - mKeyboardSwitcher.setInputView(mInputView); + mKeyboardSwitcher.recreateInputView(); mKeyboardSwitcher.makeKeyboards(true); - mInputView.setOnKeyboardActionListener(this); mKeyboardSwitcher.setKeyboardMode( KeyboardSwitcher.MODE_TEXT, 0, shouldShowVoiceButton(makeFieldContext(), getCurrentInputEditorInfo())); - return mInputView; + return mKeyboardSwitcher.getInputView(); } @Override @@ -418,8 +549,9 @@ public class LatinIME extends InputMethodService @Override public void onStartInputView(EditorInfo attribute, boolean restarting) { + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); // In landscape mode, this method gets called without the input view being created. - if (mInputView == null) { + if (inputView == null) { return; } @@ -448,15 +580,12 @@ public class LatinIME extends InputMethodService mAfterVoiceInput = false; mImmediatelyAfterVoiceInput = false; mShowingVoiceSuggestions = false; - mImmediatelyAfterVoiceSuggestions = false; mVoiceInputHighlighted = false; - mWordToSuggestions.clear(); mInputTypeNoAutoCorrect = false; mPredictionOn = false; mCompletionOn = false; mCompletions = null; mCapsLock = false; - mEmailText = false; mEnteredText = null; switch (attribute.inputType & EditorInfo.TYPE_MASK_CLASS) { @@ -479,9 +608,6 @@ public class LatinIME extends InputMethodService variation == EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ) { mPredictionOn = false; } - if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) { - mEmailText = true; - } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME) { mAutoSpace = false; @@ -532,7 +658,7 @@ public class LatinIME extends InputMethodService attribute.imeOptions, enableVoiceButton); updateShiftKeyState(attribute); } - mInputView.closing(); + inputView.closing(); mComposing.setLength(0); mPredicting = false; mDeleteCount = 0; @@ -548,7 +674,8 @@ public class LatinIME extends InputMethodService updateCorrectionMode(); - mInputView.setProximityCorrectionEnabled(true); + inputView.setPreviewEnabled(mPopupOn); + inputView.setProximityCorrectionEnabled(true); mPredictionOn = mPredictionOn && (mCorrectionMode > 0 || mShowSuggestions); checkTutorial(attribute.privateImeOptions); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); @@ -558,6 +685,8 @@ public class LatinIME extends InputMethodService public void onFinishInput() { super.onFinishInput(); + LatinImeLogger.commit(); + if (VOICE_INSTALLED && !mConfigurationChanging) { if (mAfterVoiceInput) { mVoiceInput.flushAllTextModificationCounters(); @@ -566,10 +695,11 @@ public class LatinIME extends InputMethodService mVoiceInput.flushLogs(); mVoiceInput.cancel(); } - if (mInputView != null) { - mInputView.closing(); + if (mKeyboardSwitcher.getInputView() != null) { + mKeyboardSwitcher.getInputView().closing(); } if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); + if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); } @Override @@ -605,15 +735,15 @@ public class LatinIME extends InputMethodService mVoiceInput.setSelectionSpan(newSelEnd - newSelStart); } - mSuggestionShouldReplaceCurrentWord = false; // If the current selection in the text view changes, we should // clear whatever candidate text we have. if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted) && (newSelStart != candidatesEnd - || newSelEnd != candidatesEnd))) { + || newSelEnd != candidatesEnd) + && mLastSelectionStart != newSelStart)) { mComposing.setLength(0); mPredicting = false; - updateSuggestions(); + postUpdateSuggestions(); TextEntryState.reset(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { @@ -622,10 +752,10 @@ public class LatinIME extends InputMethodService mVoiceInputHighlighted = false; } else if (!mPredicting && !mJustAccepted) { switch (TextEntryState.getState()) { - case TextEntryState.STATE_ACCEPTED_DEFAULT: + case ACCEPTED_DEFAULT: TextEntryState.reset(); // fall through - case TextEntryState.STATE_SPACE_AFTER_PICKED: + case SPACE_AFTER_PICKED: mJustAddedAutoSpace = false; // The user moved the cursor. break; } @@ -633,32 +763,29 @@ public class LatinIME extends InputMethodService mJustAccepted = false; postUpdateShiftKeyState(); - if (VOICE_INSTALLED) { - if (mShowingVoiceSuggestions) { - if (mImmediatelyAfterVoiceSuggestions) { - mImmediatelyAfterVoiceSuggestions = false; - } else { - updateSuggestions(); - mShowingVoiceSuggestions = false; - } - } - if (VoiceInput.ENABLE_WORD_CORRECTIONS) { - // If we have alternatives for the current word, then show them. - String word = EditingUtil.getWordAtCursor( - getCurrentInputConnection(), getWordSeparators()); - if (word != null && mWordToSuggestions.containsKey(word.trim())) { - mSuggestionShouldReplaceCurrentWord = true; - final List suggestions = mWordToSuggestions.get(word.trim()); + // Make a note of the cursor position + mLastSelectionStart = newSelStart; + mLastSelectionEnd = newSelEnd; - setSuggestions(suggestions, false, true, true); - setCandidatesViewShown(true); - } + + // Check if we should go in or out of correction mode. + if (isPredictionOn() && mJustRevertedSeparator == null + && (candidatesStart == candidatesEnd || newSelStart != oldSelStart + || TextEntryState.isCorrecting()) + && (newSelStart < newSelEnd - 1 || (!mPredicting)) + && !mVoiceInputHighlighted) { + if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) { + postUpdateOldSuggestions(); + } else { + abortCorrection(false); } } } @Override public void hideWindow() { + LatinImeLogger.commit(); + if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); @@ -675,13 +802,15 @@ public class LatinIME extends InputMethodService mVoiceInput.cancel(); } } + mWordToSuggestions.clear(); + mWordHistory.clear(); super.hideWindow(); TextEntryState.endSession(); } @Override public void onDisplayCompletions(CompletionInfo[] completions) { - if (false) { + if (DEBUG) { Log.i("foo", "Received completions:"); for (int i=0; i<(completions != null ? completions.length : 0); i++) { Log.i("foo", " #" + i + ": " + completions[i]); @@ -699,7 +828,7 @@ public class LatinIME extends InputMethodService CompletionInfo ci = completions[i]; if (ci != null) stringList.add(ci.getText()); } - //CharSequence typedWord = mWord.getTypedWord(); + // When in fullscreen mode, show completions generated by the application setSuggestions(stringList, true, true, true); mBestWord = null; setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); @@ -711,7 +840,8 @@ public class LatinIME extends InputMethodService // TODO: Remove this if we support candidates with hard keyboard if (onEvaluateInputViewShown()) { // Show the candidates view only if input view is showing - super.setCandidatesViewShown(shown && mInputView != null && mInputView.isShown()); + super.setCandidatesViewShown(shown && mKeyboardSwitcher.getInputView() != null + && mKeyboardSwitcher.getInputView().isShown()); } } @@ -723,12 +853,25 @@ public class LatinIME extends InputMethodService } } + @Override + public boolean onEvaluateFullscreenMode() { + DisplayMetrics dm = getResources().getDisplayMetrics(); + float displayHeight = dm.heightPixels; + // If the display is more than X inches high, don't go to fullscreen mode + float dimen = getResources().getDimension(R.dimen.max_height_for_fullscreen); + if (displayHeight > dimen) { + return false; + } else { + return super.onEvaluateFullscreenMode(); + } + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: - if (event.getRepeatCount() == 0 && mInputView != null) { - if (mInputView.handleBack()) { + if (event.getRepeatCount() == 0 && mKeyboardSwitcher.getInputView() != null) { + if (mKeyboardSwitcher.getInputView().handleBack()) { return true; } else if (mTutorial != null) { mTutorial.close(); @@ -760,8 +903,10 @@ public class LatinIME extends InputMethodService if (mTutorial != null) { return true; } + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); // Enable shift key and DPAD to do selections - if (mInputView != null && mInputView.isShown() && mInputView.isShifted()) { + if (inputView != null && inputView.isShown() + && inputView.isShifted()) { event = new KeyEvent(event.getDownTime(), event.getEventTime(), event.getAction(), event.getKeyCode(), event.getRepeatCount(), event.getDeviceId(), event.getScanCode(), @@ -794,7 +939,8 @@ public class LatinIME extends InputMethodService mKeyboardSwitcher = new KeyboardSwitcher(this, this); } mKeyboardSwitcher.setLanguageSwitcher(mLanguageSwitcher); - if (mInputView != null) { + if (mKeyboardSwitcher.getInputView() != null + && mKeyboardSwitcher.getKeyboardMode() != KeyboardSwitcher.MODE_NONE) { mKeyboardSwitcher.setVoiceMode(mEnableVoice && mEnableVoiceButton, mVoiceOnPrimary); } mKeyboardSwitcher.makeKeyboards(true); @@ -809,7 +955,7 @@ public class LatinIME extends InputMethodService } mCommittedLength = mComposing.length(); TextEntryState.acceptedTyped(mComposing); - checkAddToDictionary(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); + addToDictionaries(mComposing, AutoDictionary.FREQUENCY_FOR_TYPED); } updateSuggestions(); } @@ -822,9 +968,8 @@ public class LatinIME extends InputMethodService public void updateShiftKeyState(EditorInfo attr) { InputConnection ic = getCurrentInputConnection(); - if (attr != null && mInputView != null && mKeyboardSwitcher.isAlphabetMode() - && ic != null) { - mInputView.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); + if (attr != null && mKeyboardSwitcher.isAlphabetMode() && ic != null) { + mKeyboardSwitcher.setShifted(mCapsLock || getCursorCapsMode(ic, attr) != 0); } } @@ -940,6 +1085,7 @@ public class LatinIME extends InputMethodService case Keyboard.KEYCODE_DELETE: handleBackspace(); mDeleteCount++; + LatinImeLogger.logOnDelete(); break; case Keyboard.KEYCODE_SHIFT: handleShift(); @@ -959,11 +1105,7 @@ public class LatinIME extends InputMethodService toggleLanguage(false, false); break; case LatinKeyboardView.KEYCODE_SHIFT_LONGPRESS: - if (mCapsLock) { - handleShift(); - } else { - toggleCapsLock(); - } + handleCapsLock(); break; case Keyboard.KEYCODE_MODE_CHANGE: changeKeyboardMode(); @@ -980,6 +1122,7 @@ public class LatinIME extends InputMethodService if (primaryCode != KEYCODE_ENTER) { mJustAddedAutoSpace = false; } + LatinImeLogger.logOnInputChar((char)primaryCode); if (isWordSeparator(primaryCode)) { handleSeparator(primaryCode); } else { @@ -1001,6 +1144,7 @@ public class LatinIME extends InputMethodService } InputConnection ic = getCurrentInputConnection(); if (ic == null) return; + abortCorrection(false); ic.beginBatchEdit(); if (mPredicting) { commitTyped(ic); @@ -1025,6 +1169,8 @@ public class LatinIME extends InputMethodService InputConnection ic = getCurrentInputConnection(); if (ic == null) return; + ic.beginBatchEdit(); + if (mAfterVoiceInput) { // Don't log delete if the user is pressing delete at // the beginning of the text box (hence not deleting anything) @@ -1055,8 +1201,9 @@ public class LatinIME extends InputMethodService } postUpdateShiftKeyState(); TextEntryState.backspace(); - if (TextEntryState.getState() == TextEntryState.STATE_UNDO_COMMIT) { + if (TextEntryState.getState() == TextEntryState.State.UNDO_COMMIT) { revertLastWord(deleteChar); + ic.endBatchEdit(); return; } else if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) { ic.deleteSurroundingText(mEnteredText.length(), 0); @@ -1078,16 +1225,47 @@ public class LatinIME extends InputMethodService } } mJustRevertedSeparator = null; + ic.endBatchEdit(); } private void handleShift() { mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); - if (mKeyboardSwitcher.isAlphabetMode()) { - // Alphabet keyboard - checkToggleCapsLock(); - mInputView.setShifted(mCapsLock || !mInputView.isShifted()); + KeyboardSwitcher switcher = mKeyboardSwitcher; + LatinKeyboardView inputView = switcher.getInputView(); + if (switcher.isAlphabetMode()) { + if (mCapsLock) { + mCapsLock = false; + switcher.setShifted(false); + } else if (inputView != null) { + if (inputView.isShifted()) { + mCapsLock = true; + switcher.setShiftLocked(true); + } else { + switcher.setShifted(true); + } + } } else { - mKeyboardSwitcher.toggleShift(); + switcher.toggleShift(); + } + } + + private void handleCapsLock() { + mHandler.removeMessages(MSG_UPDATE_SHIFT_STATE); + KeyboardSwitcher switcher = mKeyboardSwitcher; + if (switcher.isAlphabetMode()) { + mCapsLock = !mCapsLock; + if (mCapsLock) { + switcher.setShiftLocked(true); + } else { + switcher.setShifted(false); + } + } + } + + private void abortCorrection(boolean force) { + if (force || TextEntryState.isCorrecting()) { + getCurrentInputConnection().finishComposingText(); + setSuggestions(null, false, false, false); } } @@ -1100,24 +1278,31 @@ public class LatinIME extends InputMethodService // Assume input length is 1. This assumption fails for smiley face insertions. mVoiceInput.incrementTextModificationInsertCount(1); } + abortCorrection(false); if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) { if (!mPredicting) { mPredicting = true; mComposing.setLength(0); + saveWordInHistory(mBestWord); mWord.reset(); } } - if (mInputView.isShifted()) { - // TODO: This doesn't work with ß, need to fix it in the next release. + if (mKeyboardSwitcher.getInputView().isShifted()) { + // TODO: This doesn't work with [beta], need to fix it in the next release. if (keyCodes == null || keyCodes[0] < Character.MIN_CODE_POINT || keyCodes[0] > Character.MAX_CODE_POINT) { return; } - primaryCode = new String(keyCodes, 0, 1).toUpperCase().charAt(0); + primaryCode = keyCodes[0]; + if (mKeyboardSwitcher.isAlphabetMode()) { + primaryCode = Character.toUpperCase(primaryCode); + } } if (mPredicting) { - if (mInputView.isShifted() && mComposing.length() == 0) { + if (mKeyboardSwitcher.getInputView().isShifted() + && mKeyboardSwitcher.isAlphabetMode() + && mComposing.length() == 0) { mWord.setCapitalized(true); } mComposing.append((char) primaryCode); @@ -1136,7 +1321,7 @@ public class LatinIME extends InputMethodService sendKeyChar((char)primaryCode); } updateShiftKeyState(getCurrentInputEditorInfo()); - measureCps(); + if (LatinIME.PERF_DEBUG) measureCps(); TextEntryState.typedCharacter((char) primaryCode, isWordSeparator(primaryCode)); } @@ -1160,6 +1345,7 @@ public class LatinIME extends InputMethodService InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); + abortCorrection(false); } if (mPredicting) { // In certain languages where single quote is a separator, it's better @@ -1170,8 +1356,7 @@ public class LatinIME extends InputMethodService (mJustRevertedSeparator == null || mJustRevertedSeparator.length() == 0 || mJustRevertedSeparator.charAt(0) != primaryCode)) { - pickDefaultSuggestion(); - pickedDefault = true; + pickedDefault = pickDefaultSuggestion(); // Picked the suggestion by the space key. We consider this // as "added an auto space". if (primaryCode == KEYCODE_SPACE) { @@ -1189,21 +1374,20 @@ public class LatinIME extends InputMethodService // Handle the case of ". ." -> " .." with auto-space if necessary // before changing the TextEntryState. - if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED + if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED && primaryCode == KEYCODE_PERIOD) { reswapPeriodAndSpace(); } TextEntryState.typedCharacter((char) primaryCode, true); - if (TextEntryState.getState() == TextEntryState.STATE_PUNCTUATION_AFTER_ACCEPTED + if (TextEntryState.getState() == TextEntryState.State.PUNCTUATION_AFTER_ACCEPTED && primaryCode != KEYCODE_ENTER) { swapPunctuationAndSpace(); } else if (isPredictionOn() && primaryCode == KEYCODE_SPACE) { - //else if (TextEntryState.STATE_SPACE_AFTER_ACCEPTED) { doubleSpace(); } - if (pickedDefault && mBestWord != null) { - TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); + if (pickedDefault) { + TextEntryState.backToAcceptedDefault(mWord.getTypedWord()); } updateShiftKeyState(getCurrentInputEditorInfo()); if (ic != null) { @@ -1217,21 +1401,25 @@ public class LatinIME extends InputMethodService mVoiceInput.cancel(); } requestHideSelf(0); - mInputView.closing(); + mKeyboardSwitcher.getInputView().closing(); TextEntryState.endSession(); } - private void checkToggleCapsLock() { - if (mInputView.getKeyboard().isShifted()) { - toggleCapsLock(); + private void saveWordInHistory(CharSequence result) { + if (mWord.size() <= 1) { + mWord.reset(); + return; + } + // Skip if result is null. It happens in some edge case. + if (TextUtils.isEmpty(result)) { + return; } - } - private void toggleCapsLock() { - mCapsLock = !mCapsLock; - if (mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); - } + // Make a copy of the CharSequence, since it is/could be a mutable CharSequence + final String resultCopy = result.toString(); + TypedWordAlternatives entry = new TypedWordAlternatives(resultCopy, + new WordComposer(mWord)); + mWordHistory.add(entry); } private void postUpdateSuggestions() { @@ -1239,6 +1427,11 @@ public class LatinIME extends InputMethodService mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_SUGGESTIONS), 100); } + private void postUpdateOldSuggestions() { + mHandler.removeMessages(MSG_UPDATE_OLD_SUGGESTIONS); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE_OLD_SUGGESTIONS), 300); + } + private boolean isPredictionOn() { boolean predictionOn = mPredictionOn; return predictionOn; @@ -1258,8 +1451,8 @@ public class LatinIME extends InputMethodService mHandler.post(new Runnable() { public void run() { mRecognizing = false; - if (mInputView != null) { - setInputView(mInputView); + if (mKeyboardSwitcher.getInputView() != null) { + setInputView(mKeyboardSwitcher.getInputView()); } updateInputViewShown(); }}); @@ -1358,7 +1551,7 @@ public class LatinIME extends InputMethodService Window window = mVoiceWarningDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mInputView.getWindowToken(); + lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); @@ -1394,7 +1587,8 @@ public class LatinIME extends InputMethodService final List nBest = new ArrayList(); boolean capitalizeFirstWord = preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted()); + || (mKeyboardSwitcher.isAlphabetMode() + && mKeyboardSwitcher.getInputView().isShifted()); for (String c : mVoiceResults.candidates) { if (capitalizeFirstWord) { c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length()); @@ -1419,13 +1613,6 @@ public class LatinIME extends InputMethodService if (ic != null) ic.endBatchEdit(); - // Show N-Best alternates, if there is more than one choice. - if (nBest.size() > 1) { - mImmediatelyAfterVoiceSuggestions = true; - mShowingVoiceSuggestions = true; - setSuggestions(nBest.subList(1, nBest.size()), false, true, true); - setCandidatesViewShown(true); - } mVoiceInputHighlighted = true; mWordToSuggestions.putAll(mVoiceResults.alternatives); @@ -1450,9 +1637,8 @@ public class LatinIME extends InputMethodService } private void updateSuggestions() { - mSuggestionShouldReplaceCurrentWord = false; - - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); + ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); // Check if we have a suggestion engine attached. if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) { @@ -1463,24 +1649,56 @@ public class LatinIME extends InputMethodService setNextSuggestions(); return; } + showSuggestions(mWord); + } + + private List getTypedSuggestions(WordComposer word) { + List stringList = mSuggest.getSuggestions( + mKeyboardSwitcher.getInputView(), word, false, null); + return stringList; + } + + private void showCorrections(WordAlternatives alternatives) { + List stringList = alternatives.getAlternatives(); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters(null); + showSuggestions(stringList, alternatives.getOriginalWord(), false, false); + } + + private void showSuggestions(WordComposer word) { + //long startTime = System.currentTimeMillis(); // TIME MEASUREMENT! + // TODO Maybe need better way of retrieving previous word + CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), + mWordSeparators); + List stringList = mSuggest.getSuggestions( + mKeyboardSwitcher.getInputView(), word, false, prevWord); + //long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT! + //Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime)); - List stringList = mSuggest.getSuggestions(mInputView, mWord, false); int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(nextLettersFrequencies); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).setPreferredLetters( + nextLettersFrequencies); boolean correctionAvailable = !mInputTypeNoAutoCorrect && mSuggest.hasMinimalCorrection(); //|| mCorrectionMode == mSuggest.CORRECTION_FULL; - CharSequence typedWord = mWord.getTypedWord(); + CharSequence typedWord = word.getTypedWord(); // If we're in basic correct boolean typedWordValid = mSuggest.isValidWord(typedWord) || - (preferCapitalization() && mSuggest.isValidWord(typedWord.toString().toLowerCase())); - if (mCorrectionMode == Suggest.CORRECTION_FULL) { + (preferCapitalization() + && mSuggest.isValidWord(typedWord.toString().toLowerCase())); + if (mCorrectionMode == Suggest.CORRECTION_FULL + || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) { correctionAvailable |= typedWordValid; } // Don't auto-correct words with multiple capital letter - correctionAvailable &= !mWord.isMostlyCaps(); + correctionAvailable &= !word.isMostlyCaps(); + correctionAvailable &= !TextEntryState.isCorrecting(); + showSuggestions(stringList, typedWord, typedWordValid, correctionAvailable); + } + + private void showSuggestions(List stringList, CharSequence typedWord, + boolean typedWordValid, boolean correctionAvailable) { setSuggestions(stringList, false, typedWordValid, correctionAvailable); if (stringList.size() > 0) { if (correctionAvailable && !typedWordValid && stringList.size() > 1) { @@ -1494,7 +1712,7 @@ public class LatinIME extends InputMethodService setCandidatesViewShown(isCandidateStripVisible() || mCompletionOn); } - private void pickDefaultSuggestion() { + private boolean pickDefaultSuggestion() { // Complete any pending candidate query first if (mHandler.hasMessages(MSG_UPDATE_SUGGESTIONS)) { mHandler.removeMessages(MSG_UPDATE_SUGGESTIONS); @@ -1503,14 +1721,18 @@ public class LatinIME extends InputMethodService if (mBestWord != null && mBestWord.length() > 0) { TextEntryState.acceptedDefault(mWord.getTypedWord(), mBestWord); mJustAccepted = true; - pickSuggestion(mBestWord); + pickSuggestion(mBestWord, false); // Add the word to the auto dictionary if it's not a known word - checkAddToDictionary(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); + addToDictionaries(mBestWord, AutoDictionary.FREQUENCY_FOR_TYPED); + return true; + } + return false; } public void pickSuggestionManually(int index, CharSequence suggestion) { if (mAfterVoiceInput && mShowingVoiceSuggestions) mVoiceInput.logNBestChoose(index); + List suggestions = mCandidateView.getSuggestions(); if (mAfterVoiceInput && !mShowingVoiceSuggestions) { mVoiceInput.flushAllTextModificationCounters(); @@ -1518,6 +1740,7 @@ public class LatinIME extends InputMethodService mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.length()); } + final boolean correcting = TextEntryState.isCorrecting(); InputConnection ic = getCurrentInputConnection(); if (ic != null) { ic.beginBatchEdit(); @@ -1540,7 +1763,12 @@ public class LatinIME extends InputMethodService } // If this is a punctuation, apply it through the normal key press - if (suggestion.length() == 1 && isWordSeparator(suggestion.charAt(0))) { + if (suggestion.length() == 1 && (isWordSeparator(suggestion.charAt(0)) + || 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); onKey(suggestion.charAt(0), null); if (ic != null) { ic.endBatchEdit(); @@ -1548,20 +1776,34 @@ public class LatinIME extends InputMethodService return; } mJustAccepted = true; - pickSuggestion(suggestion); + pickSuggestion(suggestion, correcting); // Add the word to the auto dictionary if it's not a known word if (index == 0) { - checkAddToDictionary(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); + addToDictionaries(suggestion, AutoDictionary.FREQUENCY_FOR_PICKED); + } else { + addToBigramDictionary(suggestion, 1); } + LatinImeLogger.logOnManualSuggestion(mComposing.toString(), suggestion.toString(), + index, suggestions); TextEntryState.acceptedSuggestion(mComposing.toString(), suggestion); // Follow it with a space - if (mAutoSpace) { + if (mAutoSpace && !correcting) { sendSpace(); mJustAddedAutoSpace = true; } - // Fool the state watcher so that a subsequent backspace will not do a revert - TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); - if (index == 0 && mCorrectionMode > 0 && !mSuggest.isValidWord(suggestion)) { + + // 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. + if (!correcting) { + TextEntryState.typedCharacter((char) KEYCODE_SPACE, true); + setNextSuggestions(); + } else { + // In case the cursor position doesn't change, make sure we show the suggestions again. + postUpdateOldSuggestions(); + } + if (index == 0 && mCorrectionMode > 0 && !mSuggest.isValidWord(suggestion) + && !mSuggest.isValidWord(suggestion.toString().toLowerCase())) { mCandidateView.showAddToDictionaryHint(suggestion); } if (ic != null) { @@ -1569,43 +1811,226 @@ public class LatinIME extends InputMethodService } } - private void pickSuggestion(CharSequence suggestion) { + private void rememberReplacedWord(CharSequence suggestion) { + if (mShowingVoiceSuggestions) { + // Retain the replaced word in the alternatives array. + EditingUtil.Range range = new EditingUtil.Range(); + String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(), + mWordSeparators, range); + if (!mWordToSuggestions.containsKey(wordToBeReplaced)) { + wordToBeReplaced = wordToBeReplaced.toLowerCase(); + } + if (mWordToSuggestions.containsKey(wordToBeReplaced)) { + List suggestions = mWordToSuggestions.get(wordToBeReplaced); + if (suggestions.contains(suggestion)) { + suggestions.remove(suggestion); + } + suggestions.add(wordToBeReplaced); + mWordToSuggestions.remove(wordToBeReplaced); + mWordToSuggestions.put(suggestion.toString(), suggestions); + } + } + } + + /** + * Commits the chosen word to the text field and saves it for later + * retrieval. + * @param suggestion the suggestion picked by the user to be committed to + * the text field + * @param correcting whether this is due to a correction of an existing + * word. + */ + private void pickSuggestion(CharSequence suggestion, boolean correcting) { + LatinKeyboardView inputView = mKeyboardSwitcher.getInputView(); if (mCapsLock) { suggestion = suggestion.toString().toUpperCase(); } else if (preferCapitalization() - || (mKeyboardSwitcher.isAlphabetMode() && mInputView.isShifted())) { + || (mKeyboardSwitcher.isAlphabetMode() + && inputView.isShifted())) { suggestion = suggestion.toString().toUpperCase().charAt(0) + suggestion.subSequence(1, suggestion.length()).toString(); } InputConnection ic = getCurrentInputConnection(); if (ic != null) { - if (mSuggestionShouldReplaceCurrentWord) { + rememberReplacedWord(suggestion); + // If text is in correction mode and we're not using composing + // text to underline, then the word at the cursor position needs + // to be removed before committing the correction + if (correcting && !MODIFY_TEXT_FOR_CORRECTION) { + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } EditingUtil.deleteWordAtCursor(ic, getWordSeparators()); } - if (!VoiceInput.DELETE_SYMBOL.equals(suggestion)) { - ic.commitText(suggestion, 1); - } + + ic.commitText(suggestion, 1); } + saveWordInHistory(suggestion); mPredicting = false; mCommittedLength = suggestion.length(); - ((LatinKeyboard) mInputView.getKeyboard()).setPreferredLetters(null); - setNextSuggestions(); + ((LatinKeyboard) inputView.getKeyboard()).setPreferredLetters(null); + // If we just corrected a word, then don't show punctuations + if (!correcting) { + setNextSuggestions(); + } updateShiftKeyState(getCurrentInputEditorInfo()); } + private void setOldSuggestions() { + // TODO: Inefficient to check if touching word and then get the touching word. Do it + // in one go. + mShowingVoiceSuggestions = false; + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + ic.beginBatchEdit(); + // If there is a selection, then undo the selection first. Unfortunately this causes + // a flicker. TODO: Add getSelectionText() to InputConnection API. + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } + if (!mPredicting && isCursorTouchingWord()) { + EditingUtil.Range range = new EditingUtil.Range(); + CharSequence touching = EditingUtil.getWordAtCursor(getCurrentInputConnection(), + mWordSeparators, range); + // If it's a selection, check if it's an entire word and no more, no less. + boolean fullword = EditingUtil.isFullWordOrInside(range, mLastSelectionStart, + mLastSelectionEnd); + if (fullword && touching != null && touching.length() > 1) { + // Strip out any trailing word separator + if (mWordSeparators.indexOf(touching.charAt(touching.length() - 1)) > 0) { + touching = touching.toString().substring(0, touching.length() - 1); + } + + // Search for result in spoken word alternatives + String selectedWord = touching.toString().trim(); + if (!mWordToSuggestions.containsKey(selectedWord)){ + selectedWord = selectedWord.toLowerCase(); + } + if (mWordToSuggestions.containsKey(selectedWord)){ + mShowingVoiceSuggestions = true; + underlineWord(touching, range.charsBefore, range.charsAfter); + List suggestions = mWordToSuggestions.get(selectedWord); + // If the first letter of touching is capitalized, make all the suggestions + // start with a capital letter. + if (Character.isUpperCase((char) touching.charAt(0))) { + for (int i=0; i< suggestions.size(); i++) { + String origSugg = (String) suggestions.get(i); + String capsSugg = origSugg.toUpperCase().charAt(0) + + origSugg.subSequence(1, origSugg.length()).toString(); + suggestions.set(i,capsSugg); + } + } + setSuggestions(suggestions, false, true, true); + setCandidatesViewShown(true); + TextEntryState.selectedForCorrection(); + ic.endBatchEdit(); + return; + } + + // If we didn't find a match, search for result in typed word history + WordComposer foundWord = null; + WordAlternatives alternatives = null; + for (WordAlternatives entry : mWordHistory) { + if (TextUtils.equals(entry.getChosenWord(), touching)) { + if (entry instanceof TypedWordAlternatives) { + foundWord = ((TypedWordAlternatives)entry).word; + } + alternatives = entry; + break; + } + } + // If we didn't find a match, at least suggest completions + if (foundWord == null && mSuggest.isValidWord(touching)) { + foundWord = new WordComposer(); + for (int i = 0; i < touching.length(); i++) { + foundWord.add(touching.charAt(i), new int[] { touching.charAt(i) }); + } + } + // Found a match, show suggestions + if (foundWord != null || alternatives != null) { + underlineWord(touching, range.charsBefore, range.charsAfter); + TextEntryState.selectedForCorrection(); + if (alternatives == null) alternatives = new TypedWordAlternatives(touching, + foundWord); + showCorrections(alternatives); + if (foundWord != null) { + mWord = new WordComposer(foundWord); + } else { + mWord.reset(); + } + // Revert the selection + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + ic.endBatchEdit(); + return; + } + abortCorrection(true); + } else { + abortCorrection(true); + setNextSuggestions(); + } + } else { + abortCorrection(true); + } + // Revert the selection + if (mLastSelectionStart < mLastSelectionEnd) { + ic.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + ic.endBatchEdit(); + } + private void setNextSuggestions() { setSuggestions(mSuggestPuncList, false, false, false); } - private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta) { + private void underlineWord(CharSequence word, int left, int right) { + InputConnection ic = getCurrentInputConnection(); + if (ic == null) return; + if (MODIFY_TEXT_FOR_CORRECTION) { + ic.finishComposingText(); + ic.deleteSurroundingText(left, right); + ic.setComposingText(word, 1); + } + ic.setSelection(mLastSelectionStart, mLastSelectionStart); + } + + private void addToDictionaries(CharSequence suggestion, int frequencyDelta) { + checkAddToDictionary(suggestion, frequencyDelta, false); + } + + private void addToBigramDictionary(CharSequence suggestion, int frequencyDelta) { + checkAddToDictionary(suggestion, frequencyDelta, true); + } + + /** + * Adds to the UserBigramDictionary and/or AutoDictionary + * @param addToBigramDictionary true if it should be added to bigram dictionary if possible + */ + private void checkAddToDictionary(CharSequence suggestion, int frequencyDelta, + boolean addToBigramDictionary) { + 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)) return; - if (mAutoDictionary.isValidWord(suggestion) - || (!mSuggest.isValidWord(suggestion.toString()) + if (!(mCorrectionMode == Suggest.CORRECTION_FULL + || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) { + return; + } + if (suggestion != null) { + if (!addToBigramDictionary && mAutoDictionary.isValidWord(suggestion) + || (!mSuggest.isValidWord(suggestion.toString()) && !mSuggest.isValidWord(suggestion.toString().toLowerCase()))) { - mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); + mAutoDictionary.addWord(suggestion.toString(), frequencyDelta); + } + + if (mUserBigramDictionary != null) { + CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(), + mSentenceSeparators); + if (!TextUtils.isEmpty(prevWord)) { + mUserBigramDictionary.addBigrams(prevWord.toString(), suggestion.toString()); + } + } } } @@ -1635,7 +2060,6 @@ public class LatinIME extends InputMethodService if (!mPredicting && length > 0) { final InputConnection ic = getCurrentInputConnection(); mPredicting = true; - ic.beginBatchEdit(); mJustRevertedSeparator = ic.getTextBeforeCursor(1, 0); if (deleteChar) ic.deleteSurroundingText(1, 0); int toDelete = mCommittedLength; @@ -1647,7 +2071,6 @@ public class LatinIME extends InputMethodService ic.deleteSurroundingText(toDelete, 0); ic.setComposingText(mComposing, 1); TextEntryState.backspace(); - ic.endBatchEdit(); postUpdateSuggestions(); } else { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); @@ -1664,7 +2087,7 @@ public class LatinIME extends InputMethodService return separators.contains(String.valueOf((char)code)); } - public boolean isSentenceSeparator(int code) { + private boolean isSentenceSeparator(int code) { return mSentenceSeparators.contains(String.valueOf((char)code)); } @@ -1688,7 +2111,7 @@ public class LatinIME extends InputMethodService ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)); CharSequence text = cm.getText(); if (!TextUtils.isEmpty(text)) { - mInputView.startPlaying(text.toString()); + mKeyboardSwitcher.getInputView().startPlaying(text.toString()); } } } @@ -1739,7 +2162,7 @@ public class LatinIME extends InputMethodService public void onRelease(int primaryCode) { // Reset any drag flags in the keyboard - ((LatinKeyboard) mInputView.getKeyboard()).keyReleased(); + ((LatinKeyboard) mKeyboardSwitcher.getInputView().getKeyboard()).keyReleased(); //vibrate(); } @@ -1791,7 +2214,7 @@ public class LatinIME extends InputMethodService // if mAudioManager is null, we don't have the ringer state yet // mAudioManager will be set by updateRingerMode if (mAudioManager == null) { - if (mInputView != null) { + if (mKeyboardSwitcher.getInputView() != null) { updateRingerMode(); } } @@ -1818,8 +2241,9 @@ public class LatinIME extends InputMethodService if (!mVibrateOn) { return; } - if (mInputView != null) { - mInputView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP, + if (mKeyboardSwitcher.getInputView() != null) { + mKeyboardSwitcher.getInputView().performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_TAP, HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } } @@ -1854,6 +2278,10 @@ public class LatinIME extends InputMethodService return mWord; } + boolean getPopupOn() { + return mPopupOn; + } + private void updateCorrectionMode() { mHasDictionary = mSuggest != null ? mSuggest.hasMainDictionary() : false; mAutoCorrectOn = (mAutoCorrectEnabled || mQuickFixes) @@ -1861,6 +2289,8 @@ public class LatinIME extends InputMethodService mCorrectionMode = (mAutoCorrectOn && mAutoCorrectEnabled) ? Suggest.CORRECTION_FULL : (mAutoCorrectOn ? Suggest.CORRECTION_BASIC : Suggest.CORRECTION_NONE); + mCorrectionMode = (mBigramSuggestionEnabled && mAutoCorrectOn && mAutoCorrectEnabled) + ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode; if (mSuggest != null) { mSuggest.setCorrectionMode(mCorrectionMode); } @@ -1877,7 +2307,7 @@ public class LatinIME extends InputMethodService launchSettings(LatinIMESettings.class); } - protected void launchSettings(Class settingsClass) { + protected void launchSettings(Class settingsClass) { handleClose(); Intent intent = new Intent(); intent.setClass(LatinIME.this, settingsClass); @@ -1890,6 +2320,8 @@ public class LatinIME extends InputMethodService SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); mVibrateOn = sp.getBoolean(PREF_VIBRATE_ON, false); mSoundOn = sp.getBoolean(PREF_SOUND_ON, false); + mPopupOn = sp.getBoolean(PREF_POPUP_ON, + mResources.getBoolean(R.bool.default_popup_preview)); mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true); mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true); mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false); @@ -1927,6 +2359,7 @@ public class LatinIME extends InputMethodService } mAutoCorrectEnabled = sp.getBoolean(PREF_AUTO_COMPLETE, mResources.getBoolean(R.bool.enable_autocorrect)) & mShowSuggestions; + mBigramSuggestionEnabled = sp.getBoolean(PREF_BIGRAM_SUGGESTIONS, true) & mShowSuggestions; updateCorrectionMode(); updateAutoTextEnabled(mResources.getConfiguration().locale); mLanguageSwitcher.loadLocales(sp); @@ -1934,14 +2367,18 @@ public class LatinIME extends InputMethodService private void initSuggestPuncList() { mSuggestPuncList = new ArrayList(); - String suggestPuncs = mResources.getString(R.string.suggested_punctuations); - if (suggestPuncs != null) { - for (int i = 0; i < suggestPuncs.length(); i++) { - mSuggestPuncList.add(suggestPuncs.subSequence(i, i + 1)); + mSuggestPuncs = mResources.getString(R.string.suggested_punctuations); + if (mSuggestPuncs != null) { + for (int i = 0; i < mSuggestPuncs.length(); i++) { + mSuggestPuncList.add(mSuggestPuncs.subSequence(i, i + 1)); } } } + private boolean isSuggestedPunctuation(int code) { + return mSuggestPuncs.contains(String.valueOf((char)code)); + } + private void showOptionsMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setCancelable(true); @@ -1970,7 +2407,7 @@ public class LatinIME extends InputMethodService mOptionsDialog = builder.create(); Window window = mOptionsDialog.getWindow(); WindowManager.LayoutParams lp = window.getAttributes(); - lp.token = mInputView.getWindowToken(); + lp.token = mKeyboardSwitcher.getInputView().getWindowToken(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; window.setAttributes(lp); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); @@ -1980,7 +2417,7 @@ public class LatinIME extends InputMethodService private void changeKeyboardMode() { mKeyboardSwitcher.toggleSymbols(); if (mCapsLock && mKeyboardSwitcher.isAlphabetMode()) { - ((LatinKeyboard) mInputView.getKeyboard()).setShiftLocked(mCapsLock); + mKeyboardSwitcher.setShiftLocked(mCapsLock); } updateShiftKeyState(getCurrentInputEditorInfo()); @@ -2010,19 +2447,17 @@ public class LatinIME extends InputMethodService p.println(" TextEntryState.state=" + TextEntryState.getState()); p.println(" mSoundOn=" + mSoundOn); p.println(" mVibrateOn=" + mVibrateOn); + p.println(" mPopupOn=" + mPopupOn); } // Characters per second measurement - private static final boolean PERF_DEBUG = false; private long mLastCpsTime; private static final int CPS_BUFFER_SIZE = 16; private long[] mCpsIntervals = new long[CPS_BUFFER_SIZE]; private int mCpsIndex; - private boolean mInputTypeNoAutoCorrect; private void measureCps() { - if (!LatinIME.PERF_DEBUG) return; long now = System.currentTimeMillis(); if (mLastCpsTime == 0) mLastCpsTime = now - 100; // Initial mCpsIntervals[mCpsIndex] = now - mLastCpsTime; diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java index 21b967420..806ef00af 100644 --- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java +++ b/java/src/com/android/inputmethod/latin/LatinIMESettings.java @@ -24,13 +24,13 @@ import android.app.Dialog; import android.app.backup.BackupManager; import android.content.DialogInterface; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; -import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; -import android.preference.Preference.OnPreferenceClickListener; import android.speech.SpeechRecognizer; import android.text.AutoText; import android.util.Log; @@ -43,11 +43,9 @@ public class LatinIMESettings extends PreferenceActivity DialogInterface.OnDismissListener { private static final String QUICK_FIXES_KEY = "quick_fixes"; - private static final String SHOW_SUGGESTIONS_KEY = "show_suggestions"; private static final String PREDICTION_SETTINGS_KEY = "prediction_settings"; private static final String VOICE_SETTINGS_KEY = "voice_mode"; - private static final String VOICE_ON_PRIMARY_KEY = "voice_on_main"; - private static final String VOICE_SERVER_KEY = "voice_server_url"; + private static final String DEBUG_MODE_KEY = "debug_mode"; private static final String TAG = "LatinIMESettings"; @@ -55,7 +53,7 @@ public class LatinIMESettings extends PreferenceActivity private static final int VOICE_INPUT_CONFIRM_DIALOG = 0; private CheckBoxPreference mQuickFixes; - private CheckBoxPreference mShowSuggestions; + private CheckBoxPreference mDebugMode; private ListPreference mVoicePreference; private boolean mVoiceOn; @@ -69,7 +67,6 @@ public class LatinIMESettings extends PreferenceActivity super.onCreate(icicle); addPreferencesFromResource(R.xml.prefs); mQuickFixes = (CheckBoxPreference) findPreference(QUICK_FIXES_KEY); - mShowSuggestions = (CheckBoxPreference) findPreference(SHOW_SUGGESTIONS_KEY); mVoicePreference = (ListPreference) findPreference(VOICE_SETTINGS_KEY); SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); prefs.registerOnSharedPreferenceChangeListener(this); @@ -77,6 +74,9 @@ public class LatinIMESettings extends PreferenceActivity mVoiceModeOff = getString(R.string.voice_mode_off); mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); mLogger = VoiceInputLogger.getLogger(this); + + mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY); + updateDebugMode(mDebugMode.isChecked()); } @Override @@ -110,11 +110,35 @@ public class LatinIMESettings extends PreferenceActivity .equals(mVoiceModeOff)) { showVoiceConfirmation(); } + } else if (key.equals(DEBUG_MODE_KEY)) { + updateDebugMode(prefs.getBoolean(DEBUG_MODE_KEY, false)); } mVoiceOn = !(prefs.getString(VOICE_SETTINGS_KEY, mVoiceModeOff).equals(mVoiceModeOff)); updateVoiceModeSummary(); } + private void updateDebugMode(boolean isDebugMode) { + if (mDebugMode == null) { + return; + } + String version = ""; + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + version = "Version " + info.versionName; + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not find version info."); + } + if (!isDebugMode) { + mDebugMode.setEnabled(false); + mDebugMode.setTitle(version); + mDebugMode.setSummary(""); + } else { + mDebugMode.setEnabled(true); + mDebugMode.setTitle(getResources().getString(R.string.prefs_debug_mode)); + mDebugMode.setSummary(version); + } + } + private void showVoiceConfirmation() { mOkClicked = false; showDialog(VOICE_INPUT_CONFIRM_DIALOG); diff --git a/java/src/com/android/inputmethod/latin/LatinIMEUtil.java b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java new file mode 100644 index 000000000..838b4fe10 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinIMEUtil.java @@ -0,0 +1,78 @@ +/* + * 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.os.AsyncTask; +import android.text.format.DateUtils; +import android.util.Log; + +public class LatinIMEUtil { + + /** + * Cancel an {@link AsyncTask}. + * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + */ + public static void cancelTask(AsyncTask task, boolean mayInterruptIfRunning) { + if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { + task.cancel(mayInterruptIfRunning); + } + } + + public static class GCUtils { + private static final String TAG = "GCUtils"; + public static final int GC_TRY_COUNT = 2; + // GC_TRY_LOOP_MAX is used for the hard limit of GC wait, + // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT. + public static final int GC_TRY_LOOP_MAX = 5; + private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS; + private static GCUtils sInstance = new GCUtils(); + private int mGCTryCount = 0; + + public static GCUtils getInstance() { + return sInstance; + } + + public void reset() { + mGCTryCount = 0; + } + + public boolean tryGCOrWait(String metaData, Throwable t) { + if (LatinImeLogger.sDBG) { + Log.d(TAG, "Encountered Exception or Error. Try GC."); + } + if (mGCTryCount == 0) { + System.gc(); + } + if (++mGCTryCount > GC_TRY_COUNT) { + LatinImeLogger.logOnException(metaData, t); + return false; + } else { + try { + Thread.sleep(GC_INTERVAL); + return true; + } catch (InterruptedException e) { + Log.e(TAG, "Sleep was interrupted."); + LatinImeLogger.logOnException(metaData, t); + return false; + } + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java new file mode 100644 index 000000000..19eead0a0 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -0,0 +1,847 @@ +/* + * 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 com.android.inputmethod.latin.Dictionary.DataType; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.AsyncTask; +import android.os.DropBoxManager; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.Pair; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "LatinIMELogs"; + public static boolean sDBG = false; + private static boolean sPRINTLOGGING = false; + // SUPPRESS_EXCEPTION should be true when released to public. + private static final boolean SUPPRESS_EXCEPTION = true; + // DEFAULT_LOG_ENABLED should be false when released to public. + private static final boolean DEFAULT_LOG_ENABLED = false; + + private static final long MINIMUMSENDINTERVAL = 300 * DateUtils.SECOND_IN_MILLIS; // 300 sec + private static final long MINIMUMCOUNTINTERVAL = 20 * DateUtils.SECOND_IN_MILLIS; // 20 sec + private static final long MINIMUMSENDSIZE = 40; + private static final char SEPARATER = ';'; + private static final char NULL_CHAR = '\uFFFC'; + private static final int EXCEPTION_MAX_LENGTH = 400; + + // ID_MANUALSUGGESTION has been replaced by ID_MANUALSUGGESTION_WITH_DATATYPE + // private static final int ID_MANUALSUGGESTION = 0; + private static final int ID_AUTOSUGGESTIONCANCELLED = 1; + private static final int ID_AUTOSUGGESTION = 2; + private static final int ID_INPUT_COUNT = 3; + private static final int ID_DELETE_COUNT = 4; + private static final int ID_WORD_COUNT = 5; + private static final int ID_ACTUAL_CHAR_COUNT = 6; + private static final int ID_THEME_ID = 7; + private static final int ID_SETTING_AUTO_COMPLETE = 8; + private static final int ID_VERSION = 9; + private static final int ID_EXCEPTION = 10; + private static final int ID_MANUALSUGGESTIONCOUNT = 11; + private static final int ID_AUTOSUGGESTIONCANCELLEDCOUNT = 12; + private static final int ID_AUTOSUGGESTIONCOUNT = 13; + private static final int ID_LANGUAGES = 14; + private static final int ID_MANUALSUGGESTION_WITH_DATATYPE = 15; + + private static final String PREF_ENABLE_LOG = "enable_logging"; + private static final String PREF_DEBUG_MODE = "debug_mode"; + private static final String PREF_AUTO_COMPLETE = "auto_complete"; + + public static boolean sLogEnabled = true; + /* package */ static LatinImeLogger sLatinImeLogger = new LatinImeLogger(); + // Store the last auto suggested word. + // This is required for a cancellation log of auto suggestion of that word. + /* package */ static String sLastAutoSuggestBefore; + /* package */ static String sLastAutoSuggestAfter; + /* package */ static String sLastAutoSuggestSeparator; + // This value holds MAIN, USER, AUTO, etc... + private static int sLastAutoSuggestDicTypeId; + // This value holds 0 (= unigram), 1 (= bigram) etc... + private static int sLastAutoSuggestDataType; + private static HashMap> sSuggestDicMap + = new HashMap>(); + private static String[] sPreviousWords; + private static DebugKeyEnabler sDebugKeyEnabler = new DebugKeyEnabler(); + + private ArrayList mLogBuffer = null; + private ArrayList mPrivacyLogBuffer = null; + /* package */ RingCharBuffer mRingCharBuffer = null; + + private Context mContext = null; + private DropBoxManager mDropBox = null; + private AddTextToDropBoxTask mAddTextToDropBoxTask; + private long mLastTimeActive; + private long mLastTimeSend; + private long mLastTimeCountEntry; + + private String mThemeId; + private String mSelectedLanguages; + private String mCurrentLanguage; + private int mDeleteCount; + private int mInputCount; + private int mWordCount; + private int[] mAutoSuggestCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int[] mManualSuggestCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int[] mAutoCancelledCountPerDic = new int[Suggest.DIC_TYPE_LAST_ID + 1]; + private int mActualCharCount; + + private static class LogEntry implements Comparable { + public final int mTag; + public final String[] mData; + public long mTime; + + public LogEntry (long time, int tag, String[] data) { + mTag = tag; + mTime = time; + mData = data; + } + + public int compareTo(LogEntry log2) { + if (mData.length == 0 && log2.mData.length == 0) { + return 0; + } else if (mData.length == 0) { + return 1; + } else if (log2.mData.length == 0) { + return -1; + } + return log2.mData[0].compareTo(mData[0]); + } + } + + private class AddTextToDropBoxTask extends AsyncTask { + private final DropBoxManager mDropBox; + private final long mTime; + private final String mData; + public AddTextToDropBoxTask(DropBoxManager db, long time, String data) { + mDropBox = db; + mTime = time; + mData = data; + } + @Override + protected Void doInBackground(Void... params) { + if (sPRINTLOGGING) { + Log.d(TAG, "Commit log: " + mData); + } + mDropBox.addText(TAG, mData); + return null; + } + @Override + protected void onPostExecute(Void v) { + mLastTimeSend = mTime; + } + } + + private void initInternal(Context context) { + mContext = context; + mDropBox = (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE); + mLastTimeSend = System.currentTimeMillis(); + mLastTimeActive = mLastTimeSend; + mLastTimeCountEntry = mLastTimeSend; + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + mLogBuffer = new ArrayList(); + mPrivacyLogBuffer = new ArrayList(); + mRingCharBuffer = new RingCharBuffer(context); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + sLogEnabled = prefs.getBoolean(PREF_ENABLE_LOG, DEFAULT_LOG_ENABLED); + mThemeId = prefs.getString(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT, + KeyboardSwitcher.DEFAULT_LAYOUT_ID); + mSelectedLanguages = prefs.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + mCurrentLanguage = prefs.getString(LatinIME.PREF_INPUT_LANGUAGE, ""); + sPRINTLOGGING = prefs.getBoolean(PREF_DEBUG_MODE, sPRINTLOGGING); + sDBG = sPRINTLOGGING; + prefs.registerOnSharedPreferenceChangeListener(this); + } + + /** + * Clear all logged data + */ + private void reset() { + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + mLogBuffer.clear(); + mPrivacyLogBuffer.clear(); + mRingCharBuffer.reset(); + } + + public void destroy() { + LatinIMEUtil.cancelTask(mAddTextToDropBoxTask, false); + } + + /** + * Check if the input string is safe as an entry or not. + */ + private static boolean checkStringDataSafe(String s) { + if (sDBG) { + Log.d(TAG, "Check String safety: " + s); + } + for (int i = 0; i < s.length(); ++i) { + if (Character.isDigit(s.charAt(i))) { + return false; + } + } + return true; + } + + private void addCountEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log counts. (4)"); + } + mLogBuffer.add(new LogEntry (time, ID_DELETE_COUNT, + new String[] {String.valueOf(mDeleteCount)})); + mLogBuffer.add(new LogEntry (time, ID_INPUT_COUNT, + new String[] {String.valueOf(mInputCount)})); + mLogBuffer.add(new LogEntry (time, ID_WORD_COUNT, + new String[] {String.valueOf(mWordCount)})); + mLogBuffer.add(new LogEntry (time, ID_ACTUAL_CHAR_COUNT, + new String[] {String.valueOf(mActualCharCount)})); + mDeleteCount = 0; + mInputCount = 0; + mWordCount = 0; + mActualCharCount = 0; + mLastTimeCountEntry = time; + } + + private void addSuggestionCountEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "log suggest counts. (1)"); + } + String[] s = new String[mAutoSuggestCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mAutoSuggestCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_AUTOSUGGESTIONCOUNT, s)); + + s = new String[mAutoCancelledCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mAutoCancelledCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_AUTOSUGGESTIONCANCELLEDCOUNT, s)); + + s = new String[mManualSuggestCountPerDic.length]; + for (int i = 0; i < s.length; ++i) { + s[i] = String.valueOf(mManualSuggestCountPerDic[i]); + } + mLogBuffer.add(new LogEntry(time, ID_MANUALSUGGESTIONCOUNT, s)); + + Arrays.fill(mAutoSuggestCountPerDic, 0); + Arrays.fill(mManualSuggestCountPerDic, 0); + Arrays.fill(mAutoCancelledCountPerDic, 0); + } + + private void addThemeIdEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log theme Id. (1)"); + } + // TODO: Not to convert theme ID here. Currently "2" is treated as "6" in a log server. + if (mThemeId.equals("2")) { + mThemeId = "6"; + } else if (mThemeId.equals("3")) { + mThemeId = "7"; + } + mLogBuffer.add(new LogEntry (time, ID_THEME_ID, + new String[] {mThemeId})); + } + + private void addLanguagesEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log language settings. (1)"); + } + // CurrentLanguage and SelectedLanguages will be blank if user doesn't use multi-language + // switching. + if (TextUtils.isEmpty(mCurrentLanguage)) { + mCurrentLanguage = mContext.getResources().getConfiguration().locale.toString(); + } + mLogBuffer.add(new LogEntry (time, ID_LANGUAGES, + new String[] {mCurrentLanguage , mSelectedLanguages})); + } + + private void addSettingsEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log settings. (1)"); + } + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + mLogBuffer.add(new LogEntry (time, ID_SETTING_AUTO_COMPLETE, + new String[] {String.valueOf(prefs.getBoolean(PREF_AUTO_COMPLETE, + mContext.getResources().getBoolean(R.bool.enable_autocorrect)))})); + } + + private void addVersionNameEntry(long time) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log Version. (1)"); + } + try { + PackageInfo info = mContext.getPackageManager().getPackageInfo( + mContext.getPackageName(), 0); + mLogBuffer.add(new LogEntry (time, ID_VERSION, + new String[] {String.valueOf(info.versionCode), info.versionName})); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not find version name."); + } + } + + private void addExceptionEntry(long time, String[] data) { + if (sPRINTLOGGING) { + Log.d(TAG, "Log Exception. (1)"); + } + mLogBuffer.add(new LogEntry(time, ID_EXCEPTION, data)); + } + + private void flushPrivacyLogSafely() { + if (sPRINTLOGGING) { + Log.d(TAG, "Log obfuscated data. (" + mPrivacyLogBuffer.size() + ")"); + } + long now = System.currentTimeMillis(); + Collections.sort(mPrivacyLogBuffer); + for (LogEntry l: mPrivacyLogBuffer) { + l.mTime = now; + mLogBuffer.add(l); + } + mPrivacyLogBuffer.clear(); + } + + /** + * Add an entry + * @param tag + * @param data + */ + private void addData(int tag, Object data) { + switch (tag) { + case ID_DELETE_COUNT: + if (((mLastTimeActive - mLastTimeCountEntry) > MINIMUMCOUNTINTERVAL) + || (mDeleteCount == 0 && mInputCount == 0)) { + addCountEntry(mLastTimeActive); + } + mDeleteCount += (Integer)data; + break; + case ID_INPUT_COUNT: + if (((mLastTimeActive - mLastTimeCountEntry) > MINIMUMCOUNTINTERVAL) + || (mDeleteCount == 0 && mInputCount == 0)) { + addCountEntry(mLastTimeActive); + } + mInputCount += (Integer)data; + break; + case ID_MANUALSUGGESTION_WITH_DATATYPE: + case ID_AUTOSUGGESTION: + ++mWordCount; + String[] dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + mActualCharCount += dataStrings[1].length(); + if (checkStringDataSafe(dataStrings[0]) && checkStringDataSafe(dataStrings[1])) { + mPrivacyLogBuffer.add( + new LogEntry (System.currentTimeMillis(), tag, dataStrings)); + } else { + if (sDBG) { + Log.d(TAG, "Skipped to add an entry because data is unsafe."); + } + } + break; + case ID_AUTOSUGGESTIONCANCELLED: + --mWordCount; + dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + mActualCharCount -= dataStrings[1].length(); + if (checkStringDataSafe(dataStrings[0]) && checkStringDataSafe(dataStrings[1])) { + mPrivacyLogBuffer.add( + new LogEntry (System.currentTimeMillis(), tag, dataStrings)); + } else { + if (sDBG) { + Log.d(TAG, "Skipped to add an entry because data is unsafe."); + } + } + break; + case ID_EXCEPTION: + dataStrings = (String[]) data; + if (dataStrings.length < 2) { + if (sDBG) { + Log.e(TAG, "The length of logged string array is invalid."); + } + break; + } + addExceptionEntry(System.currentTimeMillis(), dataStrings); + break; + default: + if (sDBG) { + Log.e(TAG, "Log Tag is not entried."); + } + break; + } + } + + private void commitInternal() { + // if there is no log entry in mLogBuffer, will not send logs to DropBox. + if (!mLogBuffer.isEmpty() && (mAddTextToDropBoxTask == null + || mAddTextToDropBoxTask.getStatus() == AsyncTask.Status.FINISHED)) { + if (sPRINTLOGGING) { + Log.d(TAG, "Commit (" + mLogBuffer.size() + ")"); + } + flushPrivacyLogSafely(); + long now = System.currentTimeMillis(); + addCountEntry(now); + addThemeIdEntry(now); + addLanguagesEntry(now); + addSettingsEntry(now); + addVersionNameEntry(now); + addSuggestionCountEntry(now); + String s = LogSerializer.createStringFromEntries(mLogBuffer); + reset(); + mAddTextToDropBoxTask = (AddTextToDropBoxTask) new AddTextToDropBoxTask( + mDropBox, now, s).execute(); + } + } + + private void commitInternalAndStopSelf() { + if (sDBG) { + Log.e(TAG, "Exception was thrown and let's die."); + } + commitInternal(); + LatinIME ime = ((LatinIME) mContext); + ime.hideWindow(); + ime.stopSelf(); + } + + private synchronized void sendLogToDropBox(int tag, Object s) { + long now = System.currentTimeMillis(); + if (sDBG) { + String out = ""; + if (s instanceof String[]) { + for (String str: ((String[]) s)) { + out += str + ","; + } + } else if (s instanceof Integer) { + out += (Integer) s; + } + Log.d(TAG, "SendLog: " + tag + ";" + out + ", will be sent after " + + (- (now - mLastTimeSend - MINIMUMSENDINTERVAL) / 1000) + " sec."); + } + if (now - mLastTimeActive > MINIMUMSENDINTERVAL) { + // Send a log before adding an log entry if the last data is too old. + commitInternal(); + addData(tag, s); + } else if (now - mLastTimeSend > MINIMUMSENDINTERVAL) { + // Send a log after adding an log entry. + addData(tag, s); + commitInternal(); + } else { + addData(tag, s); + } + mLastTimeActive = now; + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (PREF_ENABLE_LOG.equals(key)) { + if (sharedPreferences.getBoolean(key, DEFAULT_LOG_ENABLED)) { + sLogEnabled = (mContext != null); + } else { + sLogEnabled = false; + } + if (sDebugKeyEnabler.check()) { + sharedPreferences.edit().putBoolean(PREF_DEBUG_MODE, true).commit(); + } + } else if (KeyboardSwitcher.PREF_KEYBOARD_LAYOUT.equals(key)) { + mThemeId = sharedPreferences.getString(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT, + KeyboardSwitcher.DEFAULT_LAYOUT_ID); + addThemeIdEntry(mLastTimeActive); + } else if (PREF_DEBUG_MODE.equals(key)) { + sPRINTLOGGING = sharedPreferences.getBoolean(PREF_DEBUG_MODE, sPRINTLOGGING); + sDBG = sPRINTLOGGING; + } else if (LatinIME.PREF_INPUT_LANGUAGE.equals(key)) { + mCurrentLanguage = sharedPreferences.getString(LatinIME.PREF_INPUT_LANGUAGE, ""); + addLanguagesEntry(mLastTimeActive); + } else if (LatinIME.PREF_INPUT_LANGUAGE.equals(key)) { + mSelectedLanguages = sharedPreferences.getString(LatinIME.PREF_SELECTED_LANGUAGES, ""); + } + } + + public static void init(Context context) { + sLatinImeLogger.initInternal(context); + } + + public static void commit() { + if (sLogEnabled) { + if (System.currentTimeMillis() - sLatinImeLogger.mLastTimeActive > MINIMUMCOUNTINTERVAL + || (sLatinImeLogger.mLogBuffer.size() + + sLatinImeLogger.mPrivacyLogBuffer.size() > MINIMUMSENDSIZE)) { + sLatinImeLogger.commitInternal(); + } + } + } + + public static void onDestroy() { + sLatinImeLogger.commitInternal(); + sLatinImeLogger.destroy(); + } + + // TODO: Handle CharSequence instead of String + public static void logOnManualSuggestion(String before, String after, int position + , List suggestions) { + if (sLogEnabled) { + // log punctuation + if (before.length() == 0 && after.length() == 1) { + sLatinImeLogger.sendLogToDropBox(ID_MANUALSUGGESTION_WITH_DATATYPE, new String[] { + before, after, String.valueOf(position), ""}); + } else if (!sSuggestDicMap.containsKey(after)) { + if (sDBG) { + Log.e(TAG, "logOnManualSuggestion was cancelled: from unknown dic."); + } + } else { + int dicTypeId = sSuggestDicMap.get(after).first; + sLatinImeLogger.mManualSuggestCountPerDic[dicTypeId]++; + if (dicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnManualSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + sPreviousWords = null; + } + // TODO: Don't send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + + /* Example: + * When user typed "Illegal imm" and picked "immigrants", + * the suggestion list has "immigrants, immediate, immigrant". + * At this time, the log strings will be something like below: + * strings[0 = COLUMN_BEFORE_ID] = imm + * strings[1 = COLUMN_AFTER_ID] = immigrants + * strings[2 = COLUMN_PICKED_POSITION_ID] = 0 + * strings[3 = COLUMN_SUGGESTION_LENGTH_ID] = 3 + * strings[4 = COLUMN_PREVIOUS_WORDS_COUNT_ID] = 1 + * strings[5] = immigrants + * strings[6] = immediate + * strings[7] = immigrant + * strings[8] = 1 (= bigram) + * strings[9] = 0 (= unigram) + * strings[10] = 1 (= bigram) + * strings[11] = Illegal + */ + + // 0 for unigram, 1 for bigram, 2 for trigram... + int previousWordsLength = (sPreviousWords == null) ? 0 : sPreviousWords.length; + int suggestionLength = suggestions.size(); + + final int COLUMN_BEFORE_ID = 0; + final int COLUMN_AFTER_ID = 1; + final int COLUMN_PICKED_POSITION_ID = 2; + final int COLUMN_SUGGESTION_LENGTH_ID = 3; + final int COLUMN_PREVIOUS_WORDS_COUNT_ID = 4; + final int BASE_COLUMN_SIZE = 5; + + String[] strings = + new String[BASE_COLUMN_SIZE + suggestionLength * 2 + previousWordsLength]; + strings[COLUMN_BEFORE_ID] = before; + strings[COLUMN_AFTER_ID] = after; + strings[COLUMN_PICKED_POSITION_ID] = String.valueOf(position); + strings[COLUMN_SUGGESTION_LENGTH_ID] = String.valueOf(suggestionLength); + strings[COLUMN_PREVIOUS_WORDS_COUNT_ID] = String.valueOf(previousWordsLength); + + for (int i = 0; i < suggestionLength; ++i) { + String s = suggestions.get(i).toString(); + if (sSuggestDicMap.containsKey(s)) { + strings[BASE_COLUMN_SIZE + i] = s; + strings[BASE_COLUMN_SIZE + suggestionLength + i] + = sSuggestDicMap.get(s).second.toString(); + } else { + strings[BASE_COLUMN_SIZE + i] = ""; + strings[BASE_COLUMN_SIZE + suggestionLength + i] = ""; + } + } + + for (int i = 0; i < previousWordsLength; ++i) { + strings[BASE_COLUMN_SIZE + suggestionLength * 2 + i] = sPreviousWords[i]; + } + + sLatinImeLogger.sendLogToDropBox(ID_MANUALSUGGESTION_WITH_DATATYPE, strings); + } + } + sSuggestDicMap.clear(); + } + } + + public static void logOnAutoSuggestion(String before, String after) { + if (sLogEnabled) { + if (!sSuggestDicMap.containsKey(after)) { + if (sDBG) { + Log.e(TAG, "logOnAutoSuggestion was cancelled: from unknown dic."); + } + } else { + String separator = String.valueOf(sLatinImeLogger.mRingCharBuffer.getLastChar()); + sLastAutoSuggestDicTypeId = sSuggestDicMap.get(after).first; + sLastAutoSuggestDataType = sSuggestDicMap.get(after).second; + sLatinImeLogger.mAutoSuggestCountPerDic[sLastAutoSuggestDicTypeId]++; + if (sLastAutoSuggestDicTypeId != Suggest.DIC_MAIN) { + if (sDBG) { + Log.d(TAG, "logOnAutoSuggestion was cancelled: not from main dic."); + } + before = ""; + after = ""; + sPreviousWords = null; + } + // TODO: Not to send a log if this doesn't come from Main Dictionary. + { + if (before.equals(after)) { + before = ""; + after = ""; + } + int previousWordsLength = (sPreviousWords == null) ? 0 : sPreviousWords.length; + + final int COLUMN_BEFORE_ID = 0; + final int COLUMN_AFTER_ID = 1; + final int COLUMN_SEPARATOR_ID = 2; + final int COLUMN_DATA_TYPE_ID = 3; + final int BASE_COLUMN_SIZE = 4; + + String[] strings = new String[4 + previousWordsLength]; + strings[COLUMN_BEFORE_ID] = before; + strings[COLUMN_AFTER_ID] = after; + strings[COLUMN_SEPARATOR_ID] = separator; + strings[COLUMN_DATA_TYPE_ID] = String.valueOf(sLastAutoSuggestDataType); + for (int i = 0; i < previousWordsLength; ++i) { + strings[BASE_COLUMN_SIZE + i] = sPreviousWords[i]; + } + sLatinImeLogger.sendLogToDropBox(ID_AUTOSUGGESTION, strings); + } + synchronized (LatinImeLogger.class) { + sLastAutoSuggestBefore = before; + sLastAutoSuggestAfter = after; + sLastAutoSuggestSeparator = separator; + } + } + sSuggestDicMap.clear(); + } + } + + public static void logOnAutoSuggestionCanceled() { + if (sLogEnabled) { + sLatinImeLogger.mAutoCancelledCountPerDic[sLastAutoSuggestDicTypeId]++; + if (sLastAutoSuggestBefore != null && sLastAutoSuggestAfter != null) { + String[] strings = new String[] { + sLastAutoSuggestBefore, sLastAutoSuggestAfter, sLastAutoSuggestSeparator}; + sLatinImeLogger.sendLogToDropBox(ID_AUTOSUGGESTIONCANCELLED, strings); + } + synchronized (LatinImeLogger.class) { + sLastAutoSuggestBefore = ""; + sLastAutoSuggestAfter = ""; + sLastAutoSuggestSeparator = ""; + } + } + } + + public static void logOnDelete() { + if (sLogEnabled) { + String mLastWord = sLatinImeLogger.mRingCharBuffer.getLastString(); + if (!TextUtils.isEmpty(mLastWord) + && mLastWord.equalsIgnoreCase(sLastAutoSuggestBefore)) { + logOnAutoSuggestionCanceled(); + } + sLatinImeLogger.mRingCharBuffer.pop(); + sLatinImeLogger.sendLogToDropBox(ID_DELETE_COUNT, 1); + } + } + + public static void logOnInputChar(char c) { + if (sLogEnabled) { + sLatinImeLogger.mRingCharBuffer.push(c); + sLatinImeLogger.sendLogToDropBox(ID_INPUT_COUNT, 1); + } + } + + public static void logOnException(String metaData, Throwable e) { + if (sLogEnabled) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(baos); + e.printStackTrace(ps); + String exceptionString = URLEncoder.encode(new String(baos.toByteArray(), 0, + Math.min(EXCEPTION_MAX_LENGTH, baos.size()))); + sLatinImeLogger.sendLogToDropBox( + ID_EXCEPTION, new String[] {metaData, exceptionString}); + if (sDBG) { + Log.e(TAG, "Exception: " + new String(baos.toByteArray())+ ":" + exceptionString); + } + if (SUPPRESS_EXCEPTION) { + sLatinImeLogger.commitInternalAndStopSelf(); + } else { + sLatinImeLogger.commitInternal(); + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else if (e instanceof Error) { + throw (Error) e; + } + } + } + } + + public static void logOnWarning(String warning) { + if (sLogEnabled) { + sLatinImeLogger.sendLogToDropBox( + ID_EXCEPTION, new String[] {warning, ""}); + } + } + + // TODO: This code supports only Bigram. + public static void onStartSuggestion(CharSequence previousWords) { + if (sLogEnabled) { + sSuggestDicMap.clear(); + sPreviousWords = new String[] { + (previousWords == null) ? "" : previousWords.toString()}; + } + } + + public static void onAddSuggestedWord(String word, int typeId, DataType dataType) { + if (sLogEnabled) { + sSuggestDicMap.put(word, new Pair(typeId, dataType.ordinal())); + } + } + + private static class LogSerializer { + private static void appendWithLength(StringBuffer sb, String data) { + sb.append(data.length()); + sb.append(SEPARATER); + sb.append(data); + sb.append(SEPARATER); + } + + private static void appendLogEntry(StringBuffer sb, String time, String tag, + String[] data) { + if (data.length > 0) { + appendWithLength(sb, String.valueOf(data.length + 2)); + appendWithLength(sb, time); + appendWithLength(sb, tag); + for (String s: data) { + appendWithLength(sb, s); + } + } + } + + public static String createStringFromEntries(ArrayList logs) { + StringBuffer sb = new StringBuffer(); + for (LogEntry log: logs) { + appendLogEntry(sb, String.valueOf(log.mTime), String.valueOf(log.mTag), log.mData); + } + return sb.toString(); + } + } + + /* package */ static class RingCharBuffer { + final int BUFSIZE = 20; + private Context mContext; + private int mEnd = 0; + /* package */ int length = 0; + private char[] mCharBuf = new char[BUFSIZE]; + + public RingCharBuffer(Context context) { + mContext = context; + } + + private int normalize(int in) { + int ret = in % BUFSIZE; + return ret < 0 ? ret + BUFSIZE : ret; + } + public void push(char c) { + mCharBuf[mEnd] = c; + mEnd = normalize(mEnd + 1); + if (length < BUFSIZE) { + ++length; + } + } + public char pop() { + if (length < 1) { + return NULL_CHAR; + } else { + mEnd = normalize(mEnd - 1); + --length; + return mCharBuf[mEnd]; + } + } + public char getLastChar() { + if (length < 1) { + return NULL_CHAR; + } else { + return mCharBuf[normalize(mEnd - 1)]; + } + } + public String getLastString() { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; ++i) { + char c = mCharBuf[normalize(mEnd - 1 - i)]; + if (!((LatinIME)mContext).isWordSeparator(c)) { + sb.append(c); + } else { + break; + } + } + return sb.reverse().toString(); + } + public void reset() { + length = 0; + } + } + + private static class DebugKeyEnabler { + private int mCounter = 0; + private long mLastTime = 0; + public boolean check() { + if (System.currentTimeMillis() - mLastTime > 10 * 1000) { + mCounter = 0; + mLastTime = System.currentTimeMillis(); + } else if (++mCounter >= 10) { + return true; + } + return false; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboard.java b/java/src/com/android/inputmethod/latin/LatinKeyboard.java index 6aea5d13a..db4d167d4 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboard.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboard.java @@ -47,7 +47,6 @@ public class LatinKeyboard extends Keyboard { private Drawable mShiftLockIcon; private Drawable mShiftLockPreviewIcon; private Drawable mOldShiftIcon; - private Drawable mOldShiftPreviewIcon; private Drawable mSpaceIcon; private Drawable mSpacePreviewIcon; private Drawable mMicIcon; @@ -68,7 +67,6 @@ public class LatinKeyboard extends Keyboard { private LanguageSwitcher mLanguageSwitcher; private Resources mRes; private Context mContext; - private int mMode; // Whether this keyboard has voice icon on it private boolean mHasVoiceButton; // Whether voice icon is enabled at all @@ -77,16 +75,16 @@ public class LatinKeyboard extends Keyboard { private CharSequence m123Label; private boolean mCurrentlyInSpace; private SlidingLocaleDrawable mSlidingLocaleIcon; - private Rect mBounds = new Rect(); private int[] mPrefLetterFrequencies; - private boolean mPreemptiveCorrection; private int mPrefLetter; private int mPrefLetterX; private int mPrefLetterY; private int mPrefDistance; - private int mExtensionResId; - + private int mExtensionResId; + // TODO: generalize for any keyboardId + private boolean mIsBlackSym; + private static final int SHIFT_OFF = 0; private static final int SHIFT_ON = 1; private static final int SHIFT_LOCKED = 2; @@ -107,7 +105,6 @@ public class LatinKeyboard extends Keyboard { super(context, xmlLayoutResId, mode); final Resources res = context.getResources(); mContext = context; - mMode = mode; mRes = res; mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); @@ -126,7 +123,8 @@ public class LatinKeyboard extends Keyboard { setDefaultBounds(m123MicPreviewIcon); sSpacebarVerticalCorrection = res.getDimensionPixelOffset( R.dimen.spacebar_vertical_correction); - mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty; + mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty + || xmlLayoutResId == R.xml.kbd_qwerty_black; mSpaceKeyIndex = indexOf((int) ' '); } @@ -182,8 +180,8 @@ public class LatinKeyboard extends Keyboard { case EditorInfo.IME_ACTION_SEARCH: mEnterKey.iconPreview = res.getDrawable( R.drawable.sym_keyboard_feedback_search); - mEnterKey.icon = res.getDrawable( - R.drawable.sym_keyboard_search); + mEnterKey.icon = res.getDrawable(mIsBlackSym ? + R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search); mEnterKey.label = null; break; case EditorInfo.IME_ACTION_SEND: @@ -201,8 +199,8 @@ public class LatinKeyboard extends Keyboard { } else { mEnterKey.iconPreview = res.getDrawable( R.drawable.sym_keyboard_feedback_return); - mEnterKey.icon = res.getDrawable( - R.drawable.sym_keyboard_return); + mEnterKey.icon = res.getDrawable(mIsBlackSym ? + R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return); mEnterKey.label = null; } break; @@ -224,7 +222,6 @@ public class LatinKeyboard extends Keyboard { ((LatinKey)mShiftKey).enableShiftLock(); } mOldShiftIcon = mShiftKey.icon; - mOldShiftPreviewIcon = mShiftKey.iconPreview; } } @@ -277,6 +274,10 @@ public class LatinKeyboard extends Keyboard { } } + /* package */ boolean isAlphaKeyboard() { + return mIsAlphaKeyboard; + } + public void setExtension(int resId) { mExtensionResId = resId; } @@ -285,6 +286,26 @@ public class LatinKeyboard extends Keyboard { return mExtensionResId; } + public void setBlackFlag(boolean f) { + mIsBlackSym = f; + if (f) { + mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked); + mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space); + mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic); + m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic); + } else { + mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked); + mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space); + mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic); + m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic); + } + updateF1Key(); + if (mSpaceKey != null) { + mSpaceKey.icon = mSpaceIcon; + updateSpaceBarForLocale(f); + } + } + private void setDefaultBounds(Drawable drawable) { drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); } @@ -322,39 +343,40 @@ public class LatinKeyboard extends Keyboard { } } - private void updateSpaceBarForLocale() { + private void updateSpaceBarForLocale(boolean isBlack) { if (mLocale != null) { // Create the graphic for spacebar Bitmap buffer = Bitmap.createBitmap(mSpaceKey.width, mSpaceIcon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(buffer); - drawSpaceBar(canvas, buffer.getWidth(), buffer.getHeight(), 255); + drawSpaceBar(canvas, buffer.getWidth(), buffer.getHeight(), 255, isBlack); mSpaceKey.icon = new BitmapDrawable(mRes, buffer); mSpaceKey.repeatable = mLanguageSwitcher.getLocaleCount() < 2; } else { - mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space); + mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space) + : mRes.getDrawable(R.drawable.sym_keyboard_space); mSpaceKey.repeatable = true; } } - private void drawSpaceBar(Canvas canvas, int width, int height, int opacity) { - canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + private void drawSpaceBar(Canvas canvas, int width, int height, int opacity, boolean isBlack) { + canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setAlpha(opacity); // Get the text size from the theme paint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14)); paint.setTextAlign(Align.CENTER); - //// Draw a drop shadow for the text - //paint.setShadowLayer(2f, 0, 0, 0xFF000000); final String language = getInputLanguage(mSpaceKey.width, paint); final int ascent = (int) -paint.ascent(); - paint.setColor(0x80000000); - canvas.drawText(language, - width / 2, ascent - 1, paint); - paint.setColor(0xFF808080); - canvas.drawText(language, - width / 2, ascent, paint); + + int shadowColor = isBlack ? mRes.getColor(R.color.latinkeyboard_bar_language_shadow_black) + : mRes.getColor(R.color.latinkeyboard_bar_language_shadow_white); + + paint.setColor(shadowColor); + canvas.drawText(language, width / 2, ascent - 1, paint); + paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text)); + canvas.drawText(language, width / 2, ascent, paint); // Put arrows on either side of the text if (mLanguageSwitcher.getLocaleCount() > 1) { Rect bounds = new Rect(); @@ -439,7 +461,7 @@ public class LatinKeyboard extends Keyboard { } if (mLocale != null && mLocale.equals(locale)) return; mLocale = locale; - updateSpaceBarForLocale(); + updateSpaceBarForLocale(mIsBlackSym); } boolean isCurrentlyInSpace() { @@ -503,9 +525,10 @@ public class LatinKeyboard extends Keyboard { // Handle preferred next letter final int[] pref = mPrefLetterFrequencies; if (mPrefLetter > 0) { - if (DEBUG_PREFERRED_LETTER && mPrefLetter == code - && !key.isInsideSuper(x, y)) { - Log.d(TAG, "CORRECTED !!!!!!"); + if (DEBUG_PREFERRED_LETTER) { + if (mPrefLetter == code && !key.isInsideSuper(x, y)) { + Log.d(TAG, "CORRECTED !!!!!!"); + } } return mPrefLetter == code; } else { @@ -684,7 +707,7 @@ public class LatinKeyboard extends Keyboard { mTextPaint = new TextPaint(); int textSize = getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18); mTextPaint.setTextSize(textSize); - mTextPaint.setColor(0); + mTextPaint.setColor(R.color.latinkeyboard_transparent); mTextPaint.setTextAlign(Align.CENTER); mTextPaint.setAlpha(255); mTextPaint.setAntiAlias(true); @@ -718,7 +741,7 @@ public class LatinKeyboard extends Keyboard { public void draw(Canvas canvas) { canvas.save(); if (mHitThreshold) { - mTextPaint.setColor(0xFF000000); + mTextPaint.setColor(mRes.getColor(R.color.latinkeyboard_text_color)); canvas.clipRect(0, 0, mWidth, mHeight); if (mCurrentLanguage == null) { mCurrentLanguage = getInputLanguage(mWidth, mTextPaint); diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java new file mode 100644 index 000000000..665c641c2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java @@ -0,0 +1,1633 @@ +/* + * 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.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.Paint.Align; +import android.graphics.Region.Op; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup.LayoutParams; +import android.widget.PopupWindow; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and + * detecting key presses and touch movements. + * + * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground + * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout + * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset + * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize + * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize + * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor + * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection + * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout + */ +public class LatinKeyboardBaseView extends View implements View.OnClickListener { + + public interface OnKeyboardActionListener { + + /** + * Called when the user presses a key. This is sent before the + * {@link #onKey} is called. For keys that repeat, this is only + * called once. + * + * @param primaryCode + * the unicode of the key being pressed. If the touch is + * not on a valid key, the value will be zero. + */ + void onPress(int primaryCode); + + /** + * Called when the user releases a key. This is sent after the + * {@link #onKey} is called. For keys that repeat, this is only + * called once. + * + * @param primaryCode + * the code of the key that was released + */ + void onRelease(int primaryCode); + + /** + * Send a key press to the listener. + * + * @param primaryCode + * this is 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. + */ + void onKey(int primaryCode, int[] keyCodes); + + /** + * Sends a sequence of characters to the listener. + * + * @param text + * the sequence of characters to be displayed. + */ + void onText(CharSequence text); + + /** + * Called when the user quickly moves the finger from right to + * left. + */ + void swipeLeft(); + + /** + * Called when the user quickly moves the finger from left to + * right. + */ + void swipeRight(); + + /** + * Called when the user quickly moves the finger from up to down. + */ + void swipeDown(); + + /** + * Called when the user quickly moves the finger from down to up. + */ + void swipeUp(); + } + + private static final boolean DEBUG = false; + private static final int NOT_A_KEY = -1; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; + + private Keyboard mKeyboard; + private int mCurrentKeyIndex = NOT_A_KEY; + private int mLabelTextSize; + private int mKeyTextSize; + private int mKeyTextColor; + private float mShadowRadius; + private int mShadowColor; + private float mBackgroundDimAmount; + + private TextView mPreviewText; + private PopupWindow mPreviewPopup; + private int mPreviewTextSizeLarge; + private int mPreviewOffset; + private int mPreviewHeight; + private int[] mOffsetInWindow; + + private PopupWindow mPopupKeyboard; + private View mMiniKeyboardContainer; + private LatinKeyboardBaseView mMiniKeyboard; + private boolean mMiniKeyboardOnScreen; + private View mPopupParent; + private int mMiniKeyboardOffsetX; + private int mMiniKeyboardOffsetY; + private Map mMiniKeyboardCache; + private int[] mWindowOffset; + private Key[] mKeys; + private Typeface mKeyTextStyle = Typeface.DEFAULT; + private int mSymbolColorScheme = 0; + + /** Listener for {@link OnKeyboardActionListener}. */ + private OnKeyboardActionListener mKeyboardActionListener; + + private static final int DELAY_BEFORE_PREVIEW = 0; + private static final int DELAY_AFTER_PREVIEW = 70; + private static final int DEBOUNCE_TIME = 70; + + private int mVerticalCorrection; + private int mProximityThreshold; + + private boolean mPreviewCentered = false; + private boolean mShowPreview = true; + private boolean mShowTouchPoints = true; + private int mPopupPreviewX; + private int mPopupPreviewY; + private int mWindowY; + + private boolean mProximityCorrectOn; + + private Paint mPaint; + private Rect mPadding; + + private int mCurrentKey = NOT_A_KEY; + private int mDownKey = NOT_A_KEY; + private int mStartX; + private int mStartY; + + private KeyDebouncer mDebouncer; + + private GestureDetector mGestureDetector; + private int mPopupX; + private int mPopupY; + private int mRepeatKeyIndex = NOT_A_KEY; + private int mPopupLayout; + private boolean mAbortKey; + private Key mInvalidatedKey; + private Rect mClipRegion = new Rect(0, 0, 0, 0); + private boolean mPossiblePoly; + private SwipeTracker mSwipeTracker = new SwipeTracker(); + private int mSwipeThreshold; + private boolean mDisambiguateSwipe; + + // Variables for dealing with multiple pointers + private int mOldPointerCount = 1; + private float mOldPointerX; + private float mOldPointerY; + + private Drawable mKeyBackground; + + private static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int REPEAT_START_DELAY = 400; + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + + private static int MAX_NEARBY_KEYS = 12; + private int[] mDistances = new int[MAX_NEARBY_KEYS]; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private StringBuilder mPreviewLabel = new StringBuilder(1); + + /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ + private boolean mDrawPending; + /** The dirty region in the keyboard bitmap */ + private Rect mDirtyRect = new Rect(); + /** The keyboard bitmap for faster updates */ + private Bitmap mBuffer; + /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */ + private boolean mKeyboardChanged; + /** The canvas for the above mutable keyboard bitmap */ + private Canvas mCanvas; + + UIHandler mHandler = new UIHandler(); + + class UIHandler extends Handler { + private static final int MSG_POPUP_PREVIEW = 1; + private static final int MSG_DISMISS_PREVIEW = 2; + private static final int MSG_REPEAT_KEY = 3; + private static final int MSG_LOGPRESS_KEY = 4; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_POPUP_PREVIEW: + showKey(msg.arg1); + break; + case MSG_DISMISS_PREVIEW: + mPreviewText.setVisibility(INVISIBLE); + break; + case MSG_REPEAT_KEY: + if (repeatKey()) { + startKeyRepeatTimer(REPEAT_INTERVAL); + } + break; + case MSG_LOGPRESS_KEY: + openPopupIfRequired((MotionEvent) msg.obj); + break; + } + } + + public void popupPreview(int keyIndex, long delay) { + removeMessages(MSG_POPUP_PREVIEW); + sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0), delay); + } + + public void cancelPopupPreview() { + removeMessages(MSG_POPUP_PREVIEW); + } + + public void dismissPreview(long delay) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay); + } + + public void cancelDismissPreview() { + removeMessages(MSG_DISMISS_PREVIEW); + } + + public void startKeyRepeatTimer(long delay) { + sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY), delay); + } + + public void startLongPressTimer(MotionEvent me, long delay) { + sendMessageDelayed(obtainMessage(MSG_LOGPRESS_KEY, me), delay); + } + + public void cancelLongPressTimer() { + removeMessages(MSG_LOGPRESS_KEY); + } + + public void cancelKeyTimers() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + } + + public void cancelKeyTimersAndPopupPreview() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + removeMessages(MSG_POPUP_PREVIEW); + } + + public void cancelAllMessages() { + removeMessages(MSG_REPEAT_KEY); + removeMessages(MSG_LOGPRESS_KEY); + removeMessages(MSG_POPUP_PREVIEW); + removeMessages(MSG_DISMISS_PREVIEW); + } + }; + + static class KeyDebouncer { + private final Key[] mKeys; + private final int mKeyDebounceThresholdSquared; + + // for move de-bouncing + private int mLastCodeX; + private int mLastCodeY; + private int mLastX; + private int mLastY; + + // for time de-bouncing + private int mLastKey; + private long mLastKeyTime; + private long mLastMoveTime; + private long mCurrentKeyTime; + + KeyDebouncer(Key[] keys, float hysteresisPixel) { + if (keys == null || hysteresisPixel < 1.0f) + throw new IllegalArgumentException(); + mKeys = keys; + mKeyDebounceThresholdSquared = (int)(hysteresisPixel * hysteresisPixel); + } + + public int getLastCodeX() { + return mLastCodeX; + } + + public int getLastCodeY() { + return mLastCodeY; + } + + public int getLastX() { + return mLastX; + } + + public int getLastY() { + return mLastY; + } + + public int getLastKey() { + return mLastKey; + } + + public void startMoveDebouncing(int x, int y) { + mLastCodeX = x; + mLastCodeY = y; + } + + public void updateMoveDebouncing(int x, int y) { + mLastX = x; + mLastY = y; + } + + public void resetMoveDebouncing() { + mLastCodeX = mLastX; + mLastCodeY = mLastY; + } + + public boolean isMinorMoveBounce(int x, int y, int newKey, int curKey) { + if (newKey == curKey) { + return true; + } else if (curKey >= 0 && curKey < mKeys.length) { + return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) + < mKeyDebounceThresholdSquared; + } else { + return false; + } + } + + private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { + final int left = key.x; + final int right = key.x + key.width; + final int top = key.y; + final int bottom = key.y + key.height; + final int edgeX = x < left ? left : (x > right ? right : x); + final int edgeY = y < top ? top : (y > bottom ? bottom : y); + final int dx = x - edgeX; + final int dy = y - edgeY; + return dx * dx + dy * dy; + } + + public void startTimeDebouncing(long eventTime) { + mLastKey = NOT_A_KEY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + public void updateTimeDebouncing(long eventTime) { + mCurrentKeyTime += eventTime - mLastMoveTime; + mLastMoveTime = eventTime; + } + + public void resetTimeDebouncing(long eventTime, int currentKey) { + mLastKey = currentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + public boolean isMinorTimeBounce() { + return mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME + && mLastKey != NOT_A_KEY; + } + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.keyboardViewStyle); + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView); + LayoutInflater inflate = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int previewLayout = 0; + int keyTextSize = 0; + + int n = a.getIndexCount(); + + for (int i = 0; i < n; i++) { + int attr = a.getIndex(i); + + switch (attr) { + case R.styleable.LatinKeyboardBaseView_keyBackground: + mKeyBackground = a.getDrawable(attr); + break; + case R.styleable.LatinKeyboardBaseView_verticalCorrection: + mVerticalCorrection = a.getDimensionPixelOffset(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewLayout: + previewLayout = a.getResourceId(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewOffset: + mPreviewOffset = a.getDimensionPixelOffset(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_keyPreviewHeight: + mPreviewHeight = a.getDimensionPixelSize(attr, 80); + break; + case R.styleable.LatinKeyboardBaseView_keyTextSize: + mKeyTextSize = a.getDimensionPixelSize(attr, 18); + break; + case R.styleable.LatinKeyboardBaseView_keyTextColor: + mKeyTextColor = a.getColor(attr, 0xFF000000); + break; + case R.styleable.LatinKeyboardBaseView_labelTextSize: + mLabelTextSize = a.getDimensionPixelSize(attr, 14); + break; + case R.styleable.LatinKeyboardBaseView_popupLayout: + mPopupLayout = a.getResourceId(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_shadowColor: + mShadowColor = a.getColor(attr, 0); + break; + case R.styleable.LatinKeyboardBaseView_shadowRadius: + mShadowRadius = a.getFloat(attr, 0f); + break; + // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) + case R.styleable.LatinKeyboardBaseView_backgroundDimAmount: + mBackgroundDimAmount = a.getFloat(attr, 0.5f); + break; + //case android.R.styleable. + case R.styleable.LatinKeyboardBaseView_keyTextStyle: + int textStyle = a.getInt(attr, 0); + switch (textStyle) { + case 0: + mKeyTextStyle = Typeface.DEFAULT; + break; + case 1: + mKeyTextStyle = Typeface.DEFAULT_BOLD; + break; + default: + mKeyTextStyle = Typeface.defaultFromStyle(textStyle); + break; + } + break; + case R.styleable.LatinKeyboardBaseView_symbolColorScheme: + mSymbolColorScheme = a.getInt(attr, 0); + break; + } + } + + mPreviewPopup = new PopupWindow(context); + if (previewLayout != 0) { + mPreviewText = (TextView) inflate.inflate(previewLayout, null); + mPreviewTextSizeLarge = (int) mPreviewText.getTextSize(); + mPreviewPopup.setContentView(mPreviewText); + mPreviewPopup.setBackgroundDrawable(null); + } else { + mShowPreview = false; + } + + mPreviewPopup.setTouchable(false); + + mPopupKeyboard = new PopupWindow(context); + mPopupKeyboard.setBackgroundDrawable(null); + //mPopupKeyboard.setClippingEnabled(false); + + mPopupParent = this; + //mPredicting = true; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setTextSize(keyTextSize); + mPaint.setTextAlign(Align.CENTER); + mPaint.setAlpha(255); + + mPadding = new Rect(0, 0, 0, 0); + mMiniKeyboardCache = new HashMap(); + mKeyBackground.getPadding(mPadding); + + mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density); + // TODO: Refer frameworks/base/core/res/res/values/config.xml + mDisambiguateSwipe = getResources().getBoolean(R.bool.config_swipeDisambiguation); + resetMultiTap(); + initGestureDetector(); + } + + private void initGestureDetector() { + mGestureDetector = new GestureDetector( + getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, + float velocityX, float velocityY) { + if (mPossiblePoly) return false; + final float absX = Math.abs(velocityX); + final float absY = Math.abs(velocityY); + float deltaX = me2.getX() - me1.getX(); + float deltaY = me2.getY() - me1.getY(); + int travelX = getWidth() / 2; // Half the keyboard width + int travelY = getHeight() / 2; // Half the keyboard height + mSwipeTracker.computeCurrentVelocity(1000); + final float endingVelocityX = mSwipeTracker.getXVelocity(); + final float endingVelocityY = mSwipeTracker.getYVelocity(); + boolean sendDownKey = false; + if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) { + if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) { + sendDownKey = true; + } else { + swipeRight(); + return true; + } + } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) { + if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) { + sendDownKey = true; + } else { + swipeLeft(); + return true; + } + } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) { + if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) { + sendDownKey = true; + } else { + swipeUp(); + return true; + } + } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { + if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) { + sendDownKey = true; + } else { + swipeDown(); + return true; + } + } + + if (sendDownKey) { + detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime()); + } + return false; + } + }); + + mGestureDetector.setIsLongpressEnabled(false); + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mKeyboardActionListener = listener; + } + + /** + * Returns the {@link OnKeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected OnKeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * 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 + */ + public void setKeyboard(Keyboard keyboard) { + if (mKeyboard != null) { + showPreview(NOT_A_KEY); + } + // Remove any pending messages, except dismissing preview + mHandler.cancelKeyTimersAndPopupPreview(); + mKeyboard = keyboard; + List keys = mKeyboard.getKeys(); + mKeys = keys.toArray(new Key[keys.size()]); + requestLayout(); + // Hint to reallocate the buffer if the size changed + mKeyboardChanged = true; + invalidateAllKeys(); + computeProximityThreshold(keyboard); + mMiniKeyboardCache.clear(); + // Not really necessary to do every time, but will free up views + // Switching to a different keyboard should abort any pending keys so that the key up + // doesn't get delivered to the old or new keyboard + mAbortKey = true; // Until the next ACTION_DOWN + } + + /** + * Returns the current keyboard being displayed by this view. + * @return the currently attached keyboard + * @see #setKeyboard(Keyboard) + */ + public Keyboard getKeyboard() { + return mKeyboard; + } + + /** + * Sets the state of the shift key of the keyboard, if any. + * @param shifted whether or not to enable the state of the shift key + * @return true if the shift key state changed, false if there was no change + */ + public boolean setShifted(boolean shifted) { + if (mKeyboard != null) { + if (mKeyboard.setShifted(shifted)) { + // The whole keyboard probably needs to be redrawn + invalidateAllKeys(); + return true; + } + } + return false; + } + + /** + * Returns the state of the shift key of the keyboard, if any. + * @return true if the shift is in a pressed state, false otherwise. If there is + * no shift key on the keyboard or there is no keyboard attached, it returns false. + */ + public boolean isShifted() { + if (mKeyboard != null) { + return mKeyboard.isShifted(); + } + return false; + } + + /** + * 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 popup + * @see #isPreviewEnabled() + */ + public void setPreviewEnabled(boolean previewEnabled) { + mShowPreview = previewEnabled; + } + + /** + * Returns the enabled state of the key feedback popup. + * @return whether or not the key feedback popup is enabled + * @see #setPreviewEnabled(boolean) + */ + public boolean isPreviewEnabled() { + return mShowPreview; + } + + public int getSymbolColorSheme() { + return mSymbolColorScheme; + } + + public void setVerticalCorrection(int verticalOffset) { + } + + public void setPopupParent(View v) { + mPopupParent = v; + } + + public void setPopupOffset(int x, int y) { + mMiniKeyboardOffsetX = x; + mMiniKeyboardOffsetY = y; + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + } + + /** + * When enabled, calls to {@link OnKeyboardActionListener#onKey} 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) { + mProximityCorrectOn = enabled; + } + + /** + * Returns true if proximity correction is enabled. + */ + public boolean isProximityCorrectionEnabled() { + return mProximityCorrectOn; + } + + /** + * Popup keyboard close button clicked. + * @hide + */ + public void onClick(View v) { + dismissPopupKeyboard(); + } + + protected CharSequence adjustCase(CharSequence label) { + if (mKeyboard.isShifted() && label != null && label.length() < 3 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Round up a little + if (mKeyboard == null) { + setMeasuredDimension( + getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); + } else { + int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); + if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { + width = MeasureSpec.getSize(widthMeasureSpec); + } + setMeasuredDimension( + width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); + } + } + + /** + * Compute the average distance between adjacent keys (horizontally and vertically) + * and square it to get the proximity threshold. We use a square here and in computing + * the touch distance from a key's center to avoid taking a square root. + * @param keyboard + */ + private void computeProximityThreshold(Keyboard keyboard) { + if (keyboard == null) return; + final Key[] keys = mKeys; + if (keys == null) return; + int length = keys.length; + int dimensionSum = 0; + for (int i = 0; i < length; i++) { + Key key = keys[i]; + dimensionSum += Math.min(key.width, key.height) + key.gap; + } + if (dimensionSum < 0 || length == 0) return; + mProximityThreshold = (int) (dimensionSum * 1.4f / length); + mProximityThreshold *= mProximityThreshold; // Square it + + final float hysteresisPixel = getContext().getResources() + .getDimension(R.dimen.key_debounce_hysteresis_distance); + mDebouncer = new KeyDebouncer(keys, hysteresisPixel); + } + + @Override + public void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Release the buffer, if any and it will be reallocated on the next draw + mBuffer = null; + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mDrawPending || mBuffer == null || mKeyboardChanged) { + onBufferDraw(); + } + canvas.drawBitmap(mBuffer, 0, 0, null); + } + + private void onBufferDraw() { + if (mBuffer == null || mKeyboardChanged) { + if (mBuffer == null || mKeyboardChanged && + (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { + // Make sure our bitmap is at least 1x1 + final int width = Math.max(1, getWidth()); + final int height = Math.max(1, getHeight()); + mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBuffer); + } + invalidateAllKeys(); + mKeyboardChanged = false; + } + final Canvas canvas = mCanvas; + canvas.clipRect(mDirtyRect, Op.REPLACE); + + if (mKeyboard == null) return; + + final Paint paint = mPaint; + final Drawable keyBackground = mKeyBackground; + final Rect clipRegion = mClipRegion; + final Rect padding = mPadding; + final int kbdPaddingLeft = getPaddingLeft(); + final int kbdPaddingTop = getPaddingTop(); + final Key[] keys = mKeys; + final Key invalidKey = mInvalidatedKey; + + paint.setColor(mKeyTextColor); + boolean drawSingleKey = false; + if (invalidKey != null && canvas.getClipBounds(clipRegion)) { + // Is clipRegion completely contained within the invalidated key? + if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left && + invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top && + invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right && + invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) { + drawSingleKey = true; + } + } + canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); + final int keyCount = keys.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[i]; + if (drawSingleKey && invalidKey != key) { + continue; + } + int[] drawableState = key.getCurrentDrawableState(); + keyBackground.setState(drawableState); + + // Switch the character to uppercase if shift is pressed + String label = key.label == null? null : adjustCase(key.label).toString(); + + final Rect bounds = keyBackground.getBounds(); + if (key.width != bounds.right || + key.height != bounds.bottom) { + keyBackground.setBounds(0, 0, key.width, key.height); + } + canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop); + keyBackground.draw(canvas); + + if (label != null) { + // For characters, use large font. For labels like "Done", use small font. + if (label.length() > 1 && key.codes.length < 2) { + paint.setTextSize(mLabelTextSize); + paint.setTypeface(Typeface.DEFAULT_BOLD); + } else { + paint.setTextSize(mKeyTextSize); + paint.setTypeface(mKeyTextStyle); + } + // Draw a drop shadow for the text + paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor); + // Draw the text + canvas.drawText(label, + (key.width - padding.left - padding.right) / 2 + + padding.left, + (key.height - padding.top - padding.bottom) / 2 + + (paint.getTextSize() - paint.descent()) / 2 + padding.top, + paint); + // Turn off drop shadow + paint.setShadowLayer(0, 0, 0, 0); + } else if (key.icon != null) { + final int drawableX = (key.width - padding.left - padding.right + - key.icon.getIntrinsicWidth()) / 2 + padding.left; + final int drawableY = (key.height - padding.top - padding.bottom + - key.icon.getIntrinsicHeight()) / 2 + padding.top; + canvas.translate(drawableX, drawableY); + key.icon.setBounds(0, 0, + key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); + key.icon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + } + canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop); + } + mInvalidatedKey = null; + // Overlay a dark rectangle to dim the keyboard + if (mMiniKeyboardOnScreen) { + paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); + canvas.drawRect(0, 0, getWidth(), getHeight(), paint); + } + + if (DEBUG) { + if (mShowTouchPoints) { + int lastX = mDebouncer.getLastX(); + int lastY = mDebouncer.getLastY(); + paint.setAlpha(128); + paint.setColor(0xFFFF0000); + canvas.drawCircle(mStartX, mStartY, 3, paint); + canvas.drawLine(mStartX, mStartY, lastX, lastY, paint); + paint.setColor(0xFF0000FF); + canvas.drawCircle(lastX, lastY, 3, paint); + paint.setColor(0xFF00FF00); + canvas.drawCircle((mStartX + lastX) / 2, (mStartY + lastY) / 2, 2, paint); + } + } + + mDrawPending = false; + mDirtyRect.setEmpty(); + } + + private int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) { + final Key[] keys = mKeys; + int primaryIndex = NOT_A_KEY; + int closestKey = NOT_A_KEY; + int closestKeyDist = mProximityThreshold + 1; + Arrays.fill(mDistances, Integer.MAX_VALUE); + int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y); + final int keyCount = nearestKeyIndices.length; + for (int i = 0; i < keyCount; i++) { + final Key key = keys[nearestKeyIndices[i]]; + int dist = 0; + boolean isInside = key.isInside(x,y); + if (isInside) { + primaryIndex = nearestKeyIndices[i]; + } + + if (((mProximityCorrectOn + && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) + || isInside) + && key.codes[0] > 32) { + // Find insertion point + final int nCodes = key.codes.length; + if (dist < closestKeyDist) { + closestKeyDist = dist; + closestKey = nearestKeyIndices[i]; + } + + if (allKeys == null) continue; + + for (int j = 0; j < mDistances.length; j++) { + if (mDistances[j] > dist) { + // Make space for nCodes codes + System.arraycopy(mDistances, j, mDistances, j + nCodes, + mDistances.length - j - nCodes); + System.arraycopy(allKeys, j, allKeys, j + nCodes, + allKeys.length - j - nCodes); + System.arraycopy(key.codes, 0, allKeys, j, nCodes); + Arrays.fill(mDistances, j, j + nCodes, dist); + break; + } + } + } + } + if (primaryIndex == NOT_A_KEY) { + primaryIndex = closestKey; + } + return primaryIndex; + } + + private void detectAndSendKey(int index, int x, int y, long eventTime) { + if (index != NOT_A_KEY && index < mKeys.length) { + final Key key = mKeys[index]; + if (key.text != null) { + mKeyboardActionListener.onText(key.text); + mKeyboardActionListener.onRelease(NOT_A_KEY); + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = new int[MAX_NEARBY_KEYS]; + Arrays.fill(codes, NOT_A_KEY); + getKeyIndexAndNearbyCodes(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + /* + * 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; + } + mKeyboardActionListener.onKey(code, codes); + mKeyboardActionListener.onRelease(code); + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + private CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return adjustCase(mPreviewLabel); + } else { + return adjustCase(key.label); + } + } + + private void showPreview(int keyIndex) { + int oldKeyIndex = mCurrentKeyIndex; + final PopupWindow previewPopup = mPreviewPopup; + + mCurrentKeyIndex = keyIndex; + // Release the old key and press the new key + final Key[] keys = mKeys; + if (oldKeyIndex != mCurrentKeyIndex) { + if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) { + keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY); + invalidateKey(oldKeyIndex); + } + if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) { + keys[mCurrentKeyIndex].onPressed(); + invalidateKey(mCurrentKeyIndex); + } + } + // If key changed and preview is on ... + if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) { + if (keyIndex == NOT_A_KEY) { + mHandler.cancelPopupPreview(); + if (previewPopup.isShowing()) { + mHandler.dismissPreview(DELAY_AFTER_PREVIEW); + } + } else { + if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) { + // Show right away, if it's already visible and finger is moving around + showKey(keyIndex); + } else { + mHandler.popupPreview(keyIndex, DELAY_BEFORE_PREVIEW); + } + } + } + } + + private void showKey(final int keyIndex) { + final PopupWindow previewPopup = mPreviewPopup; + final Key[] keys = mKeys; + if (keyIndex < 0 || keyIndex >= mKeys.length) return; + Key key = keys[keyIndex]; + if (key.icon != null) { + mPreviewText.setCompoundDrawables(null, null, null, + key.iconPreview != null ? key.iconPreview : key.icon); + mPreviewText.setText(null); + } else { + mPreviewText.setCompoundDrawables(null, null, null, null); + mPreviewText.setText(getPreviewText(key)); + if (key.label.length() > 1 && key.codes.length < 2) { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize); + mPreviewText.setTypeface(Typeface.DEFAULT_BOLD); + } else { + mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge); + mPreviewText.setTypeface(mKeyTextStyle); + } + } + mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width + + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight()); + final int popupHeight = mPreviewHeight; + LayoutParams lp = mPreviewText.getLayoutParams(); + if (lp != null) { + lp.width = popupWidth; + lp.height = popupHeight; + } + if (!mPreviewCentered) { + mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + getPaddingLeft(); + mPopupPreviewY = key.y - popupHeight + mPreviewOffset; + } else { + // TODO: Fix this if centering is brought back + mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2; + mPopupPreviewY = - mPreviewText.getMeasuredHeight(); + } + mHandler.cancelDismissPreview(); + if (mOffsetInWindow == null) { + mOffsetInWindow = new int[2]; + getLocationInWindow(mOffsetInWindow); + mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero + mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero + int[] mWindowLocation = new int[2]; + getLocationOnScreen(mWindowLocation); + mWindowY = mWindowLocation[1]; + } + // Set the preview background state + mPreviewText.getBackground().setState( + key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); + mPopupPreviewX += mOffsetInWindow[0]; + mPopupPreviewY += mOffsetInWindow[1]; + + // If the popup cannot be shown above the key, put it on the side + if (mPopupPreviewY + mWindowY < 0) { + // If the key you're pressing is on the left side of the keyboard, show the popup on + // the right, offset by enough to see at least one key to the left/right. + if (key.x + key.width <= getWidth() / 2) { + mPopupPreviewX += (int) (key.width * 2.5); + } else { + mPopupPreviewX -= (int) (key.width * 2.5); + } + mPopupPreviewY += popupHeight; + } + + if (previewPopup.isShowing()) { + previewPopup.update(mPopupPreviewX, mPopupPreviewY, + popupWidth, popupHeight); + } else { + previewPopup.setWidth(popupWidth); + previewPopup.setHeight(popupHeight); + previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, + mPopupPreviewX, mPopupPreviewY); + } + mPreviewText.setVisibility(VISIBLE); + } + + /** + * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient + * because the keyboard renders the keys to an off-screen buffer and an invalidate() only + * draws the cached buffer. + * @see #invalidateKey(int) + */ + public void invalidateAllKeys() { + mDirtyRect.union(0, 0, getWidth(), getHeight()); + mDrawPending = true; + invalidate(); + } + + /** + * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only + * one key is changing it's content. Any changes that affect the position or size of the key + * may not be honored. + * @param keyIndex the index of the key in the attached {@link Keyboard}. + * @see #invalidateAllKeys + */ + public void invalidateKey(int keyIndex) { + if (mKeys == null) return; + if (keyIndex < 0 || keyIndex >= mKeys.length) { + return; + } + final Key key = mKeys[keyIndex]; + mInvalidatedKey = key; + mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(), + key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); + onBufferDraw(); + invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(), + key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop()); + } + + private boolean openPopupIfRequired(MotionEvent me) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) { + return false; + } + + Key popupKey = mKeys[mCurrentKey]; + boolean result = onLongPress(popupKey); + if (result) { + mAbortKey = true; + showPreview(NOT_A_KEY); + } + return result; + } + + /** + * Called when a key is long pressed. By default this will open any popup keyboard associated + * with this key through the attributes popupLayout and popupCharacters. + * @param popupKey the key that was long pressed + * @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 popupKey) { + int popupKeyboardId = popupKey.popupResId; + + if (popupKeyboardId != 0) { + mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey); + if (mMiniKeyboardContainer == null) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null); + mMiniKeyboard = (LatinKeyboardBaseView) mMiniKeyboardContainer.findViewById( + R.id.LatinKeyboardBaseView); + View closeButton = mMiniKeyboardContainer.findViewById( + R.id.closeButton); + if (closeButton != null) closeButton.setOnClickListener(this); + mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() { + public void onKey(int primaryCode, int[] keyCodes) { + mKeyboardActionListener.onKey(primaryCode, keyCodes); + dismissPopupKeyboard(); + } + + public void onText(CharSequence text) { + mKeyboardActionListener.onText(text); + dismissPopupKeyboard(); + } + + public void swipeLeft() { } + public void swipeRight() { } + public void swipeUp() { } + public void swipeDown() { } + public void onPress(int primaryCode) { + mKeyboardActionListener.onPress(primaryCode); + } + public void onRelease(int primaryCode) { + mKeyboardActionListener.onRelease(primaryCode); + } + }); + //mInputView.setSuggest(mSuggest); + Keyboard keyboard; + if (popupKey.popupCharacters != null) { + keyboard = new Keyboard(getContext(), popupKeyboardId, + popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight()); + } else { + keyboard = new Keyboard(getContext(), popupKeyboardId); + } + mMiniKeyboard.setKeyboard(keyboard); + mMiniKeyboard.setPopupParent(this); + mMiniKeyboardContainer.measure( + MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); + + mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer); + } else { + mMiniKeyboard = (LatinKeyboardBaseView) mMiniKeyboardContainer.findViewById( + R.id.LatinKeyboardBaseView); + } + if (mWindowOffset == null) { + mWindowOffset = new int[2]; + getLocationInWindow(mWindowOffset); + } + mPopupX = popupKey.x + getPaddingLeft(); + mPopupY = popupKey.y + getPaddingTop(); + mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); + mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); + mMiniKeyboard.setShifted(isShifted()); + mPopupKeyboard.setContentView(mMiniKeyboardContainer); + mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth()); + mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight()); + mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y); + mMiniKeyboardOnScreen = true; + //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me)); + invalidateAllKeys(); + return true; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + // Convert multi-pointer up/down events to single up/down events to + // deal with the typical multi-pointer behavior of two-thumb typing + final int pointerCount = me.getPointerCount(); + final int action = me.getAction(); + boolean result = false; + final long now = me.getEventTime(); + + if (pointerCount != mOldPointerCount) { + if (pointerCount == 1) { + // Send a down event for the latest pointer + MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, + me.getX(), me.getY(), me.getMetaState()); + result = onModifiedTouchEvent(down, false); + down.recycle(); + // If it's an up action, then deliver the up as well. + if (action == MotionEvent.ACTION_UP) { + result = onModifiedTouchEvent(me, true); + } + } else { + // Send an up event for the last pointer + MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, + mOldPointerX, mOldPointerY, me.getMetaState()); + result = onModifiedTouchEvent(up, true); + up.recycle(); + } + } else { + if (pointerCount == 1) { + result = onModifiedTouchEvent(me, false); + mOldPointerX = me.getX(); + mOldPointerY = me.getY(); + } else { + // Don't do anything when 2 pointers are down and moving. + result = true; + } + } + mOldPointerCount = pointerCount; + + return result; + } + + private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) { + int touchX = (int) me.getX() - getPaddingLeft(); + int touchY = (int) me.getY() + mVerticalCorrection - getPaddingTop(); + final int action = me.getAction(); + final long eventTime = me.getEventTime(); + int keyIndex = getKeyIndexAndNearbyCodes(touchX, touchY, null); + mPossiblePoly = possiblePoly; + + // Track the last few movements to look for spurious swipes. + if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); + mSwipeTracker.addMovement(me); + + // Ignore all motion events until a DOWN. + if (mAbortKey + && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + if (mGestureDetector.onTouchEvent(me)) { + showPreview(NOT_A_KEY); + mHandler.cancelKeyTimers(); + return true; + } + + // Needs to be called after the gesture detector gets a turn, as it may have + // displayed the mini keyboard + if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { + return true; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mAbortKey = false; + mCurrentKey = keyIndex; + mDownKey = keyIndex; + mStartX = touchX; + mStartY = touchY; + mDebouncer.startMoveDebouncing(touchX, touchY); + mDebouncer.startTimeDebouncing(eventTime); + checkMultiTap(eventTime, keyIndex); + mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ? + mKeys[keyIndex].codes[0] : 0); + if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { + mRepeatKeyIndex = mCurrentKey; + mHandler.startKeyRepeatTimer(REPEAT_START_DELAY); + repeatKey(); + // Delivering the key could have caused an abort + if (mAbortKey) { + mRepeatKeyIndex = NOT_A_KEY; + break; + } + } + if (mCurrentKey != NOT_A_KEY) { + mHandler.startLongPressTimer(me, LONGPRESS_TIMEOUT); + } + showPreview(keyIndex); + break; + + case MotionEvent.ACTION_MOVE: + boolean continueLongPress = false; + if (keyIndex != NOT_A_KEY) { + if (mCurrentKey == NOT_A_KEY) { + mCurrentKey = keyIndex; + mDebouncer.updateTimeDebouncing(eventTime); + } else if (mDebouncer.isMinorMoveBounce(touchX, touchY, keyIndex, + mCurrentKey)) { + mDebouncer.updateTimeDebouncing(eventTime); + continueLongPress = true; + } else if (mRepeatKeyIndex == NOT_A_KEY) { + resetMultiTap(); + mDebouncer.resetTimeDebouncing(eventTime, mCurrentKey); + mDebouncer.resetMoveDebouncing(); + mCurrentKey = keyIndex; + } + } + if (!continueLongPress) { + // Cancel old longpress + mHandler.cancelLongPressTimer(); + // Start new longpress if key has changed + if (keyIndex != NOT_A_KEY) { + mHandler.startLongPressTimer(me, LONGPRESS_TIMEOUT); + } + } + /* + * While time debouncing is in effect, mCurrentKey holds the new key and mDebouncer + * holds the last key. At ACTION_UP event if time debouncing will be in effect + * eventually, the last key should be sent as the result. In such case mCurrentKey + * should not be showed as popup preview. + */ + showPreview(mDebouncer.isMinorTimeBounce() ? mDebouncer.getLastKey() : mCurrentKey); + break; + + case MotionEvent.ACTION_UP: + mHandler.cancelKeyTimersAndPopupPreview(); + if (mDebouncer.isMinorMoveBounce(touchX, touchY, keyIndex, mCurrentKey)) { + mDebouncer.updateTimeDebouncing(eventTime); + } else { + resetMultiTap(); + mDebouncer.resetTimeDebouncing(eventTime, mCurrentKey); + mCurrentKey = keyIndex; + } + if (mDebouncer.isMinorTimeBounce()) { + mCurrentKey = mDebouncer.getLastKey(); + touchX = mDebouncer.getLastCodeX(); + touchY = mDebouncer.getLastCodeY(); + } + showPreview(NOT_A_KEY); + // If we're not on a repeating key (which sends on a DOWN event) + if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) { + detectAndSendKey(mCurrentKey, touchX, touchY, eventTime); + } + invalidateKey(keyIndex); + mRepeatKeyIndex = NOT_A_KEY; + break; + + case MotionEvent.ACTION_CANCEL: + mHandler.cancelKeyTimersAndPopupPreview(); + dismissPopupKeyboard(); + mAbortKey = true; + showPreview(NOT_A_KEY); + invalidateKey(mCurrentKey); + break; + } + mDebouncer.updateMoveDebouncing(touchX, touchY); + return true; + } + + private boolean repeatKey() { + Key key = mKeys[mRepeatKeyIndex]; + detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime); + return true; + } + + protected void swipeRight() { + mKeyboardActionListener.swipeRight(); + } + + protected void swipeLeft() { + mKeyboardActionListener.swipeLeft(); + } + + protected void swipeUp() { + mKeyboardActionListener.swipeUp(); + } + + protected void swipeDown() { + mKeyboardActionListener.swipeDown(); + } + + public void closing() { + if (mPreviewPopup.isShowing()) { + mPreviewPopup.dismiss(); + } + mHandler.cancelAllMessages(); + + dismissPopupKeyboard(); + mBuffer = null; + mCanvas = null; + mMiniKeyboardCache.clear(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + closing(); + } + + private void dismissPopupKeyboard() { + if (mPopupKeyboard.isShowing()) { + mPopupKeyboard.dismiss(); + mMiniKeyboardOnScreen = false; + invalidateAllKeys(); + } + } + + public boolean handleBack() { + if (mPopupKeyboard.isShowing()) { + dismissPopupKeyboard(); + return true; + } + return false; + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + if (keyIndex == NOT_A_KEY) return; + Key key = mKeys[keyIndex]; + if (key.codes.length > 1) { + mInMultiTap = true; + if (eventTime < mLastTapTime + MULTITAP_INTERVAL + && keyIndex == mLastSentIndex) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) { + resetMultiTap(); + } + } + + private static class SwipeTracker { + + static final int NUM_PAST = 4; + static final int LONGEST_PAST_TIME = 200; + + final float mPastX[] = new float[NUM_PAST]; + final float mPastY[] = new float[NUM_PAST]; + final long mPastTime[] = new long[NUM_PAST]; + + float mYVelocity; + float mXVelocity; + + public void clear() { + mPastTime[0] = 0; + } + + public void addMovement(MotionEvent ev) { + long time = ev.getEventTime(); + final int N = ev.getHistorySize(); + for (int i=0; i= 0) { + final int start = drop+1; + final int count = NUM_PAST-drop-1; + System.arraycopy(pastX, start, pastX, 0, count); + System.arraycopy(pastY, start, pastY, 0, count); + System.arraycopy(pastTime, start, pastTime, 0, count); + i -= (drop+1); + } + pastX[i] = x; + pastY[i] = y; + pastTime[i] = time; + i++; + if (i < NUM_PAST) { + pastTime[i] = 0; + } + } + + public void computeCurrentVelocity(int units) { + computeCurrentVelocity(units, Float.MAX_VALUE); + } + + public void computeCurrentVelocity(int units, float maxVelocity) { + final float[] pastX = mPastX; + final float[] pastY = mPastY; + final long[] pastTime = mPastTime; + + final float oldestX = pastX[0]; + final float oldestY = pastY[0]; + final long oldestTime = pastTime[0]; + float accumX = 0; + float accumY = 0; + int N=0; + while (N < NUM_PAST) { + if (pastTime[N] == 0) { + break; + } + N++; + } + + for (int i=1; i < N; i++) { + final int dur = (int)(pastTime[i] - oldestTime); + if (dur == 0) continue; + float dist = pastX[i] - oldestX; + float vel = (dist/dur) * units; // pixels/frame. + if (accumX == 0) accumX = vel; + else accumX = (accumX + vel) * .5f; + + dist = pastY[i] - 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; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java index 74fc475e6..38d9cefb1 100644 --- a/java/src/com/android/inputmethod/latin/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/latin/LatinKeyboardView.java @@ -22,8 +22,6 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.inputmethodservice.Keyboard; -import android.inputmethodservice.KeyboardView; -import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; import android.inputmethodservice.Keyboard.Key; import android.os.Handler; import android.os.Message; @@ -33,7 +31,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.widget.PopupWindow; -public class LatinKeyboardView extends KeyboardView { +public class LatinKeyboardView extends LatinKeyboardBaseView { static final int KEYCODE_OPTIONS = -100; static final int KEYCODE_SHIFT_LONGPRESS = -101; @@ -66,6 +64,8 @@ public class LatinKeyboardView extends KeyboardView { /** The y coordinate of the last row */ private int mLastRowY; + private int mExtensionLayoutResId = 0; + public LatinKeyboardView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -78,6 +78,10 @@ public class LatinKeyboardView extends KeyboardView { mPhoneKeyboard = phoneKeyboard; } + public void setExtentionLayoutResId (int id) { + mExtensionLayoutResId = id; + } + @Override public void setKeyboard(Keyboard k) { super.setKeyboard(k); @@ -107,6 +111,29 @@ public class LatinKeyboardView extends KeyboardView { } } + @Override + protected CharSequence adjustCase(CharSequence label) { + Keyboard keyboard = getKeyboard(); + if (keyboard.isShifted() + && keyboard instanceof LatinKeyboard + && ((LatinKeyboard) keyboard).isAlphaKeyboard() + && label != null && label.length() < 3 + && Character.isLowerCase(label.charAt(0))) { + label = label.toString().toUpperCase(); + } + return label; + } + + public boolean setShiftLocked(boolean shiftLocked) { + Keyboard keyboard = getKeyboard(); + if (keyboard != null && keyboard instanceof LatinKeyboard) { + ((LatinKeyboard)keyboard).setShiftLocked(shiftLocked); + invalidateAllKeys(); + return true; + } + return false; + } + /** * 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. @@ -295,7 +322,8 @@ public class LatinKeyboardView extends KeyboardView { mExtensionPopup.setBackgroundDrawable(null); LayoutInflater li = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); - mExtension = (LatinKeyboardView) li.inflate(R.layout.input_trans, null); + mExtension = (LatinKeyboardView) li.inflate(mExtensionLayoutResId == 0 ? + R.layout.input_trans : mExtensionLayoutResId, null); mExtension.setExtensionType(true); mExtension.setOnKeyboardActionListener( new ExtensionKeyboardListener(getOnKeyboardActionListener())); @@ -452,27 +480,39 @@ public class LatinKeyboardView extends KeyboardView { } } } - - void startPlaying(String s) { - if (!DEBUG_AUTO_PLAY) return; - if (s == null) return; - mStringToPlay = s.toLowerCase(); - mPlaying = true; - mDownDelivered = false; - mStringIndex = 0; - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); + + public void startPlaying(String s) { + if (DEBUG_AUTO_PLAY) { + if (s == null) return; + mStringToPlay = s.toLowerCase(); + mPlaying = true; + mDownDelivered = false; + mStringIndex = 0; + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 10); + } } @Override public void draw(Canvas c) { - super.draw(c); - if (DEBUG_AUTO_PLAY && mPlaying) { - mHandler2.removeMessages(MSG_TOUCH_DOWN); - mHandler2.removeMessages(MSG_TOUCH_UP); - if (mDownDelivered) { - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); - } else { - mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); + LatinIMEUtil.GCUtils.getInstance().reset(); + boolean tryGC = true; + for (int i = 0; i < LatinIMEUtil.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { + try { + super.draw(c); + tryGC = false; + } catch (OutOfMemoryError e) { + tryGC = LatinIMEUtil.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e); + } + } + if (DEBUG_AUTO_PLAY) { + if (mPlaying) { + mHandler2.removeMessages(MSG_TOUCH_DOWN); + mHandler2.removeMessages(MSG_TOUCH_UP); + if (mDownDelivered) { + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_UP, 20); + } else { + mHandler2.sendEmptyMessageDelayed(MSG_TOUCH_DOWN, 20); + } } } if (DEBUG_LINE) { diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index a70bea003..a96737f5c 100755 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -16,18 +16,17 @@ package com.android.inputmethod.latin; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import android.content.Context; import android.text.AutoText; import android.text.TextUtils; import android.util.Log; import android.view.View; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.android.inputmethod.latin.WordComposer; - /** * This class loads a dictionary and provides a list of suggestions for a given sequence of * characters. This includes corrections and completions. @@ -35,9 +34,36 @@ import com.android.inputmethod.latin.WordComposer; */ public class Suggest implements Dictionary.WordCallback { + private static final String TAG = "Suggest"; + + public static final int APPROX_MAX_WORD_LENGTH = 32; + 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 frequency 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 freqeuncy will get the BIGRAM_MULTIPLIER_MAX as the multiplier. + */ + public static final int MAXIMUM_BIGRAM_FREQUENCY = 127; + + 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_CONTACTS = 4; + // If you add a type of dictionary, increment DIC_TYPE_LAST_ID + public static final int DIC_TYPE_LAST_ID = 4; static final int LARGE_DICTIONARY_THRESHOLD = 200 * 1000; @@ -49,11 +75,17 @@ public class Suggest implements Dictionary.WordCallback { private Dictionary mContactsDictionary; + private Dictionary mUserBigramDictionary; + private int mPrefMaxSuggestions = 12; + private static final int PREF_MAX_BIGRAMS = 60; + private boolean mAutoTextEnabled; private int[] mPriorities = new int[mPrefMaxSuggestions]; + private int[] mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + // Handle predictive correction for only the first 1280 characters for performance reasons // If we support scripts that need latin characters beyond that, we should probably use some // kind of a sparse array or language specific list with a mapping lookup table. @@ -61,6 +93,7 @@ public class Suggest implements Dictionary.WordCallback { // latin characters. private int[] mNextLettersFrequencies = new int[1280]; private ArrayList mSuggestions = new ArrayList(); + ArrayList mBigramSuggestions = new ArrayList(); private ArrayList mStringPool = new ArrayList(); private boolean mHaveCorrection; private CharSequence mOriginalWord; @@ -69,11 +102,19 @@ public class Suggest implements Dictionary.WordCallback { private int mCorrectionMode = CORRECTION_BASIC; + public Suggest(Context context, int[] dictionaryResId) { + mMainDict = new BinaryDictionary(context, dictionaryResId, DIC_MAIN); + initPool(); + } - public Suggest(Context context, int dictionaryResId) { - mMainDict = new BinaryDictionary(context, dictionaryResId); + public Suggest(Context context, ByteBuffer byteBuffer) { + mMainDict = new BinaryDictionary(context, byteBuffer, DIC_MAIN); + initPool(); + } + + private void initPool() { for (int i = 0; i < mPrefMaxSuggestions; i++) { - StringBuilder sb = new StringBuilder(32); + StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); mStringPool.add(sb); } } @@ -94,6 +135,10 @@ public class Suggest implements Dictionary.WordCallback { return mMainDict.getSize() > LARGE_DICTIONARY_THRESHOLD; } + public 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. @@ -113,6 +158,10 @@ public class Suggest implements Dictionary.WordCallback { mAutoDictionary = autoDictionary; } + public void setUserBigramDictionary(Dictionary userBigramDictionary) { + mUserBigramDictionary = userBigramDictionary; + } + /** * Number of suggestions to generate from the input key sequence. This has * to be a number between 1 and 100 (inclusive). @@ -125,9 +174,10 @@ public class Suggest implements Dictionary.WordCallback { } mPrefMaxSuggestions = maxSuggestions; mPriorities = new int[mPrefMaxSuggestions]; - collectGarbage(); + mBigramPriorities = new int[PREF_MAX_BIGRAMS]; + collectGarbage(mSuggestions, mPrefMaxSuggestions); while (mStringPool.size() < mPrefMaxSuggestions) { - StringBuilder sb = new StringBuilder(32); + StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); mStringPool.add(sb); } } @@ -162,30 +212,77 @@ public class Suggest implements Dictionary.WordCallback { /** * Returns a list of words that match the list of character codes passed in. * This list will be overwritten the next time this function is called. - * @param a view for retrieving the context for AutoText - * @param codes the list of codes. Each list item contains an array of character codes - * in order of probability where the character at index 0 in the array has the highest - * probability. + * @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 list of suggestions. */ public List getSuggestions(View view, WordComposer wordComposer, - boolean includeTypedWordIfValid) { + boolean includeTypedWordIfValid, CharSequence prevWordForBigram) { + LatinImeLogger.onStartSuggestion(prevWordForBigram); mHaveCorrection = false; mCapitalize = wordComposer.isCapitalized(); - collectGarbage(); + collectGarbage(mSuggestions, mPrefMaxSuggestions); Arrays.fill(mPriorities, 0); Arrays.fill(mNextLettersFrequencies, 0); // Save a lowercase version of the original word mOriginalWord = wordComposer.getTypedWord(); if (mOriginalWord != null) { - mOriginalWord = mOriginalWord.toString(); - mLowerOriginalWord = mOriginalWord.toString().toLowerCase(); + final String mOriginalWordString = mOriginalWord.toString(); + mOriginalWord = mOriginalWordString; + mLowerOriginalWord = mOriginalWordString.toLowerCase(); + // Treating USER_TYPED as UNIGRAM suggestion for logging now. + LatinImeLogger.onAddSuggestedWord(mOriginalWordString, Suggest.DIC_USER_TYPED, + Dictionary.DataType.UNIGRAM); } else { mLowerOriginalWord = ""; } - // Search the dictionary only if there are at least 2 characters - if (wordComposer.size() > 1) { + + if (wordComposer.size() == 1 && (mCorrectionMode == CORRECTION_FULL_BIGRAM + || mCorrectionMode == CORRECTION_BASIC)) { + // At first character typed, search only the bigrams + Arrays.fill(mBigramPriorities, 0); + collectGarbage(mBigramSuggestions, PREF_MAX_BIGRAMS); + + if (!TextUtils.isEmpty(prevWordForBigram)) { + CharSequence lowerPrevWord = prevWordForBigram.toString().toLowerCase(); + if (mMainDict.isValidWord(lowerPrevWord)) { + prevWordForBigram = lowerPrevWord; + } + if (mUserBigramDictionary != null) { + mUserBigramDictionary.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + if (mContactsDictionary != null) { + mContactsDictionary.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + if (mMainDict != null) { + mMainDict.getBigrams(wordComposer, prevWordForBigram, this, + mNextLettersFrequencies); + } + char currentChar = wordComposer.getTypedWord().charAt(0); + char currentCharUpper = Character.toUpperCase(currentChar); + int count = 0; + int bigramSuggestionSize = mBigramSuggestions.size(); + for (int i = 0; i < bigramSuggestionSize; i++) { + if (mBigramSuggestions.get(i).charAt(0) == currentChar + || mBigramSuggestions.get(i).charAt(0) == currentCharUpper) { + int poolSize = mStringPool.size(); + StringBuilder sb = poolSize > 0 ? + (StringBuilder) mStringPool.remove(poolSize - 1) + : new StringBuilder(getApproxMaxWordLength()); + sb.setLength(0); + sb.append(mBigramSuggestions.get(i)); + mSuggestions.add(count++, sb); + if (count > mPrefMaxSuggestions) break; + } + } + } + + } else if (wordComposer.size() > 1) { + // At second character typed, search the unigrams (scores being affected by bigrams) if (mUserDictionary != null || mContactsDictionary != null) { if (mUserDictionary != null) { mUserDictionary.getWords(wordComposer, this, mNextLettersFrequencies); @@ -195,26 +292,29 @@ public class Suggest implements Dictionary.WordCallback { } if (mSuggestions.size() > 0 && isValidWord(mOriginalWord) - && mCorrectionMode == CORRECTION_FULL) { + && (mCorrectionMode == CORRECTION_FULL + || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { mHaveCorrection = true; } } mMainDict.getWords(wordComposer, this, mNextLettersFrequencies); - if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 0) { + if ((mCorrectionMode == CORRECTION_FULL || mCorrectionMode == CORRECTION_FULL_BIGRAM) + && mSuggestions.size() > 0) { mHaveCorrection = true; } } if (mOriginalWord != null) { mSuggestions.add(0, mOriginalWord.toString()); } - + // Check if the first suggestion has a minimum number of characters in common - if (mCorrectionMode == CORRECTION_FULL && mSuggestions.size() > 1) { + if (wordComposer.size() > 1 && mSuggestions.size() > 1 + && (mCorrectionMode == CORRECTION_FULL + || mCorrectionMode == CORRECTION_FULL_BIGRAM)) { if (!haveSufficientCommonality(mLowerOriginalWord, mSuggestions.get(1))) { mHaveCorrection = false; } } - if (mAutoTextEnabled) { int i = 0; int max = 6; @@ -240,7 +340,6 @@ public class Suggest implements Dictionary.WordCallback { i++; } } - removeDupes(); return mSuggestions; } @@ -294,35 +393,67 @@ public class Suggest implements Dictionary.WordCallback { return false; } - public boolean addWord(final char[] word, final int offset, final int length, final int freq) { + public boolean addWord(final char[] word, final int offset, final int length, int freq, + final int dicTypeId, final Dictionary.DataType dataType) { + Dictionary.DataType dataTypeForLog = dataType; + ArrayList suggestions; + int[] priorities; + int prefMaxSuggestions; + if(dataType == Dictionary.DataType.BIGRAM) { + suggestions = mBigramSuggestions; + priorities = mBigramPriorities; + prefMaxSuggestions = PREF_MAX_BIGRAMS; + } else { + suggestions = mSuggestions; + priorities = mPriorities; + prefMaxSuggestions = mPrefMaxSuggestions; + } + int pos = 0; - final int[] priorities = mPriorities; - final int prefMaxSuggestions = mPrefMaxSuggestions; + // Check if it's the same word, only caps are different if (compareCaseInsensitive(mLowerOriginalWord, word, offset, length)) { pos = 0; } 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) mBigramPriorities[bigramSuggestion]) + / MAXIMUM_BIGRAM_FREQUENCY) + * (BIGRAM_MULTIPLIER_MAX - BIGRAM_MULTIPLIER_MIN) + + BIGRAM_MULTIPLIER_MIN; + /* Log.d(TAG,"bigram num: " + bigramSuggestion + + " wordB: " + mBigramSuggestions.get(bigramSuggestion).toString() + + " currentPriority: " + freq + " bigramPriority: " + + mBigramPriorities[bigramSuggestion] + + " multiplier: " + multiplier); */ + freq = (int)Math.round((freq * multiplier)); + } + } + // Check the last one's priority and bail if (priorities[prefMaxSuggestions - 1] >= freq) return true; while (pos < prefMaxSuggestions) { if (priorities[pos] < freq - || (priorities[pos] == freq && length < mSuggestions - .get(pos).length())) { + || (priorities[pos] == freq && length < suggestions.get(pos).length())) { break; } pos++; } } - if (pos >= prefMaxSuggestions) { return true; } + System.arraycopy(priorities, pos, priorities, pos + 1, prefMaxSuggestions - pos - 1); priorities[pos] = freq; int poolSize = mStringPool.size(); StringBuilder sb = poolSize > 0 ? (StringBuilder) mStringPool.remove(poolSize - 1) - : new StringBuilder(32); + : new StringBuilder(getApproxMaxWordLength()); sb.setLength(0); if (mCapitalize) { sb.append(Character.toUpperCase(word[offset])); @@ -332,16 +463,38 @@ public class Suggest implements Dictionary.WordCallback { } else { sb.append(word, offset, length); } - mSuggestions.add(pos, sb); - if (mSuggestions.size() > prefMaxSuggestions) { - CharSequence garbage = mSuggestions.remove(prefMaxSuggestions); + suggestions.add(pos, sb); + if (suggestions.size() > prefMaxSuggestions) { + CharSequence garbage = suggestions.remove(prefMaxSuggestions); if (garbage instanceof StringBuilder) { mStringPool.add(garbage); } + } 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; + } + public boolean isValidWord(final CharSequence word) { if (word == null || word.length() == 0) { return false; @@ -352,21 +505,21 @@ public class Suggest implements Dictionary.WordCallback { || (mContactsDictionary != null && mContactsDictionary.isValidWord(word)); } - private void collectGarbage() { + private void collectGarbage(ArrayList suggestions, int prefMaxSuggestions) { int poolSize = mStringPool.size(); - int garbageSize = mSuggestions.size(); - while (poolSize < mPrefMaxSuggestions && garbageSize > 0) { - CharSequence garbage = mSuggestions.get(garbageSize - 1); + 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 == mPrefMaxSuggestions + 1) { + if (poolSize == prefMaxSuggestions + 1) { Log.w("Suggest", "String pool got too big: " + poolSize); } - mSuggestions.clear(); + suggestions.clear(); } public void close() { diff --git a/java/src/com/android/inputmethod/latin/TextEntryState.java b/java/src/com/android/inputmethod/latin/TextEntryState.java index d056ceb16..9011191f1 100644 --- a/java/src/com/android/inputmethod/latin/TextEntryState.java +++ b/java/src/com/android/inputmethod/latin/TextEntryState.java @@ -17,19 +17,22 @@ package com.android.inputmethod.latin; import android.content.Context; +import android.inputmethodservice.Keyboard.Key; import android.text.format.DateFormat; import android.util.Log; -import android.inputmethodservice.Keyboard.Key; - import java.io.FileOutputStream; import java.io.IOException; import java.util.Calendar; public class TextEntryState { + private static final boolean DBG = false; + + private static final String TAG = "TextEntryState"; + private static boolean LOGGING = false; - + private static int sBackspaceCount = 0; private static int sAutoSuggestCount = 0; @@ -43,35 +46,26 @@ public class TextEntryState { private static int sSessionCount = 0; private static int sTypedChars; - + private static int sActualChars; - - private static final String[] STATES = { - "Unknown", - "Start", - "In word", - "Accepted default", - "Picked suggestion", - "Punc. after word", - "Punc. after accepted", - "Space after accepted", - "Space after picked", - "Undo commit" - }; - - public static final int STATE_UNKNOWN = 0; - public static final int STATE_START = 1; - public static final int STATE_IN_WORD = 2; - public static final int STATE_ACCEPTED_DEFAULT = 3; - public static final int STATE_PICKED_SUGGESTION = 4; - public static final int STATE_PUNCTUATION_AFTER_WORD = 5; - public static final int STATE_PUNCTUATION_AFTER_ACCEPTED = 6; - public static final int STATE_SPACE_AFTER_ACCEPTED = 7; - public static final int STATE_SPACE_AFTER_PICKED = 8; - public static final int STATE_UNDO_COMMIT = 9; - - private static int sState = STATE_UNKNOWN; - + + public enum State { + UNKNOWN, + START, + IN_WORD, + ACCEPTED_DEFAULT, + PICKED_SUGGESTION, + PUNCTUATION_AFTER_WORD, + PUNCTUATION_AFTER_ACCEPTED, + SPACE_AFTER_ACCEPTED, + SPACE_AFTER_PICKED, + UNDO_COMMIT, + CORRECTING, + PICKED_CORRECTION; + } + + private static State sState = State.UNKNOWN; + private static FileOutputStream sKeyLocationFile; private static FileOutputStream sUserActionFile; @@ -84,7 +78,7 @@ public class TextEntryState { sWordNotInDictionaryCount = 0; sTypedChars = 0; sActualChars = 0; - sState = STATE_START; + sState = State.START; if (LOGGING) { try { @@ -129,90 +123,135 @@ public class TextEntryState { } sTypedChars += typedWord.length(); sActualChars += actualWord.length(); - sState = STATE_ACCEPTED_DEFAULT; + sState = State.ACCEPTED_DEFAULT; + LatinImeLogger.logOnAutoSuggestion(typedWord.toString(), actualWord.toString()); + displayState(); } - + + // 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: + sState = State.ACCEPTED_DEFAULT; + break; + } + displayState(); + } + public static void acceptedTyped(CharSequence typedWord) { sWordNotInDictionaryCount++; - sState = STATE_PICKED_SUGGESTION; + sState = State.PICKED_SUGGESTION; + displayState(); } public static void acceptedSuggestion(CharSequence typedWord, CharSequence actualWord) { sManualSuggestCount++; + State oldState = sState; if (typedWord.equals(actualWord)) { acceptedTyped(typedWord); } - sState = STATE_PICKED_SUGGESTION; + if (oldState == State.CORRECTING || oldState == State.PICKED_CORRECTION) { + sState = State.PICKED_CORRECTION; + } else { + sState = State.PICKED_SUGGESTION; + } + displayState(); } - + + public static void selectedForCorrection() { + sState = State.CORRECTING; + displayState(); + } + public static void typedCharacter(char c, boolean isSeparator) { boolean isSpace = c == ' '; switch (sState) { - case STATE_IN_WORD: + case IN_WORD: if (isSpace || isSeparator) { - sState = STATE_START; + sState = State.START; } else { // State hasn't changed. } break; - case STATE_ACCEPTED_DEFAULT: - case STATE_SPACE_AFTER_PICKED: + case ACCEPTED_DEFAULT: + case SPACE_AFTER_PICKED: if (isSpace) { - sState = STATE_SPACE_AFTER_ACCEPTED; + sState = State.SPACE_AFTER_ACCEPTED; } else if (isSeparator) { - sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + sState = State.PUNCTUATION_AFTER_ACCEPTED; } else { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } break; - case STATE_PICKED_SUGGESTION: + case PICKED_SUGGESTION: + case PICKED_CORRECTION: if (isSpace) { - sState = STATE_SPACE_AFTER_PICKED; + sState = State.SPACE_AFTER_PICKED; } else if (isSeparator) { // Swap - sState = STATE_PUNCTUATION_AFTER_ACCEPTED; + sState = State.PUNCTUATION_AFTER_ACCEPTED; } else { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } break; - case STATE_START: - case STATE_UNKNOWN: - case STATE_SPACE_AFTER_ACCEPTED: - case STATE_PUNCTUATION_AFTER_ACCEPTED: - case STATE_PUNCTUATION_AFTER_WORD: + case START: + case UNKNOWN: + case SPACE_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_ACCEPTED: + case PUNCTUATION_AFTER_WORD: if (!isSpace && !isSeparator) { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } else { - sState = STATE_START; + sState = State.START; } break; - case STATE_UNDO_COMMIT: + case UNDO_COMMIT: if (isSpace || isSeparator) { - sState = STATE_ACCEPTED_DEFAULT; + sState = State.ACCEPTED_DEFAULT; } else { - sState = STATE_IN_WORD; + sState = State.IN_WORD; } + break; + case CORRECTING: + sState = State.START; + break; } + displayState(); } public static void backspace() { - if (sState == STATE_ACCEPTED_DEFAULT) { - sState = STATE_UNDO_COMMIT; + if (sState == State.ACCEPTED_DEFAULT) { + sState = State.UNDO_COMMIT; sAutoSuggestUndoneCount++; - } else if (sState == STATE_UNDO_COMMIT) { - sState = STATE_IN_WORD; + LatinImeLogger.logOnAutoSuggestionCanceled(); + } else if (sState == State.UNDO_COMMIT) { + sState = State.IN_WORD; } sBackspaceCount++; + displayState(); } - + public static void reset() { - sState = STATE_START; + sState = State.START; + displayState(); } - - public static int getState() { + + public static State getState() { + if (DBG) { + Log.d(TAG, "Returning state = " + sState); + } return sState; } - + + public static boolean isCorrecting() { + return sState == State.CORRECTING || sState == State.PICKED_CORRECTION; + } + public static void keyPressedAt(Key key, int x, int y) { if (LOGGING && sKeyLocationFile != null && key.codes[0] >= 32) { String out = @@ -229,5 +268,11 @@ public class TextEntryState { } } } + + private static void displayState() { + if (DBG) { + Log.d(TAG, "State = " + sState); + } + } } diff --git a/java/src/com/android/inputmethod/latin/UserBigramDictionary.java b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java new file mode 100644 index 000000000..c3eab94f1 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/UserBigramDictionary.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * 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.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +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; + +/** + * 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; + + /** + * If this pair is typed 6 times, it would be suggested. + * Should be smaller than ContactsDictionary.FREQUENCY_FOR_CONTACTS_BIGRAM + */ + protected static final int SUGGEST_THRESHOLD = 6 * FREQUENCY_FOR_TYPED; + + /** 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 mPendingWrites = new HashSet(); + private final Object mPendingWritesLock = new Object(); + private static volatile boolean sUpdatingDB = false; + + private final static HashMap sDictProjectionMap; + + static { + sDictProjectionMap = new HashMap(); + 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 { + String word1; + String word2; + int frequency; + + Bigram(String word1, String word2, int frequency) { + this.word1 = word1; + this.word2 = word2; + this.frequency = frequency; + } + + @Override + public boolean equals(Object bigram) { + Bigram bigram2 = (Bigram) bigram; + return (word1.equals(bigram2.word1) && word2.equals(bigram2.word2)); + } + + @Override + public int hashCode() { + return (word1 + " " + word2).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 (mIme != null && mIme.getCurrentWord().isAutoCapitalized()) { + word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1); + } + + 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(); + } + } + + /** 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 { + private final HashSet mMap; + private final DatabaseHelper mDbHelper; + private final String mLocale; + + public UpdateDbTask(Context context, DatabaseHelper openHelper, + HashSet 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 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.word1, bi.word2, 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.word1, bi.word2, mLocale)); + pairId = pairIdLong.intValue(); + } + c.close(); + + // insert new frequency + long s = 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/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java index e8ca33af3..3315cf6c9 100644 --- a/java/src/com/android/inputmethod/latin/UserDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserDictionary.java @@ -38,7 +38,7 @@ public class UserDictionary extends ExpandableDictionary { private String mLocale; public UserDictionary(Context context, String locale) { - super(context); + super(context, Suggest.DIC_USER); mLocale = locale; // Perform a managed query. The Activity will handle closing and requerying the cursor // when needed. @@ -54,6 +54,7 @@ public class UserDictionary extends ExpandableDictionary { loadDictionary(); } + @Override public synchronized void close() { if (mObserver != null) { getContext().getContentResolver().unregisterContentObserver(mObserver); diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 19f714ae7..1ea74847a 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -44,11 +44,20 @@ public class WordComposer { */ private boolean mIsCapitalized; - WordComposer() { + public WordComposer() { mCodes = new ArrayList(12); mTypedWord = new StringBuilder(20); } + WordComposer(WordComposer copy) { + mCodes = (ArrayList) copy.mCodes.clone(); + mPreferredWord = copy.mPreferredWord; + mTypedWord = new StringBuilder(copy.mTypedWord); + mCapsCount = copy.mCapsCount; + mAutoCapitalized = copy.mAutoCapitalized; + mIsCapitalized = copy.mIsCapitalized; + } + /** * Clear out the keys registered so far. */ diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java deleted file mode 100644 index ccbf5b6bc..000000000 --- a/java/src/com/android/inputmethod/voice/LatinIMEWithVoice.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2009 Google Inc. - * - * 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.voice; - -import android.content.Intent; - -import com.android.inputmethod.latin.LatinIME; - -public class LatinIMEWithVoice extends LatinIME { - @Override - protected void launchSettings() { - launchSettings(LatinIMEWithVoiceSettings.class); - } -} diff --git a/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java b/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java deleted file mode 100644 index 13a58e14d..000000000 --- a/java/src/com/android/inputmethod/voice/LatinIMEWithVoiceSettings.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2009 Google Inc. - * - * 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.voice; - -import com.android.inputmethod.latin.LatinIMESettings; - -public class LatinIMEWithVoiceSettings extends LatinIMESettings {} diff --git a/java/src/com/android/inputmethod/voice/VoiceInput.java b/java/src/com/android/inputmethod/voice/VoiceInput.java index ac06ab50d..f24c180d0 100644 --- a/java/src/com/android/inputmethod/voice/VoiceInput.java +++ b/java/src/com/android/inputmethod/voice/VoiceInput.java @@ -25,6 +25,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.Parcelable; import android.speech.RecognitionListener; import android.speech.SpeechRecognizer; import android.speech.RecognizerIntent; @@ -52,6 +53,8 @@ public class VoiceInput implements OnClickListener { 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 final String DEFAULT_RECOMMENDED_PACKAGES = "com.android.mms " + @@ -63,7 +66,7 @@ public class VoiceInput implements OnClickListener { // 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 = false; + public static boolean ENABLE_WORD_CORRECTIONS = true; // Dummy word suggestion which means "delete current word" public static final String DELETE_SYMBOL = " \u00D7 "; // times symbol @@ -73,6 +76,25 @@ public class VoiceInput implements OnClickListener { 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. + 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 = @@ -304,12 +326,12 @@ public class VoiceInput implements OnClickListener { 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(); @@ -563,26 +585,42 @@ public class VoiceInput implements OnClickListener { public void onResults(Bundle resultsBundle) { List 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> alternatives = - new HashMap>(); - if (results.size() >= 2 && ENABLE_WORD_CORRECTIONS) { - final String[][] words = new String[results.size()][]; - for (int i = 0; i < words.length; i++) { - words[i] = results.get(i).split(" "); - } + new HashMap>(); - for (int key = 0; key < words[0].length; key++) { - alternatives.put(words[0][key], new ArrayList()); - for (int alt = 1; alt < words.length; alt++) { - int keyBegin = key * words[alt].length / words[0].length; - int keyEnd = (key + 1) * words[alt].length / words[0].length; - - for (int i = keyBegin; i < Math.min(words[alt].length, keyEnd); i++) { - List altList = alternatives.get(words[0][key]); - if (!altList.contains(words[alt][i]) && altList.size() < 6) { - altList.add(words[alt][i]); + 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 altList = alternatives.get(words[start]); + if (altList == null) { + altList = new ArrayList(); + 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); } } } diff --git a/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java b/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java deleted file mode 100644 index 8a339d14a..000000000 --- a/java/src/com/google/android/voicesearch/LatinIMEWithVoice.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * - * Copyright (C) 2009 Google Inc. - * - * 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.google.android.voicesearch; - -import android.content.Intent; - -import com.android.inputmethod.latin.LatinIME; - -public class LatinIMEWithVoice extends LatinIME { - @Override - protected void launchSettings() { - launchSettings(LatinIMEWithVoiceSettings.class); - } -} diff --git a/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java b/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java deleted file mode 100644 index a53cebfd9..000000000 --- a/java/src/com/google/android/voicesearch/LatinIMEWithVoiceSettings.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.google.android.voicesearch; - -import com.android.inputmethod.latin.LatinIMESettings; - -public class LatinIMEWithVoiceSettings extends LatinIMESettings {} diff --git a/native/Android.mk b/native/Android.mk index 12b6964b9..b2944699c 100644 --- a/native/Android.mk +++ b/native/Android.mk @@ -5,19 +5,11 @@ LOCAL_C_INCLUDES += $(LOCAL_PATH)/src LOCAL_SRC_FILES := \ jni/com_android_inputmethod_latin_BinaryDictionary.cpp \ - src/dictionary.cpp + src/dictionary.cpp \ + src/char_utils.cpp -LOCAL_C_INCLUDES += \ - external/icu4c/common \ - $(JNI_H_INCLUDE) - -LOCAL_LDLIBS := -lm - -LOCAL_SHARED_LIBRARIES := \ - libandroid_runtime \ - libcutils \ - libutils \ - libicuuc +LOCAL_NDK_VERSION := 4 +LOCAL_SDK_VERSION := 8 LOCAL_MODULE := libjni_latinime diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp index d068f3faf..bf7ec0d1a 100644 --- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp +++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp @@ -15,31 +15,18 @@ ** limitations under the License. */ -#define LOG_TAG "BinaryDictionary" -#include "utils/Log.h" - #include #include #include #include -#include -#include "utils/AssetManager.h" -#include "utils/Asset.h" - +#include #include "dictionary.h" // ---------------------------------------------------------------------------- using namespace latinime; -using namespace android; - -static jfieldID sDescriptorField; -static jfieldID sAssetManagerNativeField; -static jmethodID sAddWordMethod; -static jfieldID sDictLength; - // // helper function to throw an exception // @@ -54,35 +41,15 @@ static void throwException(JNIEnv *env, const char* ex, const char* fmt, int dat } static jint latinime_BinaryDictionary_open - (JNIEnv *env, jobject object, jobject assetManager, jstring resourceString, + (JNIEnv *env, jobject object, jobject dictDirectBuffer, jint typedLetterMultiplier, jint fullWordMultiplier) { - // Get the native file descriptor from the FileDescriptor object - AssetManager *am = (AssetManager*) env->GetIntField(assetManager, sAssetManagerNativeField); - if (!am) { - LOGE("DICT: Couldn't get AssetManager native peer\n"); - return 0; - } - const char *resourcePath = env->GetStringUTFChars(resourceString, NULL); - - Asset *dictAsset = am->openNonAsset(resourcePath, Asset::ACCESS_BUFFER); - if (dictAsset == NULL) { - LOGE("DICT: Couldn't get asset %s\n", resourcePath); - env->ReleaseStringUTFChars(resourceString, resourcePath); - return 0; - } - - void *dict = (void*) dictAsset->getBuffer(false); + void *dict = env->GetDirectBufferAddress(dictDirectBuffer); if (dict == NULL) { - LOGE("DICT: Dictionary buffer is null\n"); - env->ReleaseStringUTFChars(resourceString, resourcePath); + fprintf(stderr, "DICT: Dictionary buffer is null\n"); return 0; } Dictionary *dictionary = new Dictionary(dict, typedLetterMultiplier, fullWordMultiplier); - dictionary->setAsset(dictAsset); - env->SetIntField(object, sDictLength, (jint) dictAsset->getLength()); - - env->ReleaseStringUTFChars(resourceString, resourcePath); return (jint) dictionary; } @@ -92,8 +59,7 @@ static int latinime_BinaryDictionary_getSuggestions( jint maxAlternatives, jint skipPos, jintArray nextLettersArray, jint nextLettersSize) { Dictionary *dictionary = (Dictionary*) dict; - if (dictionary == NULL) - return 0; + if (dictionary == NULL) return 0; int *frequencies = env->GetIntArrayElements(frequencyArray, NULL); int *inputCodes = env->GetIntArrayElements(inputArray, NULL); @@ -101,8 +67,9 @@ static int latinime_BinaryDictionary_getSuggestions( int *nextLetters = nextLettersArray != NULL ? env->GetIntArrayElements(nextLettersArray, NULL) : NULL; - int count = dictionary->getSuggestions(inputCodes, arraySize, (unsigned short*) outputChars, frequencies, - maxWordLength, maxWords, maxAlternatives, skipPos, nextLetters, nextLettersSize); + int count = dictionary->getSuggestions(inputCodes, arraySize, (unsigned short*) outputChars, + frequencies, maxWordLength, maxWords, maxAlternatives, skipPos, nextLetters, + nextLettersSize); env->ReleaseIntArrayElements(frequencyArray, frequencies, 0); env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); @@ -114,6 +81,32 @@ static int latinime_BinaryDictionary_getSuggestions( return count; } +static int latinime_BinaryDictionary_getBigrams + (JNIEnv *env, jobject object, jint dict, jcharArray prevWordArray, jint prevWordLength, + jintArray inputArray, jint inputArraySize, jcharArray outputArray, + jintArray frequencyArray, jint maxWordLength, jint maxBigrams, jint maxAlternatives) +{ + Dictionary *dictionary = (Dictionary*) dict; + if (dictionary == NULL) return 0; + + jchar *prevWord = env->GetCharArrayElements(prevWordArray, NULL); + int *inputCodes = env->GetIntArrayElements(inputArray, NULL); + jchar *outputChars = env->GetCharArrayElements(outputArray, NULL); + int *frequencies = env->GetIntArrayElements(frequencyArray, NULL); + + int count = dictionary->getBigrams((unsigned short*) prevWord, prevWordLength, inputCodes, + inputArraySize, (unsigned short*) outputChars, frequencies, maxWordLength, maxBigrams, + maxAlternatives); + + env->ReleaseCharArrayElements(prevWordArray, prevWord, JNI_ABORT); + env->ReleaseIntArrayElements(inputArray, inputCodes, JNI_ABORT); + env->ReleaseCharArrayElements(outputArray, outputChars, 0); + env->ReleaseIntArrayElements(frequencyArray, frequencies, 0); + + return count; +} + + static jboolean latinime_BinaryDictionary_isValidWord (JNIEnv *env, jobject object, jint dict, jcharArray wordArray, jint wordLength) { @@ -131,18 +124,18 @@ static void latinime_BinaryDictionary_close (JNIEnv *env, jobject object, jint dict) { Dictionary *dictionary = (Dictionary*) dict; - ((Asset*) dictionary->getAsset())->close(); delete (Dictionary*) dict; } // ---------------------------------------------------------------------------- static JNINativeMethod gMethods[] = { - {"openNative", "(Landroid/content/res/AssetManager;Ljava/lang/String;II)I", + {"openNative", "(Ljava/nio/ByteBuffer;II)I", (void*)latinime_BinaryDictionary_open}, {"closeNative", "(I)V", (void*)latinime_BinaryDictionary_close}, {"getSuggestionsNative", "(I[II[C[IIIII[II)I", (void*)latinime_BinaryDictionary_getSuggestions}, - {"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord} + {"isValidWordNative", "(I[CI)Z", (void*)latinime_BinaryDictionary_isValidWord}, + {"getBigramsNative", "(I[CI[II[C[IIII)I", (void*)latinime_BinaryDictionary_getBigrams} }; static int registerNativeMethods(JNIEnv* env, const char* className, @@ -167,30 +160,6 @@ static int registerNativeMethods(JNIEnv* env, const char* className, static int registerNatives(JNIEnv *env) { const char* const kClassPathName = "com/android/inputmethod/latin/BinaryDictionary"; - jclass clazz; - - clazz = env->FindClass("java/io/FileDescriptor"); - if (clazz == NULL) { - LOGE("Can't find %s", "java/io/FileDescriptor"); - return -1; - } - sDescriptorField = env->GetFieldID(clazz, "descriptor", "I"); - - clazz = env->FindClass("android/content/res/AssetManager"); - if (clazz == NULL) { - LOGE("Can't find %s", "java/io/FileDescriptor"); - return -1; - } - sAssetManagerNativeField = env->GetFieldID(clazz, "mObject", "I"); - - // Get the field pointer for the dictionary length - clazz = env->FindClass(kClassPathName); - if (clazz == NULL) { - LOGE("Can't find %s", kClassPathName); - return -1; - } - sDictLength = env->GetFieldID(clazz, "mDictLength", "I"); - return registerNativeMethods(env, kClassPathName, gMethods, sizeof(gMethods) / sizeof(gMethods[0])); } diff --git a/native/src/char_utils.cpp b/native/src/char_utils.cpp new file mode 100644 index 000000000..a31a0632c --- /dev/null +++ b/native/src/char_utils.cpp @@ -0,0 +1,899 @@ +/* + * 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. + */ + +#include + +namespace latinime { + +struct LatinCapitalSmallPair { + unsigned short capital; + unsigned short small; +}; + +// Generated from http://unicode.org/Public/UNIDATA/UnicodeData.txt +// +// 1. Run the following code. Bascially taken from +// Dictionary::toLowerCase(unsigned short c) in dictionary.cpp. +// Then, get the list of chars where cc != ccc. +// +// unsigned short c, cc, ccc, ccc2; +// for (c = 0; c < 0xFFFF ; c++) { +// if (c < sizeof(BASE_CHARS) / sizeof(BASE_CHARS[0])) { +// cc = BASE_CHARS[c]; +// } else { +// cc = c; +// } +// +// // tolower +// int isBase = 0; +// if (cc >='A' && cc <= 'Z') { +// ccc = (cc | 0x20); +// ccc2 = ccc; +// isBase = 1; +// } else if (cc > 0x7F) { +// ccc = u_tolower(cc); +// ccc2 = latin_tolower(cc); +// } else { +// ccc = cc; +// ccc2 = ccc; +// } +// if (!isBase && cc != ccc) { +// wprintf(L" 0x%04X => 0x%04X => 0x%04X %lc => %lc => %lc \n", +// c, cc, ccc, c, cc, ccc); +// //assert(ccc == ccc2); +// } +// } +// +// Initially, started with an empty latin_tolower() as below. +// +// unsigned short latin_tolower(unsigned short c) { +// return c; +// } +// +// +// 2. Process the list obtained by 1 by the following perl script and apply +// 'sort -u' as well. Get the SORTED_CHAR_MAP[]. +// Note that '$1' in the perl script is 'cc' in the above C code. +// +// while(<>) { +// / 0x\w* => 0x(\w*) =/; +// open(HDL, "grep -iw ^" . $1 . " UnicodeData.txt | "); +// $line = ; +// chomp $line; +// @cols = split(/;/, $line); +// print " { 0x$1, 0x$cols[13] }, // $cols[1]\n"; +// } +// +// +// 3. Update the latin_tolower() function above with SORTED_CHAR_MAP. Enable +// the assert(ccc == ccc2) above and confirm the function exits successfully. +// +static const struct LatinCapitalSmallPair SORTED_CHAR_MAP[] = { + { 0x00C4, 0x00E4 }, // LATIN CAPITAL LETTER A WITH DIAERESIS + { 0x00C5, 0x00E5 }, // LATIN CAPITAL LETTER A WITH RING ABOVE + { 0x00C6, 0x00E6 }, // LATIN CAPITAL LETTER AE + { 0x00D0, 0x00F0 }, // LATIN CAPITAL LETTER ETH + { 0x00D5, 0x00F5 }, // LATIN CAPITAL LETTER O WITH TILDE + { 0x00D6, 0x00F6 }, // LATIN CAPITAL LETTER O WITH DIAERESIS + { 0x00D8, 0x00F8 }, // LATIN CAPITAL LETTER O WITH STROKE + { 0x00DC, 0x00FC }, // LATIN CAPITAL LETTER U WITH DIAERESIS + { 0x00DE, 0x00FE }, // LATIN CAPITAL LETTER THORN + { 0x0110, 0x0111 }, // LATIN CAPITAL LETTER D WITH STROKE + { 0x0126, 0x0127 }, // LATIN CAPITAL LETTER H WITH STROKE + { 0x0141, 0x0142 }, // LATIN CAPITAL LETTER L WITH STROKE + { 0x014A, 0x014B }, // LATIN CAPITAL LETTER ENG + { 0x0152, 0x0153 }, // LATIN CAPITAL LIGATURE OE + { 0x0166, 0x0167 }, // LATIN CAPITAL LETTER T WITH STROKE + { 0x0181, 0x0253 }, // LATIN CAPITAL LETTER B WITH HOOK + { 0x0182, 0x0183 }, // LATIN CAPITAL LETTER B WITH TOPBAR + { 0x0184, 0x0185 }, // LATIN CAPITAL LETTER TONE SIX + { 0x0186, 0x0254 }, // LATIN CAPITAL LETTER OPEN O + { 0x0187, 0x0188 }, // LATIN CAPITAL LETTER C WITH HOOK + { 0x0189, 0x0256 }, // LATIN CAPITAL LETTER AFRICAN D + { 0x018A, 0x0257 }, // LATIN CAPITAL LETTER D WITH HOOK + { 0x018B, 0x018C }, // LATIN CAPITAL LETTER D WITH TOPBAR + { 0x018E, 0x01DD }, // LATIN CAPITAL LETTER REVERSED E + { 0x018F, 0x0259 }, // LATIN CAPITAL LETTER SCHWA + { 0x0190, 0x025B }, // LATIN CAPITAL LETTER OPEN E + { 0x0191, 0x0192 }, // LATIN CAPITAL LETTER F WITH HOOK + { 0x0193, 0x0260 }, // LATIN CAPITAL LETTER G WITH HOOK + { 0x0194, 0x0263 }, // LATIN CAPITAL LETTER GAMMA + { 0x0196, 0x0269 }, // LATIN CAPITAL LETTER IOTA + { 0x0197, 0x0268 }, // LATIN CAPITAL LETTER I WITH STROKE + { 0x0198, 0x0199 }, // LATIN CAPITAL LETTER K WITH HOOK + { 0x019C, 0x026F }, // LATIN CAPITAL LETTER TURNED M + { 0x019D, 0x0272 }, // LATIN CAPITAL LETTER N WITH LEFT HOOK + { 0x019F, 0x0275 }, // LATIN CAPITAL LETTER O WITH MIDDLE TILDE + { 0x01A2, 0x01A3 }, // LATIN CAPITAL LETTER OI + { 0x01A4, 0x01A5 }, // LATIN CAPITAL LETTER P WITH HOOK + { 0x01A6, 0x0280 }, // LATIN LETTER YR + { 0x01A7, 0x01A8 }, // LATIN CAPITAL LETTER TONE TWO + { 0x01A9, 0x0283 }, // LATIN CAPITAL LETTER ESH + { 0x01AC, 0x01AD }, // LATIN CAPITAL LETTER T WITH HOOK + { 0x01AE, 0x0288 }, // LATIN CAPITAL LETTER T WITH RETROFLEX HOOK + { 0x01B1, 0x028A }, // LATIN CAPITAL LETTER UPSILON + { 0x01B2, 0x028B }, // LATIN CAPITAL LETTER V WITH HOOK + { 0x01B3, 0x01B4 }, // LATIN CAPITAL LETTER Y WITH HOOK + { 0x01B5, 0x01B6 }, // LATIN CAPITAL LETTER Z WITH STROKE + { 0x01B7, 0x0292 }, // LATIN CAPITAL LETTER EZH + { 0x01B8, 0x01B9 }, // LATIN CAPITAL LETTER EZH REVERSED + { 0x01BC, 0x01BD }, // LATIN CAPITAL LETTER TONE FIVE + { 0x01E4, 0x01E5 }, // LATIN CAPITAL LETTER G WITH STROKE + { 0x01EA, 0x01EB }, // LATIN CAPITAL LETTER O WITH OGONEK + { 0x01F6, 0x0195 }, // LATIN CAPITAL LETTER HWAIR + { 0x01F7, 0x01BF }, // LATIN CAPITAL LETTER WYNN + { 0x021C, 0x021D }, // LATIN CAPITAL LETTER YOGH + { 0x0220, 0x019E }, // LATIN CAPITAL LETTER N WITH LONG RIGHT LEG + { 0x0222, 0x0223 }, // LATIN CAPITAL LETTER OU + { 0x0224, 0x0225 }, // LATIN CAPITAL LETTER Z WITH HOOK + { 0x0226, 0x0227 }, // LATIN CAPITAL LETTER A WITH DOT ABOVE + { 0x022E, 0x022F }, // LATIN CAPITAL LETTER O WITH DOT ABOVE + { 0x023A, 0x2C65 }, // LATIN CAPITAL LETTER A WITH STROKE + { 0x023B, 0x023C }, // LATIN CAPITAL LETTER C WITH STROKE + { 0x023D, 0x019A }, // LATIN CAPITAL LETTER L WITH BAR + { 0x023E, 0x2C66 }, // LATIN CAPITAL LETTER T WITH DIAGONAL STROKE + { 0x0241, 0x0242 }, // LATIN CAPITAL LETTER GLOTTAL STOP + { 0x0243, 0x0180 }, // LATIN CAPITAL LETTER B WITH STROKE + { 0x0244, 0x0289 }, // LATIN CAPITAL LETTER U BAR + { 0x0245, 0x028C }, // LATIN CAPITAL LETTER TURNED V + { 0x0246, 0x0247 }, // LATIN CAPITAL LETTER E WITH STROKE + { 0x0248, 0x0249 }, // LATIN CAPITAL LETTER J WITH STROKE + { 0x024A, 0x024B }, // LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL + { 0x024C, 0x024D }, // LATIN CAPITAL LETTER R WITH STROKE + { 0x024E, 0x024F }, // LATIN CAPITAL LETTER Y WITH STROKE + { 0x0370, 0x0371 }, // GREEK CAPITAL LETTER HETA + { 0x0372, 0x0373 }, // GREEK CAPITAL LETTER ARCHAIC SAMPI + { 0x0376, 0x0377 }, // GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA + { 0x0391, 0x03B1 }, // GREEK CAPITAL LETTER ALPHA + { 0x0392, 0x03B2 }, // GREEK CAPITAL LETTER BETA + { 0x0393, 0x03B3 }, // GREEK CAPITAL LETTER GAMMA + { 0x0394, 0x03B4 }, // GREEK CAPITAL LETTER DELTA + { 0x0395, 0x03B5 }, // GREEK CAPITAL LETTER EPSILON + { 0x0396, 0x03B6 }, // GREEK CAPITAL LETTER ZETA + { 0x0397, 0x03B7 }, // GREEK CAPITAL LETTER ETA + { 0x0398, 0x03B8 }, // GREEK CAPITAL LETTER THETA + { 0x0399, 0x03B9 }, // GREEK CAPITAL LETTER IOTA + { 0x039A, 0x03BA }, // GREEK CAPITAL LETTER KAPPA + { 0x039B, 0x03BB }, // GREEK CAPITAL LETTER LAMDA + { 0x039C, 0x03BC }, // GREEK CAPITAL LETTER MU + { 0x039D, 0x03BD }, // GREEK CAPITAL LETTER NU + { 0x039E, 0x03BE }, // GREEK CAPITAL LETTER XI + { 0x039F, 0x03BF }, // GREEK CAPITAL LETTER OMICRON + { 0x03A0, 0x03C0 }, // GREEK CAPITAL LETTER PI + { 0x03A1, 0x03C1 }, // GREEK CAPITAL LETTER RHO + { 0x03A3, 0x03C3 }, // GREEK CAPITAL LETTER SIGMA + { 0x03A4, 0x03C4 }, // GREEK CAPITAL LETTER TAU + { 0x03A5, 0x03C5 }, // GREEK CAPITAL LETTER UPSILON + { 0x03A6, 0x03C6 }, // GREEK CAPITAL LETTER PHI + { 0x03A7, 0x03C7 }, // GREEK CAPITAL LETTER CHI + { 0x03A8, 0x03C8 }, // GREEK CAPITAL LETTER PSI + { 0x03A9, 0x03C9 }, // GREEK CAPITAL LETTER OMEGA + { 0x03CF, 0x03D7 }, // GREEK CAPITAL KAI SYMBOL + { 0x03D8, 0x03D9 }, // GREEK LETTER ARCHAIC KOPPA + { 0x03DA, 0x03DB }, // GREEK LETTER STIGMA + { 0x03DC, 0x03DD }, // GREEK LETTER DIGAMMA + { 0x03DE, 0x03DF }, // GREEK LETTER KOPPA + { 0x03E0, 0x03E1 }, // GREEK LETTER SAMPI + { 0x03E2, 0x03E3 }, // COPTIC CAPITAL LETTER SHEI + { 0x03E4, 0x03E5 }, // COPTIC CAPITAL LETTER FEI + { 0x03E6, 0x03E7 }, // COPTIC CAPITAL LETTER KHEI + { 0x03E8, 0x03E9 }, // COPTIC CAPITAL LETTER HORI + { 0x03EA, 0x03EB }, // COPTIC CAPITAL LETTER GANGIA + { 0x03EC, 0x03ED }, // COPTIC CAPITAL LETTER SHIMA + { 0x03EE, 0x03EF }, // COPTIC CAPITAL LETTER DEI + { 0x03F7, 0x03F8 }, // GREEK CAPITAL LETTER SHO + { 0x03FA, 0x03FB }, // GREEK CAPITAL LETTER SAN + { 0x03FD, 0x037B }, // GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL + { 0x03FE, 0x037C }, // GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL + { 0x03FF, 0x037D }, // GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL + { 0x0402, 0x0452 }, // CYRILLIC CAPITAL LETTER DJE + { 0x0404, 0x0454 }, // CYRILLIC CAPITAL LETTER UKRAINIAN IE + { 0x0405, 0x0455 }, // CYRILLIC CAPITAL LETTER DZE + { 0x0406, 0x0456 }, // CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I + { 0x0408, 0x0458 }, // CYRILLIC CAPITAL LETTER JE + { 0x0409, 0x0459 }, // CYRILLIC CAPITAL LETTER LJE + { 0x040A, 0x045A }, // CYRILLIC CAPITAL LETTER NJE + { 0x040B, 0x045B }, // CYRILLIC CAPITAL LETTER TSHE + { 0x040F, 0x045F }, // CYRILLIC CAPITAL LETTER DZHE + { 0x0410, 0x0430 }, // CYRILLIC CAPITAL LETTER A + { 0x0411, 0x0431 }, // CYRILLIC CAPITAL LETTER BE + { 0x0412, 0x0432 }, // CYRILLIC CAPITAL LETTER VE + { 0x0413, 0x0433 }, // CYRILLIC CAPITAL LETTER GHE + { 0x0414, 0x0434 }, // CYRILLIC CAPITAL LETTER DE + { 0x0415, 0x0435 }, // CYRILLIC CAPITAL LETTER IE + { 0x0416, 0x0436 }, // CYRILLIC CAPITAL LETTER ZHE + { 0x0417, 0x0437 }, // CYRILLIC CAPITAL LETTER ZE + { 0x0418, 0x0438 }, // CYRILLIC CAPITAL LETTER I + { 0x041A, 0x043A }, // CYRILLIC CAPITAL LETTER KA + { 0x041B, 0x043B }, // CYRILLIC CAPITAL LETTER EL + { 0x041C, 0x043C }, // CYRILLIC CAPITAL LETTER EM + { 0x041D, 0x043D }, // CYRILLIC CAPITAL LETTER EN + { 0x041E, 0x043E }, // CYRILLIC CAPITAL LETTER O + { 0x041F, 0x043F }, // CYRILLIC CAPITAL LETTER PE + { 0x0420, 0x0440 }, // CYRILLIC CAPITAL LETTER ER + { 0x0421, 0x0441 }, // CYRILLIC CAPITAL LETTER ES + { 0x0422, 0x0442 }, // CYRILLIC CAPITAL LETTER TE + { 0x0423, 0x0443 }, // CYRILLIC CAPITAL LETTER U + { 0x0424, 0x0444 }, // CYRILLIC CAPITAL LETTER EF + { 0x0425, 0x0445 }, // CYRILLIC CAPITAL LETTER HA + { 0x0426, 0x0446 }, // CYRILLIC CAPITAL LETTER TSE + { 0x0427, 0x0447 }, // CYRILLIC CAPITAL LETTER CHE + { 0x0428, 0x0448 }, // CYRILLIC CAPITAL LETTER SHA + { 0x0429, 0x0449 }, // CYRILLIC CAPITAL LETTER SHCHA + { 0x042A, 0x044A }, // CYRILLIC CAPITAL LETTER HARD SIGN + { 0x042B, 0x044B }, // CYRILLIC CAPITAL LETTER YERU + { 0x042C, 0x044C }, // CYRILLIC CAPITAL LETTER SOFT SIGN + { 0x042D, 0x044D }, // CYRILLIC CAPITAL LETTER E + { 0x042E, 0x044E }, // CYRILLIC CAPITAL LETTER YU + { 0x042F, 0x044F }, // CYRILLIC CAPITAL LETTER YA + { 0x0460, 0x0461 }, // CYRILLIC CAPITAL LETTER OMEGA + { 0x0462, 0x0463 }, // CYRILLIC CAPITAL LETTER YAT + { 0x0464, 0x0465 }, // CYRILLIC CAPITAL LETTER IOTIFIED E + { 0x0466, 0x0467 }, // CYRILLIC CAPITAL LETTER LITTLE YUS + { 0x0468, 0x0469 }, // CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS + { 0x046A, 0x046B }, // CYRILLIC CAPITAL LETTER BIG YUS + { 0x046C, 0x046D }, // CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS + { 0x046E, 0x046F }, // CYRILLIC CAPITAL LETTER KSI + { 0x0470, 0x0471 }, // CYRILLIC CAPITAL LETTER PSI + { 0x0472, 0x0473 }, // CYRILLIC CAPITAL LETTER FITA + { 0x0474, 0x0475 }, // CYRILLIC CAPITAL LETTER IZHITSA + { 0x0478, 0x0479 }, // CYRILLIC CAPITAL LETTER UK + { 0x047A, 0x047B }, // CYRILLIC CAPITAL LETTER ROUND OMEGA + { 0x047C, 0x047D }, // CYRILLIC CAPITAL LETTER OMEGA WITH TITLO + { 0x047E, 0x047F }, // CYRILLIC CAPITAL LETTER OT + { 0x0480, 0x0481 }, // CYRILLIC CAPITAL LETTER KOPPA + { 0x048A, 0x048B }, // CYRILLIC CAPITAL LETTER SHORT I WITH TAIL + { 0x048C, 0x048D }, // CYRILLIC CAPITAL LETTER SEMISOFT SIGN + { 0x048E, 0x048F }, // CYRILLIC CAPITAL LETTER ER WITH TICK + { 0x0490, 0x0491 }, // CYRILLIC CAPITAL LETTER GHE WITH UPTURN + { 0x0492, 0x0493 }, // CYRILLIC CAPITAL LETTER GHE WITH STROKE + { 0x0494, 0x0495 }, // CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK + { 0x0496, 0x0497 }, // CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER + { 0x0498, 0x0499 }, // CYRILLIC CAPITAL LETTER ZE WITH DESCENDER + { 0x049A, 0x049B }, // CYRILLIC CAPITAL LETTER KA WITH DESCENDER + { 0x049C, 0x049D }, // CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE + { 0x049E, 0x049F }, // CYRILLIC CAPITAL LETTER KA WITH STROKE + { 0x04A0, 0x04A1 }, // CYRILLIC CAPITAL LETTER BASHKIR KA + { 0x04A2, 0x04A3 }, // CYRILLIC CAPITAL LETTER EN WITH DESCENDER + { 0x04A4, 0x04A5 }, // CYRILLIC CAPITAL LIGATURE EN GHE + { 0x04A6, 0x04A7 }, // CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK + { 0x04A8, 0x04A9 }, // CYRILLIC CAPITAL LETTER ABKHASIAN HA + { 0x04AA, 0x04AB }, // CYRILLIC CAPITAL LETTER ES WITH DESCENDER + { 0x04AC, 0x04AD }, // CYRILLIC CAPITAL LETTER TE WITH DESCENDER + { 0x04AE, 0x04AF }, // CYRILLIC CAPITAL LETTER STRAIGHT U + { 0x04B0, 0x04B1 }, // CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE + { 0x04B2, 0x04B3 }, // CYRILLIC CAPITAL LETTER HA WITH DESCENDER + { 0x04B4, 0x04B5 }, // CYRILLIC CAPITAL LIGATURE TE TSE + { 0x04B6, 0x04B7 }, // CYRILLIC CAPITAL LETTER CHE WITH DESCENDER + { 0x04B8, 0x04B9 }, // CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE + { 0x04BA, 0x04BB }, // CYRILLIC CAPITAL LETTER SHHA + { 0x04BC, 0x04BD }, // CYRILLIC CAPITAL LETTER ABKHASIAN CHE + { 0x04BE, 0x04BF }, // CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER + { 0x04C0, 0x04CF }, // CYRILLIC LETTER PALOCHKA + { 0x04C3, 0x04C4 }, // CYRILLIC CAPITAL LETTER KA WITH HOOK + { 0x04C5, 0x04C6 }, // CYRILLIC CAPITAL LETTER EL WITH TAIL + { 0x04C7, 0x04C8 }, // CYRILLIC CAPITAL LETTER EN WITH HOOK + { 0x04C9, 0x04CA }, // CYRILLIC CAPITAL LETTER EN WITH TAIL + { 0x04CB, 0x04CC }, // CYRILLIC CAPITAL LETTER KHAKASSIAN CHE + { 0x04CD, 0x04CE }, // CYRILLIC CAPITAL LETTER EM WITH TAIL + { 0x04D4, 0x04D5 }, // CYRILLIC CAPITAL LIGATURE A IE + { 0x04D8, 0x04D9 }, // CYRILLIC CAPITAL LETTER SCHWA + { 0x04E0, 0x04E1 }, // CYRILLIC CAPITAL LETTER ABKHASIAN DZE + { 0x04E8, 0x04E9 }, // CYRILLIC CAPITAL LETTER BARRED O + { 0x04F6, 0x04F7 }, // CYRILLIC CAPITAL LETTER GHE WITH DESCENDER + { 0x04FA, 0x04FB }, // CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK + { 0x04FC, 0x04FD }, // CYRILLIC CAPITAL LETTER HA WITH HOOK + { 0x04FE, 0x04FF }, // CYRILLIC CAPITAL LETTER HA WITH STROKE + { 0x0500, 0x0501 }, // CYRILLIC CAPITAL LETTER KOMI DE + { 0x0502, 0x0503 }, // CYRILLIC CAPITAL LETTER KOMI DJE + { 0x0504, 0x0505 }, // CYRILLIC CAPITAL LETTER KOMI ZJE + { 0x0506, 0x0507 }, // CYRILLIC CAPITAL LETTER KOMI DZJE + { 0x0508, 0x0509 }, // CYRILLIC CAPITAL LETTER KOMI LJE + { 0x050A, 0x050B }, // CYRILLIC CAPITAL LETTER KOMI NJE + { 0x050C, 0x050D }, // CYRILLIC CAPITAL LETTER KOMI SJE + { 0x050E, 0x050F }, // CYRILLIC CAPITAL LETTER KOMI TJE + { 0x0510, 0x0511 }, // CYRILLIC CAPITAL LETTER REVERSED ZE + { 0x0512, 0x0513 }, // CYRILLIC CAPITAL LETTER EL WITH HOOK + { 0x0514, 0x0515 }, // CYRILLIC CAPITAL LETTER LHA + { 0x0516, 0x0517 }, // CYRILLIC CAPITAL LETTER RHA + { 0x0518, 0x0519 }, // CYRILLIC CAPITAL LETTER YAE + { 0x051A, 0x051B }, // CYRILLIC CAPITAL LETTER QA + { 0x051C, 0x051D }, // CYRILLIC CAPITAL LETTER WE + { 0x051E, 0x051F }, // CYRILLIC CAPITAL LETTER ALEUT KA + { 0x0520, 0x0521 }, // CYRILLIC CAPITAL LETTER EL WITH MIDDLE HOOK + { 0x0522, 0x0523 }, // CYRILLIC CAPITAL LETTER EN WITH MIDDLE HOOK + { 0x0524, 0x0525 }, // CYRILLIC CAPITAL LETTER PE WITH DESCENDER + { 0x0531, 0x0561 }, // ARMENIAN CAPITAL LETTER AYB + { 0x0532, 0x0562 }, // ARMENIAN CAPITAL LETTER BEN + { 0x0533, 0x0563 }, // ARMENIAN CAPITAL LETTER GIM + { 0x0534, 0x0564 }, // ARMENIAN CAPITAL LETTER DA + { 0x0535, 0x0565 }, // ARMENIAN CAPITAL LETTER ECH + { 0x0536, 0x0566 }, // ARMENIAN CAPITAL LETTER ZA + { 0x0537, 0x0567 }, // ARMENIAN CAPITAL LETTER EH + { 0x0538, 0x0568 }, // ARMENIAN CAPITAL LETTER ET + { 0x0539, 0x0569 }, // ARMENIAN CAPITAL LETTER TO + { 0x053A, 0x056A }, // ARMENIAN CAPITAL LETTER ZHE + { 0x053B, 0x056B }, // ARMENIAN CAPITAL LETTER INI + { 0x053C, 0x056C }, // ARMENIAN CAPITAL LETTER LIWN + { 0x053D, 0x056D }, // ARMENIAN CAPITAL LETTER XEH + { 0x053E, 0x056E }, // ARMENIAN CAPITAL LETTER CA + { 0x053F, 0x056F }, // ARMENIAN CAPITAL LETTER KEN + { 0x0540, 0x0570 }, // ARMENIAN CAPITAL LETTER HO + { 0x0541, 0x0571 }, // ARMENIAN CAPITAL LETTER JA + { 0x0542, 0x0572 }, // ARMENIAN CAPITAL LETTER GHAD + { 0x0543, 0x0573 }, // ARMENIAN CAPITAL LETTER CHEH + { 0x0544, 0x0574 }, // ARMENIAN CAPITAL LETTER MEN + { 0x0545, 0x0575 }, // ARMENIAN CAPITAL LETTER YI + { 0x0546, 0x0576 }, // ARMENIAN CAPITAL LETTER NOW + { 0x0547, 0x0577 }, // ARMENIAN CAPITAL LETTER SHA + { 0x0548, 0x0578 }, // ARMENIAN CAPITAL LETTER VO + { 0x0549, 0x0579 }, // ARMENIAN CAPITAL LETTER CHA + { 0x054A, 0x057A }, // ARMENIAN CAPITAL LETTER PEH + { 0x054B, 0x057B }, // ARMENIAN CAPITAL LETTER JHEH + { 0x054C, 0x057C }, // ARMENIAN CAPITAL LETTER RA + { 0x054D, 0x057D }, // ARMENIAN CAPITAL LETTER SEH + { 0x054E, 0x057E }, // ARMENIAN CAPITAL LETTER VEW + { 0x054F, 0x057F }, // ARMENIAN CAPITAL LETTER TIWN + { 0x0550, 0x0580 }, // ARMENIAN CAPITAL LETTER REH + { 0x0551, 0x0581 }, // ARMENIAN CAPITAL LETTER CO + { 0x0552, 0x0582 }, // ARMENIAN CAPITAL LETTER YIWN + { 0x0553, 0x0583 }, // ARMENIAN CAPITAL LETTER PIWR + { 0x0554, 0x0584 }, // ARMENIAN CAPITAL LETTER KEH + { 0x0555, 0x0585 }, // ARMENIAN CAPITAL LETTER OH + { 0x0556, 0x0586 }, // ARMENIAN CAPITAL LETTER FEH + { 0x10A0, 0x2D00 }, // GEORGIAN CAPITAL LETTER AN + { 0x10A1, 0x2D01 }, // GEORGIAN CAPITAL LETTER BAN + { 0x10A2, 0x2D02 }, // GEORGIAN CAPITAL LETTER GAN + { 0x10A3, 0x2D03 }, // GEORGIAN CAPITAL LETTER DON + { 0x10A4, 0x2D04 }, // GEORGIAN CAPITAL LETTER EN + { 0x10A5, 0x2D05 }, // GEORGIAN CAPITAL LETTER VIN + { 0x10A6, 0x2D06 }, // GEORGIAN CAPITAL LETTER ZEN + { 0x10A7, 0x2D07 }, // GEORGIAN CAPITAL LETTER TAN + { 0x10A8, 0x2D08 }, // GEORGIAN CAPITAL LETTER IN + { 0x10A9, 0x2D09 }, // GEORGIAN CAPITAL LETTER KAN + { 0x10AA, 0x2D0A }, // GEORGIAN CAPITAL LETTER LAS + { 0x10AB, 0x2D0B }, // GEORGIAN CAPITAL LETTER MAN + { 0x10AC, 0x2D0C }, // GEORGIAN CAPITAL LETTER NAR + { 0x10AD, 0x2D0D }, // GEORGIAN CAPITAL LETTER ON + { 0x10AE, 0x2D0E }, // GEORGIAN CAPITAL LETTER PAR + { 0x10AF, 0x2D0F }, // GEORGIAN CAPITAL LETTER ZHAR + { 0x10B0, 0x2D10 }, // GEORGIAN CAPITAL LETTER RAE + { 0x10B1, 0x2D11 }, // GEORGIAN CAPITAL LETTER SAN + { 0x10B2, 0x2D12 }, // GEORGIAN CAPITAL LETTER TAR + { 0x10B3, 0x2D13 }, // GEORGIAN CAPITAL LETTER UN + { 0x10B4, 0x2D14 }, // GEORGIAN CAPITAL LETTER PHAR + { 0x10B5, 0x2D15 }, // GEORGIAN CAPITAL LETTER KHAR + { 0x10B6, 0x2D16 }, // GEORGIAN CAPITAL LETTER GHAN + { 0x10B7, 0x2D17 }, // GEORGIAN CAPITAL LETTER QAR + { 0x10B8, 0x2D18 }, // GEORGIAN CAPITAL LETTER SHIN + { 0x10B9, 0x2D19 }, // GEORGIAN CAPITAL LETTER CHIN + { 0x10BA, 0x2D1A }, // GEORGIAN CAPITAL LETTER CAN + { 0x10BB, 0x2D1B }, // GEORGIAN CAPITAL LETTER JIL + { 0x10BC, 0x2D1C }, // GEORGIAN CAPITAL LETTER CIL + { 0x10BD, 0x2D1D }, // GEORGIAN CAPITAL LETTER CHAR + { 0x10BE, 0x2D1E }, // GEORGIAN CAPITAL LETTER XAN + { 0x10BF, 0x2D1F }, // GEORGIAN CAPITAL LETTER JHAN + { 0x10C0, 0x2D20 }, // GEORGIAN CAPITAL LETTER HAE + { 0x10C1, 0x2D21 }, // GEORGIAN CAPITAL LETTER HE + { 0x10C2, 0x2D22 }, // GEORGIAN CAPITAL LETTER HIE + { 0x10C3, 0x2D23 }, // GEORGIAN CAPITAL LETTER WE + { 0x10C4, 0x2D24 }, // GEORGIAN CAPITAL LETTER HAR + { 0x10C5, 0x2D25 }, // GEORGIAN CAPITAL LETTER HOE + { 0x1E00, 0x1E01 }, // LATIN CAPITAL LETTER A WITH RING BELOW + { 0x1E02, 0x1E03 }, // LATIN CAPITAL LETTER B WITH DOT ABOVE + { 0x1E04, 0x1E05 }, // LATIN CAPITAL LETTER B WITH DOT BELOW + { 0x1E06, 0x1E07 }, // LATIN CAPITAL LETTER B WITH LINE BELOW + { 0x1E08, 0x1E09 }, // LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE + { 0x1E0A, 0x1E0B }, // LATIN CAPITAL LETTER D WITH DOT ABOVE + { 0x1E0C, 0x1E0D }, // LATIN CAPITAL LETTER D WITH DOT BELOW + { 0x1E0E, 0x1E0F }, // LATIN CAPITAL LETTER D WITH LINE BELOW + { 0x1E10, 0x1E11 }, // LATIN CAPITAL LETTER D WITH CEDILLA + { 0x1E12, 0x1E13 }, // LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW + { 0x1E14, 0x1E15 }, // LATIN CAPITAL LETTER E WITH MACRON AND GRAVE + { 0x1E16, 0x1E17 }, // LATIN CAPITAL LETTER E WITH MACRON AND ACUTE + { 0x1E18, 0x1E19 }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW + { 0x1E1A, 0x1E1B }, // LATIN CAPITAL LETTER E WITH TILDE BELOW + { 0x1E1C, 0x1E1D }, // LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE + { 0x1E1E, 0x1E1F }, // LATIN CAPITAL LETTER F WITH DOT ABOVE + { 0x1E20, 0x1E21 }, // LATIN CAPITAL LETTER G WITH MACRON + { 0x1E22, 0x1E23 }, // LATIN CAPITAL LETTER H WITH DOT ABOVE + { 0x1E24, 0x1E25 }, // LATIN CAPITAL LETTER H WITH DOT BELOW + { 0x1E26, 0x1E27 }, // LATIN CAPITAL LETTER H WITH DIAERESIS + { 0x1E28, 0x1E29 }, // LATIN CAPITAL LETTER H WITH CEDILLA + { 0x1E2A, 0x1E2B }, // LATIN CAPITAL LETTER H WITH BREVE BELOW + { 0x1E2C, 0x1E2D }, // LATIN CAPITAL LETTER I WITH TILDE BELOW + { 0x1E2E, 0x1E2F }, // LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE + { 0x1E30, 0x1E31 }, // LATIN CAPITAL LETTER K WITH ACUTE + { 0x1E32, 0x1E33 }, // LATIN CAPITAL LETTER K WITH DOT BELOW + { 0x1E34, 0x1E35 }, // LATIN CAPITAL LETTER K WITH LINE BELOW + { 0x1E36, 0x1E37 }, // LATIN CAPITAL LETTER L WITH DOT BELOW + { 0x1E38, 0x1E39 }, // LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON + { 0x1E3A, 0x1E3B }, // LATIN CAPITAL LETTER L WITH LINE BELOW + { 0x1E3C, 0x1E3D }, // LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW + { 0x1E3E, 0x1E3F }, // LATIN CAPITAL LETTER M WITH ACUTE + { 0x1E40, 0x1E41 }, // LATIN CAPITAL LETTER M WITH DOT ABOVE + { 0x1E42, 0x1E43 }, // LATIN CAPITAL LETTER M WITH DOT BELOW + { 0x1E44, 0x1E45 }, // LATIN CAPITAL LETTER N WITH DOT ABOVE + { 0x1E46, 0x1E47 }, // LATIN CAPITAL LETTER N WITH DOT BELOW + { 0x1E48, 0x1E49 }, // LATIN CAPITAL LETTER N WITH LINE BELOW + { 0x1E4A, 0x1E4B }, // LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW + { 0x1E4C, 0x1E4D }, // LATIN CAPITAL LETTER O WITH TILDE AND ACUTE + { 0x1E4E, 0x1E4F }, // LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS + { 0x1E50, 0x1E51 }, // LATIN CAPITAL LETTER O WITH MACRON AND GRAVE + { 0x1E52, 0x1E53 }, // LATIN CAPITAL LETTER O WITH MACRON AND ACUTE + { 0x1E54, 0x1E55 }, // LATIN CAPITAL LETTER P WITH ACUTE + { 0x1E56, 0x1E57 }, // LATIN CAPITAL LETTER P WITH DOT ABOVE + { 0x1E58, 0x1E59 }, // LATIN CAPITAL LETTER R WITH DOT ABOVE + { 0x1E5A, 0x1E5B }, // LATIN CAPITAL LETTER R WITH DOT BELOW + { 0x1E5C, 0x1E5D }, // LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON + { 0x1E5E, 0x1E5F }, // LATIN CAPITAL LETTER R WITH LINE BELOW + { 0x1E60, 0x1E61 }, // LATIN CAPITAL LETTER S WITH DOT ABOVE + { 0x1E62, 0x1E63 }, // LATIN CAPITAL LETTER S WITH DOT BELOW + { 0x1E64, 0x1E65 }, // LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE + { 0x1E66, 0x1E67 }, // LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE + { 0x1E68, 0x1E69 }, // LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE + { 0x1E6A, 0x1E6B }, // LATIN CAPITAL LETTER T WITH DOT ABOVE + { 0x1E6C, 0x1E6D }, // LATIN CAPITAL LETTER T WITH DOT BELOW + { 0x1E6E, 0x1E6F }, // LATIN CAPITAL LETTER T WITH LINE BELOW + { 0x1E70, 0x1E71 }, // LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW + { 0x1E72, 0x1E73 }, // LATIN CAPITAL LETTER U WITH DIAERESIS BELOW + { 0x1E74, 0x1E75 }, // LATIN CAPITAL LETTER U WITH TILDE BELOW + { 0x1E76, 0x1E77 }, // LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW + { 0x1E78, 0x1E79 }, // LATIN CAPITAL LETTER U WITH TILDE AND ACUTE + { 0x1E7A, 0x1E7B }, // LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS + { 0x1E7C, 0x1E7D }, // LATIN CAPITAL LETTER V WITH TILDE + { 0x1E7E, 0x1E7F }, // LATIN CAPITAL LETTER V WITH DOT BELOW + { 0x1E80, 0x1E81 }, // LATIN CAPITAL LETTER W WITH GRAVE + { 0x1E82, 0x1E83 }, // LATIN CAPITAL LETTER W WITH ACUTE + { 0x1E84, 0x1E85 }, // LATIN CAPITAL LETTER W WITH DIAERESIS + { 0x1E86, 0x1E87 }, // LATIN CAPITAL LETTER W WITH DOT ABOVE + { 0x1E88, 0x1E89 }, // LATIN CAPITAL LETTER W WITH DOT BELOW + { 0x1E8A, 0x1E8B }, // LATIN CAPITAL LETTER X WITH DOT ABOVE + { 0x1E8C, 0x1E8D }, // LATIN CAPITAL LETTER X WITH DIAERESIS + { 0x1E8E, 0x1E8F }, // LATIN CAPITAL LETTER Y WITH DOT ABOVE + { 0x1E90, 0x1E91 }, // LATIN CAPITAL LETTER Z WITH CIRCUMFLEX + { 0x1E92, 0x1E93 }, // LATIN CAPITAL LETTER Z WITH DOT BELOW + { 0x1E94, 0x1E95 }, // LATIN CAPITAL LETTER Z WITH LINE BELOW + { 0x1E9E, 0x00DF }, // LATIN CAPITAL LETTER SHARP S + { 0x1EA0, 0x1EA1 }, // LATIN CAPITAL LETTER A WITH DOT BELOW + { 0x1EA2, 0x1EA3 }, // LATIN CAPITAL LETTER A WITH HOOK ABOVE + { 0x1EA4, 0x1EA5 }, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE + { 0x1EA6, 0x1EA7 }, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE + { 0x1EA8, 0x1EA9 }, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE + { 0x1EAA, 0x1EAB }, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE + { 0x1EAC, 0x1EAD }, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW + { 0x1EAE, 0x1EAF }, // LATIN CAPITAL LETTER A WITH BREVE AND ACUTE + { 0x1EB0, 0x1EB1 }, // LATIN CAPITAL LETTER A WITH BREVE AND GRAVE + { 0x1EB2, 0x1EB3 }, // LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE + { 0x1EB4, 0x1EB5 }, // LATIN CAPITAL LETTER A WITH BREVE AND TILDE + { 0x1EB6, 0x1EB7 }, // LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW + { 0x1EB8, 0x1EB9 }, // LATIN CAPITAL LETTER E WITH DOT BELOW + { 0x1EBA, 0x1EBB }, // LATIN CAPITAL LETTER E WITH HOOK ABOVE + { 0x1EBC, 0x1EBD }, // LATIN CAPITAL LETTER E WITH TILDE + { 0x1EBE, 0x1EBF }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE + { 0x1EC0, 0x1EC1 }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE + { 0x1EC2, 0x1EC3 }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE + { 0x1EC4, 0x1EC5 }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE + { 0x1EC6, 0x1EC7 }, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW + { 0x1EC8, 0x1EC9 }, // LATIN CAPITAL LETTER I WITH HOOK ABOVE + { 0x1ECA, 0x1ECB }, // LATIN CAPITAL LETTER I WITH DOT BELOW + { 0x1ECC, 0x1ECD }, // LATIN CAPITAL LETTER O WITH DOT BELOW + { 0x1ECE, 0x1ECF }, // LATIN CAPITAL LETTER O WITH HOOK ABOVE + { 0x1ED0, 0x1ED1 }, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE + { 0x1ED2, 0x1ED3 }, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE + { 0x1ED4, 0x1ED5 }, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE + { 0x1ED6, 0x1ED7 }, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE + { 0x1ED8, 0x1ED9 }, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW + { 0x1EDA, 0x1EDB }, // LATIN CAPITAL LETTER O WITH HORN AND ACUTE + { 0x1EDC, 0x1EDD }, // LATIN CAPITAL LETTER O WITH HORN AND GRAVE + { 0x1EDE, 0x1EDF }, // LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE + { 0x1EE0, 0x1EE1 }, // LATIN CAPITAL LETTER O WITH HORN AND TILDE + { 0x1EE2, 0x1EE3 }, // LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW + { 0x1EE4, 0x1EE5 }, // LATIN CAPITAL LETTER U WITH DOT BELOW + { 0x1EE6, 0x1EE7 }, // LATIN CAPITAL LETTER U WITH HOOK ABOVE + { 0x1EE8, 0x1EE9 }, // LATIN CAPITAL LETTER U WITH HORN AND ACUTE + { 0x1EEA, 0x1EEB }, // LATIN CAPITAL LETTER U WITH HORN AND GRAVE + { 0x1EEC, 0x1EED }, // LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE + { 0x1EEE, 0x1EEF }, // LATIN CAPITAL LETTER U WITH HORN AND TILDE + { 0x1EF0, 0x1EF1 }, // LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW + { 0x1EF2, 0x1EF3 }, // LATIN CAPITAL LETTER Y WITH GRAVE + { 0x1EF4, 0x1EF5 }, // LATIN CAPITAL LETTER Y WITH DOT BELOW + { 0x1EF6, 0x1EF7 }, // LATIN CAPITAL LETTER Y WITH HOOK ABOVE + { 0x1EF8, 0x1EF9 }, // LATIN CAPITAL LETTER Y WITH TILDE + { 0x1EFA, 0x1EFB }, // LATIN CAPITAL LETTER MIDDLE-WELSH LL + { 0x1EFC, 0x1EFD }, // LATIN CAPITAL LETTER MIDDLE-WELSH V + { 0x1EFE, 0x1EFF }, // LATIN CAPITAL LETTER Y WITH LOOP + { 0x1F08, 0x1F00 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI + { 0x1F09, 0x1F01 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA + { 0x1F0A, 0x1F02 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA + { 0x1F0B, 0x1F03 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA + { 0x1F0C, 0x1F04 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA + { 0x1F0D, 0x1F05 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA + { 0x1F0E, 0x1F06 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI + { 0x1F0F, 0x1F07 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI + { 0x1F18, 0x1F10 }, // GREEK CAPITAL LETTER EPSILON WITH PSILI + { 0x1F19, 0x1F11 }, // GREEK CAPITAL LETTER EPSILON WITH DASIA + { 0x1F1A, 0x1F12 }, // GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA + { 0x1F1B, 0x1F13 }, // GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA + { 0x1F1C, 0x1F14 }, // GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA + { 0x1F1D, 0x1F15 }, // GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA + { 0x1F28, 0x1F20 }, // GREEK CAPITAL LETTER ETA WITH PSILI + { 0x1F29, 0x1F21 }, // GREEK CAPITAL LETTER ETA WITH DASIA + { 0x1F2A, 0x1F22 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA + { 0x1F2B, 0x1F23 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA + { 0x1F2C, 0x1F24 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA + { 0x1F2D, 0x1F25 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA + { 0x1F2E, 0x1F26 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI + { 0x1F2F, 0x1F27 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI + { 0x1F38, 0x1F30 }, // GREEK CAPITAL LETTER IOTA WITH PSILI + { 0x1F39, 0x1F31 }, // GREEK CAPITAL LETTER IOTA WITH DASIA + { 0x1F3A, 0x1F32 }, // GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA + { 0x1F3B, 0x1F33 }, // GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA + { 0x1F3C, 0x1F34 }, // GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA + { 0x1F3D, 0x1F35 }, // GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA + { 0x1F3E, 0x1F36 }, // GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI + { 0x1F3F, 0x1F37 }, // GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI + { 0x1F48, 0x1F40 }, // GREEK CAPITAL LETTER OMICRON WITH PSILI + { 0x1F49, 0x1F41 }, // GREEK CAPITAL LETTER OMICRON WITH DASIA + { 0x1F4A, 0x1F42 }, // GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA + { 0x1F4B, 0x1F43 }, // GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA + { 0x1F4C, 0x1F44 }, // GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA + { 0x1F4D, 0x1F45 }, // GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA + { 0x1F59, 0x1F51 }, // GREEK CAPITAL LETTER UPSILON WITH DASIA + { 0x1F5B, 0x1F53 }, // GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA + { 0x1F5D, 0x1F55 }, // GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA + { 0x1F5F, 0x1F57 }, // GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI + { 0x1F68, 0x1F60 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI + { 0x1F69, 0x1F61 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA + { 0x1F6A, 0x1F62 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA + { 0x1F6B, 0x1F63 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA + { 0x1F6C, 0x1F64 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA + { 0x1F6D, 0x1F65 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA + { 0x1F6E, 0x1F66 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI + { 0x1F6F, 0x1F67 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI + { 0x1F88, 0x1F80 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI + { 0x1F89, 0x1F81 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI + { 0x1F8A, 0x1F82 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI + { 0x1F8B, 0x1F83 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI + { 0x1F8C, 0x1F84 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI + { 0x1F8D, 0x1F85 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI + { 0x1F8E, 0x1F86 }, // GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1F8F, 0x1F87 }, // GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1F98, 0x1F90 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI + { 0x1F99, 0x1F91 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI + { 0x1F9A, 0x1F92 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI + { 0x1F9B, 0x1F93 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI + { 0x1F9C, 0x1F94 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI + { 0x1F9D, 0x1F95 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI + { 0x1F9E, 0x1F96 }, // GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1F9F, 0x1F97 }, // GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1FA8, 0x1FA0 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI + { 0x1FA9, 0x1FA1 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI + { 0x1FAA, 0x1FA2 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI + { 0x1FAB, 0x1FA3 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI + { 0x1FAC, 0x1FA4 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI + { 0x1FAD, 0x1FA5 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI + { 0x1FAE, 0x1FA6 }, // GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1FAF, 0x1FA7 }, // GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI + { 0x1FB8, 0x1FB0 }, // GREEK CAPITAL LETTER ALPHA WITH VRACHY + { 0x1FB9, 0x1FB1 }, // GREEK CAPITAL LETTER ALPHA WITH MACRON + { 0x1FBA, 0x1F70 }, // GREEK CAPITAL LETTER ALPHA WITH VARIA + { 0x1FBB, 0x1F71 }, // GREEK CAPITAL LETTER ALPHA WITH OXIA + { 0x1FBC, 0x1FB3 }, // GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI + { 0x1FC8, 0x1F72 }, // GREEK CAPITAL LETTER EPSILON WITH VARIA + { 0x1FC9, 0x1F73 }, // GREEK CAPITAL LETTER EPSILON WITH OXIA + { 0x1FCA, 0x1F74 }, // GREEK CAPITAL LETTER ETA WITH VARIA + { 0x1FCB, 0x1F75 }, // GREEK CAPITAL LETTER ETA WITH OXIA + { 0x1FCC, 0x1FC3 }, // GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI + { 0x1FD8, 0x1FD0 }, // GREEK CAPITAL LETTER IOTA WITH VRACHY + { 0x1FD9, 0x1FD1 }, // GREEK CAPITAL LETTER IOTA WITH MACRON + { 0x1FDA, 0x1F76 }, // GREEK CAPITAL LETTER IOTA WITH VARIA + { 0x1FDB, 0x1F77 }, // GREEK CAPITAL LETTER IOTA WITH OXIA + { 0x1FE8, 0x1FE0 }, // GREEK CAPITAL LETTER UPSILON WITH VRACHY + { 0x1FE9, 0x1FE1 }, // GREEK CAPITAL LETTER UPSILON WITH MACRON + { 0x1FEA, 0x1F7A }, // GREEK CAPITAL LETTER UPSILON WITH VARIA + { 0x1FEB, 0x1F7B }, // GREEK CAPITAL LETTER UPSILON WITH OXIA + { 0x1FEC, 0x1FE5 }, // GREEK CAPITAL LETTER RHO WITH DASIA + { 0x1FF8, 0x1F78 }, // GREEK CAPITAL LETTER OMICRON WITH VARIA + { 0x1FF9, 0x1F79 }, // GREEK CAPITAL LETTER OMICRON WITH OXIA + { 0x1FFA, 0x1F7C }, // GREEK CAPITAL LETTER OMEGA WITH VARIA + { 0x1FFB, 0x1F7D }, // GREEK CAPITAL LETTER OMEGA WITH OXIA + { 0x1FFC, 0x1FF3 }, // GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI + { 0x2126, 0x03C9 }, // OHM SIGN + { 0x212A, 0x006B }, // KELVIN SIGN + { 0x212B, 0x00E5 }, // ANGSTROM SIGN + { 0x2132, 0x214E }, // TURNED CAPITAL F + { 0x2160, 0x2170 }, // ROMAN NUMERAL ONE + { 0x2161, 0x2171 }, // ROMAN NUMERAL TWO + { 0x2162, 0x2172 }, // ROMAN NUMERAL THREE + { 0x2163, 0x2173 }, // ROMAN NUMERAL FOUR + { 0x2164, 0x2174 }, // ROMAN NUMERAL FIVE + { 0x2165, 0x2175 }, // ROMAN NUMERAL SIX + { 0x2166, 0x2176 }, // ROMAN NUMERAL SEVEN + { 0x2167, 0x2177 }, // ROMAN NUMERAL EIGHT + { 0x2168, 0x2178 }, // ROMAN NUMERAL NINE + { 0x2169, 0x2179 }, // ROMAN NUMERAL TEN + { 0x216A, 0x217A }, // ROMAN NUMERAL ELEVEN + { 0x216B, 0x217B }, // ROMAN NUMERAL TWELVE + { 0x216C, 0x217C }, // ROMAN NUMERAL FIFTY + { 0x216D, 0x217D }, // ROMAN NUMERAL ONE HUNDRED + { 0x216E, 0x217E }, // ROMAN NUMERAL FIVE HUNDRED + { 0x216F, 0x217F }, // ROMAN NUMERAL ONE THOUSAND + { 0x2183, 0x2184 }, // ROMAN NUMERAL REVERSED ONE HUNDRED + { 0x24B6, 0x24D0 }, // CIRCLED LATIN CAPITAL LETTER A + { 0x24B7, 0x24D1 }, // CIRCLED LATIN CAPITAL LETTER B + { 0x24B8, 0x24D2 }, // CIRCLED LATIN CAPITAL LETTER C + { 0x24B9, 0x24D3 }, // CIRCLED LATIN CAPITAL LETTER D + { 0x24BA, 0x24D4 }, // CIRCLED LATIN CAPITAL LETTER E + { 0x24BB, 0x24D5 }, // CIRCLED LATIN CAPITAL LETTER F + { 0x24BC, 0x24D6 }, // CIRCLED LATIN CAPITAL LETTER G + { 0x24BD, 0x24D7 }, // CIRCLED LATIN CAPITAL LETTER H + { 0x24BE, 0x24D8 }, // CIRCLED LATIN CAPITAL LETTER I + { 0x24BF, 0x24D9 }, // CIRCLED LATIN CAPITAL LETTER J + { 0x24C0, 0x24DA }, // CIRCLED LATIN CAPITAL LETTER K + { 0x24C1, 0x24DB }, // CIRCLED LATIN CAPITAL LETTER L + { 0x24C2, 0x24DC }, // CIRCLED LATIN CAPITAL LETTER M + { 0x24C3, 0x24DD }, // CIRCLED LATIN CAPITAL LETTER N + { 0x24C4, 0x24DE }, // CIRCLED LATIN CAPITAL LETTER O + { 0x24C5, 0x24DF }, // CIRCLED LATIN CAPITAL LETTER P + { 0x24C6, 0x24E0 }, // CIRCLED LATIN CAPITAL LETTER Q + { 0x24C7, 0x24E1 }, // CIRCLED LATIN CAPITAL LETTER R + { 0x24C8, 0x24E2 }, // CIRCLED LATIN CAPITAL LETTER S + { 0x24C9, 0x24E3 }, // CIRCLED LATIN CAPITAL LETTER T + { 0x24CA, 0x24E4 }, // CIRCLED LATIN CAPITAL LETTER U + { 0x24CB, 0x24E5 }, // CIRCLED LATIN CAPITAL LETTER V + { 0x24CC, 0x24E6 }, // CIRCLED LATIN CAPITAL LETTER W + { 0x24CD, 0x24E7 }, // CIRCLED LATIN CAPITAL LETTER X + { 0x24CE, 0x24E8 }, // CIRCLED LATIN CAPITAL LETTER Y + { 0x24CF, 0x24E9 }, // CIRCLED LATIN CAPITAL LETTER Z + { 0x2C00, 0x2C30 }, // GLAGOLITIC CAPITAL LETTER AZU + { 0x2C01, 0x2C31 }, // GLAGOLITIC CAPITAL LETTER BUKY + { 0x2C02, 0x2C32 }, // GLAGOLITIC CAPITAL LETTER VEDE + { 0x2C03, 0x2C33 }, // GLAGOLITIC CAPITAL LETTER GLAGOLI + { 0x2C04, 0x2C34 }, // GLAGOLITIC CAPITAL LETTER DOBRO + { 0x2C05, 0x2C35 }, // GLAGOLITIC CAPITAL LETTER YESTU + { 0x2C06, 0x2C36 }, // GLAGOLITIC CAPITAL LETTER ZHIVETE + { 0x2C07, 0x2C37 }, // GLAGOLITIC CAPITAL LETTER DZELO + { 0x2C08, 0x2C38 }, // GLAGOLITIC CAPITAL LETTER ZEMLJA + { 0x2C09, 0x2C39 }, // GLAGOLITIC CAPITAL LETTER IZHE + { 0x2C0A, 0x2C3A }, // GLAGOLITIC CAPITAL LETTER INITIAL IZHE + { 0x2C0B, 0x2C3B }, // GLAGOLITIC CAPITAL LETTER I + { 0x2C0C, 0x2C3C }, // GLAGOLITIC CAPITAL LETTER DJERVI + { 0x2C0D, 0x2C3D }, // GLAGOLITIC CAPITAL LETTER KAKO + { 0x2C0E, 0x2C3E }, // GLAGOLITIC CAPITAL LETTER LJUDIJE + { 0x2C0F, 0x2C3F }, // GLAGOLITIC CAPITAL LETTER MYSLITE + { 0x2C10, 0x2C40 }, // GLAGOLITIC CAPITAL LETTER NASHI + { 0x2C11, 0x2C41 }, // GLAGOLITIC CAPITAL LETTER ONU + { 0x2C12, 0x2C42 }, // GLAGOLITIC CAPITAL LETTER POKOJI + { 0x2C13, 0x2C43 }, // GLAGOLITIC CAPITAL LETTER RITSI + { 0x2C14, 0x2C44 }, // GLAGOLITIC CAPITAL LETTER SLOVO + { 0x2C15, 0x2C45 }, // GLAGOLITIC CAPITAL LETTER TVRIDO + { 0x2C16, 0x2C46 }, // GLAGOLITIC CAPITAL LETTER UKU + { 0x2C17, 0x2C47 }, // GLAGOLITIC CAPITAL LETTER FRITU + { 0x2C18, 0x2C48 }, // GLAGOLITIC CAPITAL LETTER HERU + { 0x2C19, 0x2C49 }, // GLAGOLITIC CAPITAL LETTER OTU + { 0x2C1A, 0x2C4A }, // GLAGOLITIC CAPITAL LETTER PE + { 0x2C1B, 0x2C4B }, // GLAGOLITIC CAPITAL LETTER SHTA + { 0x2C1C, 0x2C4C }, // GLAGOLITIC CAPITAL LETTER TSI + { 0x2C1D, 0x2C4D }, // GLAGOLITIC CAPITAL LETTER CHRIVI + { 0x2C1E, 0x2C4E }, // GLAGOLITIC CAPITAL LETTER SHA + { 0x2C1F, 0x2C4F }, // GLAGOLITIC CAPITAL LETTER YERU + { 0x2C20, 0x2C50 }, // GLAGOLITIC CAPITAL LETTER YERI + { 0x2C21, 0x2C51 }, // GLAGOLITIC CAPITAL LETTER YATI + { 0x2C22, 0x2C52 }, // GLAGOLITIC CAPITAL LETTER SPIDERY HA + { 0x2C23, 0x2C53 }, // GLAGOLITIC CAPITAL LETTER YU + { 0x2C24, 0x2C54 }, // GLAGOLITIC CAPITAL LETTER SMALL YUS + { 0x2C25, 0x2C55 }, // GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL + { 0x2C26, 0x2C56 }, // GLAGOLITIC CAPITAL LETTER YO + { 0x2C27, 0x2C57 }, // GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS + { 0x2C28, 0x2C58 }, // GLAGOLITIC CAPITAL LETTER BIG YUS + { 0x2C29, 0x2C59 }, // GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS + { 0x2C2A, 0x2C5A }, // GLAGOLITIC CAPITAL LETTER FITA + { 0x2C2B, 0x2C5B }, // GLAGOLITIC CAPITAL LETTER IZHITSA + { 0x2C2C, 0x2C5C }, // GLAGOLITIC CAPITAL LETTER SHTAPIC + { 0x2C2D, 0x2C5D }, // GLAGOLITIC CAPITAL LETTER TROKUTASTI A + { 0x2C2E, 0x2C5E }, // GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE + { 0x2C60, 0x2C61 }, // LATIN CAPITAL LETTER L WITH DOUBLE BAR + { 0x2C62, 0x026B }, // LATIN CAPITAL LETTER L WITH MIDDLE TILDE + { 0x2C63, 0x1D7D }, // LATIN CAPITAL LETTER P WITH STROKE + { 0x2C64, 0x027D }, // LATIN CAPITAL LETTER R WITH TAIL + { 0x2C67, 0x2C68 }, // LATIN CAPITAL LETTER H WITH DESCENDER + { 0x2C69, 0x2C6A }, // LATIN CAPITAL LETTER K WITH DESCENDER + { 0x2C6B, 0x2C6C }, // LATIN CAPITAL LETTER Z WITH DESCENDER + { 0x2C6D, 0x0251 }, // LATIN CAPITAL LETTER ALPHA + { 0x2C6E, 0x0271 }, // LATIN CAPITAL LETTER M WITH HOOK + { 0x2C6F, 0x0250 }, // LATIN CAPITAL LETTER TURNED A + { 0x2C70, 0x0252 }, // LATIN CAPITAL LETTER TURNED ALPHA + { 0x2C72, 0x2C73 }, // LATIN CAPITAL LETTER W WITH HOOK + { 0x2C75, 0x2C76 }, // LATIN CAPITAL LETTER HALF H + { 0x2C7E, 0x023F }, // LATIN CAPITAL LETTER S WITH SWASH TAIL + { 0x2C7F, 0x0240 }, // LATIN CAPITAL LETTER Z WITH SWASH TAIL + { 0x2C80, 0x2C81 }, // COPTIC CAPITAL LETTER ALFA + { 0x2C82, 0x2C83 }, // COPTIC CAPITAL LETTER VIDA + { 0x2C84, 0x2C85 }, // COPTIC CAPITAL LETTER GAMMA + { 0x2C86, 0x2C87 }, // COPTIC CAPITAL LETTER DALDA + { 0x2C88, 0x2C89 }, // COPTIC CAPITAL LETTER EIE + { 0x2C8A, 0x2C8B }, // COPTIC CAPITAL LETTER SOU + { 0x2C8C, 0x2C8D }, // COPTIC CAPITAL LETTER ZATA + { 0x2C8E, 0x2C8F }, // COPTIC CAPITAL LETTER HATE + { 0x2C90, 0x2C91 }, // COPTIC CAPITAL LETTER THETHE + { 0x2C92, 0x2C93 }, // COPTIC CAPITAL LETTER IAUDA + { 0x2C94, 0x2C95 }, // COPTIC CAPITAL LETTER KAPA + { 0x2C96, 0x2C97 }, // COPTIC CAPITAL LETTER LAULA + { 0x2C98, 0x2C99 }, // COPTIC CAPITAL LETTER MI + { 0x2C9A, 0x2C9B }, // COPTIC CAPITAL LETTER NI + { 0x2C9C, 0x2C9D }, // COPTIC CAPITAL LETTER KSI + { 0x2C9E, 0x2C9F }, // COPTIC CAPITAL LETTER O + { 0x2CA0, 0x2CA1 }, // COPTIC CAPITAL LETTER PI + { 0x2CA2, 0x2CA3 }, // COPTIC CAPITAL LETTER RO + { 0x2CA4, 0x2CA5 }, // COPTIC CAPITAL LETTER SIMA + { 0x2CA6, 0x2CA7 }, // COPTIC CAPITAL LETTER TAU + { 0x2CA8, 0x2CA9 }, // COPTIC CAPITAL LETTER UA + { 0x2CAA, 0x2CAB }, // COPTIC CAPITAL LETTER FI + { 0x2CAC, 0x2CAD }, // COPTIC CAPITAL LETTER KHI + { 0x2CAE, 0x2CAF }, // COPTIC CAPITAL LETTER PSI + { 0x2CB0, 0x2CB1 }, // COPTIC CAPITAL LETTER OOU + { 0x2CB2, 0x2CB3 }, // COPTIC CAPITAL LETTER DIALECT-P ALEF + { 0x2CB4, 0x2CB5 }, // COPTIC CAPITAL LETTER OLD COPTIC AIN + { 0x2CB6, 0x2CB7 }, // COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE + { 0x2CB8, 0x2CB9 }, // COPTIC CAPITAL LETTER DIALECT-P KAPA + { 0x2CBA, 0x2CBB }, // COPTIC CAPITAL LETTER DIALECT-P NI + { 0x2CBC, 0x2CBD }, // COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI + { 0x2CBE, 0x2CBF }, // COPTIC CAPITAL LETTER OLD COPTIC OOU + { 0x2CC0, 0x2CC1 }, // COPTIC CAPITAL LETTER SAMPI + { 0x2CC2, 0x2CC3 }, // COPTIC CAPITAL LETTER CROSSED SHEI + { 0x2CC4, 0x2CC5 }, // COPTIC CAPITAL LETTER OLD COPTIC SHEI + { 0x2CC6, 0x2CC7 }, // COPTIC CAPITAL LETTER OLD COPTIC ESH + { 0x2CC8, 0x2CC9 }, // COPTIC CAPITAL LETTER AKHMIMIC KHEI + { 0x2CCA, 0x2CCB }, // COPTIC CAPITAL LETTER DIALECT-P HORI + { 0x2CCC, 0x2CCD }, // COPTIC CAPITAL LETTER OLD COPTIC HORI + { 0x2CCE, 0x2CCF }, // COPTIC CAPITAL LETTER OLD COPTIC HA + { 0x2CD0, 0x2CD1 }, // COPTIC CAPITAL LETTER L-SHAPED HA + { 0x2CD2, 0x2CD3 }, // COPTIC CAPITAL LETTER OLD COPTIC HEI + { 0x2CD4, 0x2CD5 }, // COPTIC CAPITAL LETTER OLD COPTIC HAT + { 0x2CD6, 0x2CD7 }, // COPTIC CAPITAL LETTER OLD COPTIC GANGIA + { 0x2CD8, 0x2CD9 }, // COPTIC CAPITAL LETTER OLD COPTIC DJA + { 0x2CDA, 0x2CDB }, // COPTIC CAPITAL LETTER OLD COPTIC SHIMA + { 0x2CDC, 0x2CDD }, // COPTIC CAPITAL LETTER OLD NUBIAN SHIMA + { 0x2CDE, 0x2CDF }, // COPTIC CAPITAL LETTER OLD NUBIAN NGI + { 0x2CE0, 0x2CE1 }, // COPTIC CAPITAL LETTER OLD NUBIAN NYI + { 0x2CE2, 0x2CE3 }, // COPTIC CAPITAL LETTER OLD NUBIAN WAU + { 0x2CEB, 0x2CEC }, // COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI + { 0x2CED, 0x2CEE }, // COPTIC CAPITAL LETTER CRYPTOGRAMMIC GANGIA + { 0xA640, 0xA641 }, // CYRILLIC CAPITAL LETTER ZEMLYA + { 0xA642, 0xA643 }, // CYRILLIC CAPITAL LETTER DZELO + { 0xA644, 0xA645 }, // CYRILLIC CAPITAL LETTER REVERSED DZE + { 0xA646, 0xA647 }, // CYRILLIC CAPITAL LETTER IOTA + { 0xA648, 0xA649 }, // CYRILLIC CAPITAL LETTER DJERV + { 0xA64A, 0xA64B }, // CYRILLIC CAPITAL LETTER MONOGRAPH UK + { 0xA64C, 0xA64D }, // CYRILLIC CAPITAL LETTER BROAD OMEGA + { 0xA64E, 0xA64F }, // CYRILLIC CAPITAL LETTER NEUTRAL YER + { 0xA650, 0xA651 }, // CYRILLIC CAPITAL LETTER YERU WITH BACK YER + { 0xA652, 0xA653 }, // CYRILLIC CAPITAL LETTER IOTIFIED YAT + { 0xA654, 0xA655 }, // CYRILLIC CAPITAL LETTER REVERSED YU + { 0xA656, 0xA657 }, // CYRILLIC CAPITAL LETTER IOTIFIED A + { 0xA658, 0xA659 }, // CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS + { 0xA65A, 0xA65B }, // CYRILLIC CAPITAL LETTER BLENDED YUS + { 0xA65C, 0xA65D }, // CYRILLIC CAPITAL LETTER IOTIFIED CLOSED LITTLE YUS + { 0xA65E, 0xA65F }, // CYRILLIC CAPITAL LETTER YN + { 0xA662, 0xA663 }, // CYRILLIC CAPITAL LETTER SOFT DE + { 0xA664, 0xA665 }, // CYRILLIC CAPITAL LETTER SOFT EL + { 0xA666, 0xA667 }, // CYRILLIC CAPITAL LETTER SOFT EM + { 0xA668, 0xA669 }, // CYRILLIC CAPITAL LETTER MONOCULAR O + { 0xA66A, 0xA66B }, // CYRILLIC CAPITAL LETTER BINOCULAR O + { 0xA66C, 0xA66D }, // CYRILLIC CAPITAL LETTER DOUBLE MONOCULAR O + { 0xA680, 0xA681 }, // CYRILLIC CAPITAL LETTER DWE + { 0xA682, 0xA683 }, // CYRILLIC CAPITAL LETTER DZWE + { 0xA684, 0xA685 }, // CYRILLIC CAPITAL LETTER ZHWE + { 0xA686, 0xA687 }, // CYRILLIC CAPITAL LETTER CCHE + { 0xA688, 0xA689 }, // CYRILLIC CAPITAL LETTER DZZE + { 0xA68A, 0xA68B }, // CYRILLIC CAPITAL LETTER TE WITH MIDDLE HOOK + { 0xA68C, 0xA68D }, // CYRILLIC CAPITAL LETTER TWE + { 0xA68E, 0xA68F }, // CYRILLIC CAPITAL LETTER TSWE + { 0xA690, 0xA691 }, // CYRILLIC CAPITAL LETTER TSSE + { 0xA692, 0xA693 }, // CYRILLIC CAPITAL LETTER TCHE + { 0xA694, 0xA695 }, // CYRILLIC CAPITAL LETTER HWE + { 0xA696, 0xA697 }, // CYRILLIC CAPITAL LETTER SHWE + { 0xA722, 0xA723 }, // LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF + { 0xA724, 0xA725 }, // LATIN CAPITAL LETTER EGYPTOLOGICAL AIN + { 0xA726, 0xA727 }, // LATIN CAPITAL LETTER HENG + { 0xA728, 0xA729 }, // LATIN CAPITAL LETTER TZ + { 0xA72A, 0xA72B }, // LATIN CAPITAL LETTER TRESILLO + { 0xA72C, 0xA72D }, // LATIN CAPITAL LETTER CUATRILLO + { 0xA72E, 0xA72F }, // LATIN CAPITAL LETTER CUATRILLO WITH COMMA + { 0xA732, 0xA733 }, // LATIN CAPITAL LETTER AA + { 0xA734, 0xA735 }, // LATIN CAPITAL LETTER AO + { 0xA736, 0xA737 }, // LATIN CAPITAL LETTER AU + { 0xA738, 0xA739 }, // LATIN CAPITAL LETTER AV + { 0xA73A, 0xA73B }, // LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR + { 0xA73C, 0xA73D }, // LATIN CAPITAL LETTER AY + { 0xA73E, 0xA73F }, // LATIN CAPITAL LETTER REVERSED C WITH DOT + { 0xA740, 0xA741 }, // LATIN CAPITAL LETTER K WITH STROKE + { 0xA742, 0xA743 }, // LATIN CAPITAL LETTER K WITH DIAGONAL STROKE + { 0xA744, 0xA745 }, // LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE + { 0xA746, 0xA747 }, // LATIN CAPITAL LETTER BROKEN L + { 0xA748, 0xA749 }, // LATIN CAPITAL LETTER L WITH HIGH STROKE + { 0xA74A, 0xA74B }, // LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY + { 0xA74C, 0xA74D }, // LATIN CAPITAL LETTER O WITH LOOP + { 0xA74E, 0xA74F }, // LATIN CAPITAL LETTER OO + { 0xA750, 0xA751 }, // LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER + { 0xA752, 0xA753 }, // LATIN CAPITAL LETTER P WITH FLOURISH + { 0xA754, 0xA755 }, // LATIN CAPITAL LETTER P WITH SQUIRREL TAIL + { 0xA756, 0xA757 }, // LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER + { 0xA758, 0xA759 }, // LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE + { 0xA75A, 0xA75B }, // LATIN CAPITAL LETTER R ROTUNDA + { 0xA75C, 0xA75D }, // LATIN CAPITAL LETTER RUM ROTUNDA + { 0xA75E, 0xA75F }, // LATIN CAPITAL LETTER V WITH DIAGONAL STROKE + { 0xA760, 0xA761 }, // LATIN CAPITAL LETTER VY + { 0xA762, 0xA763 }, // LATIN CAPITAL LETTER VISIGOTHIC Z + { 0xA764, 0xA765 }, // LATIN CAPITAL LETTER THORN WITH STROKE + { 0xA766, 0xA767 }, // LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER + { 0xA768, 0xA769 }, // LATIN CAPITAL LETTER VEND + { 0xA76A, 0xA76B }, // LATIN CAPITAL LETTER ET + { 0xA76C, 0xA76D }, // LATIN CAPITAL LETTER IS + { 0xA76E, 0xA76F }, // LATIN CAPITAL LETTER CON + { 0xA779, 0xA77A }, // LATIN CAPITAL LETTER INSULAR D + { 0xA77B, 0xA77C }, // LATIN CAPITAL LETTER INSULAR F + { 0xA77D, 0x1D79 }, // LATIN CAPITAL LETTER INSULAR G + { 0xA77E, 0xA77F }, // LATIN CAPITAL LETTER TURNED INSULAR G + { 0xA780, 0xA781 }, // LATIN CAPITAL LETTER TURNED L + { 0xA782, 0xA783 }, // LATIN CAPITAL LETTER INSULAR R + { 0xA784, 0xA785 }, // LATIN CAPITAL LETTER INSULAR S + { 0xA786, 0xA787 }, // LATIN CAPITAL LETTER INSULAR T + { 0xA78B, 0xA78C }, // LATIN CAPITAL LETTER SALTILLO + { 0xFF21, 0xFF41 }, // FULLWIDTH LATIN CAPITAL LETTER A + { 0xFF22, 0xFF42 }, // FULLWIDTH LATIN CAPITAL LETTER B + { 0xFF23, 0xFF43 }, // FULLWIDTH LATIN CAPITAL LETTER C + { 0xFF24, 0xFF44 }, // FULLWIDTH LATIN CAPITAL LETTER D + { 0xFF25, 0xFF45 }, // FULLWIDTH LATIN CAPITAL LETTER E + { 0xFF26, 0xFF46 }, // FULLWIDTH LATIN CAPITAL LETTER F + { 0xFF27, 0xFF47 }, // FULLWIDTH LATIN CAPITAL LETTER G + { 0xFF28, 0xFF48 }, // FULLWIDTH LATIN CAPITAL LETTER H + { 0xFF29, 0xFF49 }, // FULLWIDTH LATIN CAPITAL LETTER I + { 0xFF2A, 0xFF4A }, // FULLWIDTH LATIN CAPITAL LETTER J + { 0xFF2B, 0xFF4B }, // FULLWIDTH LATIN CAPITAL LETTER K + { 0xFF2C, 0xFF4C }, // FULLWIDTH LATIN CAPITAL LETTER L + { 0xFF2D, 0xFF4D }, // FULLWIDTH LATIN CAPITAL LETTER M + { 0xFF2E, 0xFF4E }, // FULLWIDTH LATIN CAPITAL LETTER N + { 0xFF2F, 0xFF4F }, // FULLWIDTH LATIN CAPITAL LETTER O + { 0xFF30, 0xFF50 }, // FULLWIDTH LATIN CAPITAL LETTER P + { 0xFF31, 0xFF51 }, // FULLWIDTH LATIN CAPITAL LETTER Q + { 0xFF32, 0xFF52 }, // FULLWIDTH LATIN CAPITAL LETTER R + { 0xFF33, 0xFF53 }, // FULLWIDTH LATIN CAPITAL LETTER S + { 0xFF34, 0xFF54 }, // FULLWIDTH LATIN CAPITAL LETTER T + { 0xFF35, 0xFF55 }, // FULLWIDTH LATIN CAPITAL LETTER U + { 0xFF36, 0xFF56 }, // FULLWIDTH LATIN CAPITAL LETTER V + { 0xFF37, 0xFF57 }, // FULLWIDTH LATIN CAPITAL LETTER W + { 0xFF38, 0xFF58 }, // FULLWIDTH LATIN CAPITAL LETTER X + { 0xFF39, 0xFF59 }, // FULLWIDTH LATIN CAPITAL LETTER Y + { 0xFF3A, 0xFF5A } // FULLWIDTH LATIN CAPITAL LETTER Z +}; + +static int compare_pair_capital(const void *a, const void *b) { + return (int)(*(unsigned short *)a) + - (int)((struct LatinCapitalSmallPair*)b)->capital; +} + +unsigned short latin_tolower(unsigned short c) { + struct LatinCapitalSmallPair *p = + (struct LatinCapitalSmallPair *)bsearch(&c, SORTED_CHAR_MAP, + sizeof(SORTED_CHAR_MAP) / sizeof(SORTED_CHAR_MAP[0]), + sizeof(SORTED_CHAR_MAP[0]), + compare_pair_capital); + return p ? p->small : c; +} + +} // namespace latinime diff --git a/native/src/char_utils.h b/native/src/char_utils.h new file mode 100644 index 000000000..921ecb4a5 --- /dev/null +++ b/native/src/char_utils.h @@ -0,0 +1,26 @@ +/* + * 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. + */ + +#ifndef LATINIME_CHAR_UTILS_H +#define LATINIME_CHAR_UTILS_H + +namespace latinime { + +unsigned short latin_tolower(unsigned short c); + +}; // namespace latinime + +#endif // LATINIME_CHAR_UTILS_H diff --git a/native/src/dictionary.cpp b/native/src/dictionary.cpp index 6e6f44182..1a39f585b4 100644 --- a/native/src/dictionary.cpp +++ b/native/src/dictionary.cpp @@ -19,21 +19,18 @@ #include #include #include -#include - -#include - -//#define USE_ASSET_MANAGER - -#ifdef USE_ASSET_MANAGER -#include -#include -#endif +//#define LOG_TAG "dictionary.cpp" +//#include +#define LOGI #include "dictionary.h" #include "basechars.h" +#include "char_utils.h" #define DEBUG_DICT 0 +#define DICTIONARY_VERSION_MIN 200 +#define DICTIONARY_HEADER_SIZE 2 +#define NOT_VALID_WORD -99 namespace latinime { @@ -42,6 +39,7 @@ Dictionary::Dictionary(void *dict, int typedLetterMultiplier, int fullWordMultip mDict = (unsigned char*) dict; mTypedLetterMultiplier = typedLetterMultiplier; mFullWordMultiplier = fullWordMultiplier; + getVersionNumber(); } Dictionary::~Dictionary() @@ -65,7 +63,11 @@ int Dictionary::getSuggestions(int *codes, int codesSize, unsigned short *outWor mNextLettersFrequencies = nextLetters; mNextLettersSize = nextLettersSize; - getWordsRec(0, 0, mInputLength * 3, false, 1, 0, 0); + if (checkIfDictVersionIsLatest()) { + getWordsRec(DICTIONARY_HEADER_SIZE, 0, mInputLength * 3, false, 1, 0, 0); + } else { + getWordsRec(0, 0, mInputLength * 3, false, 1, 0, 0); + } // Get the word count suggWords = 0; @@ -92,6 +94,21 @@ Dictionary::registerNextLetter(unsigned short c) } } +void +Dictionary::getVersionNumber() +{ + mVersion = (mDict[0] & 0xFF); + mBigram = (mDict[1] & 0xFF); + LOGI("IN NATIVE SUGGEST Version: %d Bigram : %d \n", mVersion, mBigram); +} + +// Checks whether it has the latest dictionary or the old dictionary +bool +Dictionary::checkIfDictVersionIsLatest() +{ + return (mVersion >= DICTIONARY_VERSION_MIN) && (mBigram == 1 || mBigram == 0); +} + unsigned short Dictionary::getChar(int *pos) { @@ -119,6 +136,28 @@ Dictionary::getAddress(int *pos) return address; } +int +Dictionary::getFreq(int *pos) +{ + int freq = mDict[(*pos)++] & 0xFF; + + if (checkIfDictVersionIsLatest()) { + // skipping bigram + int bigramExist = (mDict[*pos] & FLAG_BIGRAM_READ); + if (bigramExist > 0) { + int nextBigramExist = 1; + while (nextBigramExist > 0) { + (*pos) += 3; + nextBigramExist = (mDict[(*pos)++] & FLAG_BIGRAM_CONTINUED); + } + } else { + (*pos)++; + } + } + + return freq; +} + int Dictionary::wideStrLen(unsigned short *str) { @@ -168,6 +207,46 @@ Dictionary::addWord(unsigned short *word, int length, int frequency) return false; } +bool +Dictionary::addWordBigram(unsigned short *word, int length, int frequency) +{ + word[length] = 0; + if (DEBUG_DICT) { + char s[length + 1]; + for (int i = 0; i <= length; i++) s[i] = word[i]; + LOGI("Bigram: Found word = %s, freq = %d : \n", s, frequency); + } + + // Find the right insertion point + int insertAt = 0; + while (insertAt < mMaxBigrams) { + if (frequency > mBigramFreq[insertAt] + || (mBigramFreq[insertAt] == frequency + && length < wideStrLen(mBigramChars + insertAt * mMaxWordLength))) { + break; + } + insertAt++; + } + LOGI("Bigram: InsertAt -> %d maxBigrams: %d\n", insertAt, mMaxBigrams); + if (insertAt < mMaxBigrams) { + memmove((char*) mBigramFreq + (insertAt + 1) * sizeof(mBigramFreq[0]), + (char*) mBigramFreq + insertAt * sizeof(mBigramFreq[0]), + (mMaxBigrams - insertAt - 1) * sizeof(mBigramFreq[0])); + mBigramFreq[insertAt] = frequency; + memmove((char*) mBigramChars + (insertAt + 1) * mMaxWordLength * sizeof(short), + (char*) mBigramChars + (insertAt ) * mMaxWordLength * sizeof(short), + (mMaxBigrams - insertAt - 1) * sizeof(short) * mMaxWordLength); + unsigned short *dest = mBigramChars + (insertAt ) * mMaxWordLength; + while (length--) { + *dest++ = *word++; + } + *dest = 0; // NULL terminate + if (DEBUG_DICT) LOGI("Bigram: Added word at %d\n", insertAt); + return true; + } + return false; +} + unsigned short Dictionary::toLowerCase(unsigned short c) { if (c < sizeof(BASE_CHARS) / sizeof(BASE_CHARS[0])) { @@ -176,7 +255,7 @@ Dictionary::toLowerCase(unsigned short c) { if (c >='A' && c <= 'Z') { c |= 32; } else if (c > 127) { - c = u_tolower(c); + c = latin_tolower(c); } return c; } @@ -220,12 +299,17 @@ Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int s } for (int i = 0; i < count; i++) { + // -- at char unsigned short c = getChar(&pos); + // -- at flag/add unsigned short lowerC = toLowerCase(c); bool terminal = getTerminal(&pos); int childrenAddress = getAddress(&pos); + // -- after address or flag int freq = 1; if (terminal) freq = getFreq(&pos); + // -- after add or freq + // If we are only doing completions, no need to look at the typed characters. if (completion) { mWord[depth] = c; @@ -239,7 +323,7 @@ Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int s getWordsRec(childrenAddress, depth + 1, maxDepth, completion, snr, inputIndex, diffs); } - } else if (c == QUOTE && currentChars[0] != QUOTE || mSkipPos == depth) { + } else if ((c == QUOTE && currentChars[0] != QUOTE) || mSkipPos == depth) { // Skip the ' or other letter and continue deeper mWord[depth] = c; if (childrenAddress != 0) { @@ -277,14 +361,208 @@ Dictionary::getWordsRec(int pos, int depth, int maxDepth, bool completion, int s } } -bool -Dictionary::isValidWord(unsigned short *word, int length) +int +Dictionary::getBigramAddress(int *pos, bool advance) { - return isValidWordRec(0, word, 0, length); + int address = 0; + + address += (mDict[*pos] & 0x3F) << 16; + address += (mDict[*pos + 1] & 0xFF) << 8; + address += (mDict[*pos + 2] & 0xFF); + + if (advance) { + *pos += 3; + } + + return address; +} + +int +Dictionary::getBigramFreq(int *pos) +{ + int freq = mDict[(*pos)++] & FLAG_BIGRAM_FREQ; + + return freq; +} + + +int +Dictionary::getBigrams(unsigned short *prevWord, int prevWordLength, int *codes, int codesSize, + unsigned short *bigramChars, int *bigramFreq, int maxWordLength, int maxBigrams, + int maxAlternatives) +{ + mBigramFreq = bigramFreq; + mBigramChars = bigramChars; + mInputCodes = codes; + mInputLength = codesSize; + mMaxWordLength = maxWordLength; + mMaxBigrams = maxBigrams; + mMaxAlternatives = maxAlternatives; + + if (mBigram == 1 && checkIfDictVersionIsLatest()) { + int pos = isValidWordRec(DICTIONARY_HEADER_SIZE, prevWord, 0, prevWordLength); + LOGI("Pos -> %d\n", pos); + if (pos < 0) { + return 0; + } + + int bigramCount = 0; + int bigramExist = (mDict[pos] & FLAG_BIGRAM_READ); + if (bigramExist > 0) { + int nextBigramExist = 1; + while (nextBigramExist > 0 && bigramCount < maxBigrams) { + int bigramAddress = getBigramAddress(&pos, true); + int frequency = (FLAG_BIGRAM_FREQ & mDict[pos]); + // search for all bigrams and store them + searchForTerminalNode(bigramAddress, frequency); + nextBigramExist = (mDict[pos++] & FLAG_BIGRAM_CONTINUED); + bigramCount++; + } + } + + return bigramCount; + } + return 0; +} + +void +Dictionary::searchForTerminalNode(int addressLookingFor, int frequency) +{ + // track word with such address and store it in an array + unsigned short word[mMaxWordLength]; + + int pos; + int followDownBranchAddress = DICTIONARY_HEADER_SIZE; + bool found = false; + char followingChar = ' '; + int depth = -1; + + while(!found) { + bool followDownAddressSearchStop = false; + bool firstAddress = true; + bool haveToSearchAll = true; + + if (depth >= 0) { + word[depth] = (unsigned short) followingChar; + } + pos = followDownBranchAddress; // pos start at count + int count = mDict[pos] & 0xFF; + LOGI("count - %d\n",count); + pos++; + for (int i = 0; i < count; i++) { + // pos at data + pos++; + // pos now at flag + if (!getFirstBitOfByte(&pos)) { // non-terminal + if (!followDownAddressSearchStop) { + int addr = getBigramAddress(&pos, false); + if (addr > addressLookingFor) { + followDownAddressSearchStop = true; + if (firstAddress) { + firstAddress = false; + haveToSearchAll = true; + } else if (!haveToSearchAll) { + break; + } + } else { + followDownBranchAddress = addr; + followingChar = (char)(0xFF & mDict[pos-1]); + if (firstAddress) { + firstAddress = false; + haveToSearchAll = false; + } + } + } + pos += 3; + } else if (getFirstBitOfByte(&pos)) { // terminal + if (addressLookingFor == (pos-1)) { // found !! + depth++; + word[depth] = (0xFF & mDict[pos-1]); + found = true; + break; + } + if (getSecondBitOfByte(&pos)) { // address + freq (4 byte) + if (!followDownAddressSearchStop) { + int addr = getBigramAddress(&pos, false); + if (addr > addressLookingFor) { + followDownAddressSearchStop = true; + if (firstAddress) { + firstAddress = false; + haveToSearchAll = true; + } else if (!haveToSearchAll) { + break; + } + } else { + followDownBranchAddress = addr; + followingChar = (char)(0xFF & mDict[pos-1]); + if (firstAddress) { + firstAddress = false; + haveToSearchAll = true; + } + } + } + pos += 4; + } else { // freq only (2 byte) + pos += 2; + } + + // skipping bigram + int bigramExist = (mDict[pos] & FLAG_BIGRAM_READ); + if (bigramExist > 0) { + int nextBigramExist = 1; + while (nextBigramExist > 0) { + pos += 3; + nextBigramExist = (mDict[pos++] & FLAG_BIGRAM_CONTINUED); + } + } else { + pos++; + } + } + } + depth++; + if (followDownBranchAddress == 0) { + LOGI("ERROR!!! Cannot find bigram!!"); + break; + } + } + if (checkFirstCharacter(word)) { + addWordBigram(word, depth, frequency); + } } bool +Dictionary::checkFirstCharacter(unsigned short *word) +{ + // Checks whether this word starts with same character or neighboring characters of + // what user typed. + + int *inputCodes = mInputCodes; + int maxAlt = mMaxAlternatives; + while (maxAlt > 0) { + if ((unsigned int) *inputCodes == (unsigned int) *word) { + return true; + } + inputCodes++; + maxAlt--; + } + return false; +} + +bool +Dictionary::isValidWord(unsigned short *word, int length) +{ + if (checkIfDictVersionIsLatest()) { + return (isValidWordRec(DICTIONARY_HEADER_SIZE, word, 0, length) != NOT_VALID_WORD); + } else { + return (isValidWordRec(0, word, 0, length) != NOT_VALID_WORD); + } +} + +int Dictionary::isValidWordRec(int pos, unsigned short *word, int offset, int length) { + // returns address of bigram data of that word + // return -99 if not found + int count = getCount(&pos); unsigned short currentChar = (unsigned short) word[offset]; for (int j = 0; j < count; j++) { @@ -294,12 +572,13 @@ Dictionary::isValidWordRec(int pos, unsigned short *word, int offset, int length if (c == currentChar) { if (offset == length - 1) { if (terminal) { - return true; + return (pos+1); } } else { if (childPos != 0) { - if (isValidWordRec(childPos, word, offset + 1, length)) { - return true; + int t = isValidWordRec(childPos, word, offset + 1, length); + if (t > 0) { + return t; } } } @@ -310,7 +589,7 @@ Dictionary::isValidWordRec(int pos, unsigned short *word, int offset, int length // There could be two instances of each alphabet - upper and lower case. So continue // looking ... } - return false; + return NOT_VALID_WORD; } diff --git a/native/src/dictionary.h b/native/src/dictionary.h index 3749f3d88..d13496e01 100644 --- a/native/src/dictionary.h +++ b/native/src/dictionary.h @@ -28,12 +28,20 @@ namespace latinime { // if the word has other endings. #define FLAG_TERMINAL_MASK 0x80 +#define FLAG_BIGRAM_READ 0x80 +#define FLAG_BIGRAM_CHILDEXIST 0x40 +#define FLAG_BIGRAM_CONTINUED 0x80 +#define FLAG_BIGRAM_FREQ 0x7F + class Dictionary { public: Dictionary(void *dict, int typedLetterMultipler, int fullWordMultiplier); int getSuggestions(int *codes, int codesSize, unsigned short *outWords, int *frequencies, int maxWordLength, int maxWords, int maxAlternatives, int skipPos, int *nextLetters, int nextLettersSize); + int getBigrams(unsigned short *word, int length, int *codes, int codesSize, + unsigned short *outWords, int *frequencies, int maxWordLength, int maxBigrams, + int maxAlternatives); bool isValidWord(unsigned short *word, int length); void setAsset(void *asset) { mAsset = asset; } void *getAsset() { return mAsset; } @@ -41,28 +49,41 @@ public: private: + void getVersionNumber(); + bool checkIfDictVersionIsLatest(); int getAddress(int *pos); + int getBigramAddress(int *pos, bool advance); + int getFreq(int *pos); + int getBigramFreq(int *pos); + void searchForTerminalNode(int address, int frequency); + + bool getFirstBitOfByte(int *pos) { return (mDict[*pos] & 0x80) > 0; } + bool getSecondBitOfByte(int *pos) { return (mDict[*pos] & 0x40) > 0; } bool getTerminal(int *pos) { return (mDict[*pos] & FLAG_TERMINAL_MASK) > 0; } - int getFreq(int *pos) { return mDict[(*pos)++] & 0xFF; } int getCount(int *pos) { return mDict[(*pos)++] & 0xFF; } unsigned short getChar(int *pos); int wideStrLen(unsigned short *str); bool sameAsTyped(unsigned short *word, int length); + bool checkFirstCharacter(unsigned short *word); bool addWord(unsigned short *word, int length, int frequency); + bool addWordBigram(unsigned short *word, int length, int frequency); unsigned short toLowerCase(unsigned short c); void getWordsRec(int pos, int depth, int maxDepth, bool completion, int frequency, int inputIndex, int diffs); - bool isValidWordRec(int pos, unsigned short *word, int offset, int length); + int isValidWordRec(int pos, unsigned short *word, int offset, int length); void registerNextLetter(unsigned short c); unsigned char *mDict; void *mAsset; int *mFrequencies; + int *mBigramFreq; int mMaxWords; + int mMaxBigrams; int mMaxWordLength; unsigned short *mOutputChars; + unsigned short *mBigramChars; int *mInputCodes; int mInputLength; int mMaxAlternatives; @@ -74,6 +95,8 @@ private: int mTypedLetterMultiplier; int *mNextLettersFrequencies; int mNextLettersSize; + int mVersion; + int mBigram; }; // ---------------------------------------------------------------------------- diff --git a/tests/Android.mk b/tests/Android.mk new file mode 100644 index 000000000..fba7a8d74 --- /dev/null +++ b/tests/Android.mk @@ -0,0 +1,17 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +# We only want this apk build for tests. +LOCAL_MODULE_TAGS := tests +LOCAL_CERTIFICATE := shared + +LOCAL_JAVA_LIBRARIES := android.test.runner + +# Include all test java files. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := LatinIMETests + +LOCAL_INSTRUMENTATION_FOR := LatinIME + +include $(BUILD_PACKAGE) diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml new file mode 100644 index 000000000..210e81489 --- /dev/null +++ b/tests/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/data/bigramlist.xml b/tests/data/bigramlist.xml new file mode 100644 index 000000000..dd3f2916e --- /dev/null +++ b/tests/data/bigramlist.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/wordlist.xml b/tests/data/wordlist.xml new file mode 100644 index 000000000..b870eb2a3 --- /dev/null +++ b/tests/data/wordlist.xml @@ -0,0 +1,244 @@ + + the + and + of + to + in + that + for + with + on + it + this + you + is + was + by + or + from + but + be + Sunday + are + he + so + not + have + as + all + his + my + if + which + they + at + it's + an + your + will + about + I'm + there + had + has + when + no + were + what + more + out + just + their + up + would + here + can + who + her + me + now + our + do + some + been + two + like + them + new + time + we + she + one + over + may + any + him + calling + other + how + see + because + then + right + into + well + very + said + people + these + than + only + back + first + dot + after + where + please + could + its + before + us + again + home + also + that's + think + three + good + get + know + thank + should + going + down + last + today + those + go + through + such + don't + did + most + day + man + number + work + too + show + made + even + being + make + give + off + com + much + great + take + call + way + four + say + information + under + page + many + little + thanks + okay + five + we're + between + use + come + years + office + house + search + free + next + without + still + around + I've + business + part + every + bye + upon + you're + state + life + year + thing + since + things + something + long + got + while + I'll + help + service + really + must + does + name + both + six + want + same + each + yet + let + view + place + another + company + talk + might + am + though + find + details + look + world + old + called + case + system + news + used + contact + never + seven + city + until + during + set + why + point + twenty + high + love + services + niño + María + car + hmmm + hon + tty + ttyl + txt + ur + wah + whatcha + woah + ya + yea + yeh + yessir + yikes + yrs + diff --git a/tests/res/raw/test.dict b/tests/res/raw/test.dict new file mode 100644 index 0000000000000000000000000000000000000000..6a5d6d794f4a1f5c21f25ff97bfd89fe5a0f80f5 GIT binary patch literal 2829 zcmXYzdyG}p6~^~I?{iR^*d}UQYeO|tt3_<9Z7rk=+hQvv(o{*51R3U@nYr9M_mcaV zVVcmM65CWv)%b{kno?5Q6q;Jw;W@+O$U_DY5D1Tok5NI?f(nX_)IfjhPX9SMd+)Wr zwZ65!z0Mi7pKXXKwTpeMD$ciy;tX_(dr6JW0lONzzx6WLql9!D*6w9CnLYRL$+J zXHB2jX+^OqVXF#?4oHGv$H7XR86d36+15I&kmTHm_ar-_hdUvCEIH1 zVz${d+hc4k+hus$Vw^^94!>zCxf$7FY@3*4ww`S^wv?Nbn+)&kjZ@EFH5D=EoDwHp zxixF_V2yE7v11ALX^?PfioL>iiM`5pXB+Vndp%ndINWG-|7P}A8>b-ld(~{nSXlhf z_N!@?{J@%FD`IC_n9XK?z}OzKb8&ypI3=<3%VHO>8YDSzY=3SaFvgZM9t|FGCAWk7 z;B1f8vqJ{!4;tBiGe;cTDbDA*#Q91Oer_TA$n=Q8huBJ*zaG z8!ZC)hOK0|i8^y<);a3!G)7D z9a_!;it{Xu%XR}WOdG{{gizZJM4TnMT*`7=EAk3#*BkO*WjtaIpEQl!xiDhtMD9c( z4q^9Gp2LvZ+9Gw#*2Ed1^v(%Q{T-$Ao@Vl)QvMhcD56c=unrq!u+O+vaj&e2+g22J zl2gl0z{wp)3WIc3+zF|;-?BZ~i&?Ot*&^a%d`GXicU8o_$JWKYlNwT~bTrk~#s{g{ zF7D!PasQs??th=7A>QXn*8O=|+{bNq?*3{-({&fo1NUM4*Qylu8c&(^6B_K5-J{wV z1>{1xM*uxh&30ljV4Mzd_W*YQT^`WrYm8ILo<~9M2rCk5igz8WueXag*A|8n8t8Zy_u=M_UC_ZdQz6nGxMYmL{Qd%rQJfk@T6F5cs|mU|CFy^cPpq_>z( zdB3OjT#Ax-e?bl2-#BHd55GV6UQo?>htk|Tf0VJ|-(3~|z7{@``j?`H~&i86#ol2_+J9Dr6~WaFz~;| z61`N!pJ==E_V zCGa#^6!8+a*}hlpt?Nu&XG+$$kmW zV;o##(>%DI`^musAw9fzsbS|5yn@CBR>Q3~d5Lql~*_$uuRkF%suZ4$og_Aug|O9~`^p)8S$Z)@CO z&6oUAN)la$^={+M%cC21nR*`Gk~4v#+gcZ~D$z|eL+R$xBpO$MFyFywYD1#C;Tuhd zFJrnW(Y0Kf{&7-{Slvl8PQhQhWh)(39ON z9YWDFJ$W>!lQ23!bEDOq=Bm1NW}ZY_Eis1Gbq_^>D7hTby_YP1Ca*zFM0=llY2?#8jD)Dl#bGIb%FG2Wqr^HV{l8M9WK}C6l zCG~5yr7rQ(R&W=nf8e}^KdT`i$)q$|9%Tk)3K;JN`DJ<@AEf8;QTDkZM15UhPwL|o zsq1Bx_o$B+NgZi5-$m=Z$>$^_x|bq=OY~S{Hc50@3A+gCg*uBKzdusL?U&0~2 fgvmQlMY!bnp>54$EH2oZjB)L0858kETxtFXLKju= literal 0 HcmV?d00001 diff --git a/tests/res/raw/testtext.txt b/tests/res/raw/testtext.txt new file mode 100644 index 000000000..eca20c05f --- /dev/null +++ b/tests/res/raw/testtext.txt @@ -0,0 +1,24 @@ +This text is used as test text for measuring performance of dictionary prediction. Any text can be put into this file to test the performance (total keystroke savings). +When you think about “information,” what probably comes to mind are streams of words and numbers. Google’s pretty good at organizing these types of information, but consider all the things you can’t express with words: what does it look like in the middle of a sandstorm? What are some great examples of Art Nouveau architecture? Should I consider wedding cupcakes instead of a traditional cake? +This is why we built Google Images in 2001. We realized that for many searches, the best answer wasn’t text—it was an image or a set of images. The service has grown quite a bit since then. In 2001, we indexed around 250 million images. By 2005, we had indexed over 1 billion. And today, we have an index of over 10 billion images. +It’s not just about quantity, though. Over the past decade we’ve been baking deep computer science into Google Images to make it even faster and easier for you to find precisely the right images. We not only find images for pretty much anything you type in; we can also instantly pull out images of clip art, line drawings, faces and even colors. +There’s even more sophisticated computer vision technology powering our “Similar images” tool. For example, did you know there are nine subspecies of leopards, each with a distinct pattern of spots? Google Images can recognize the difference, returning just leopards of a particular subspecies. It can tell you the name of the subspecies in a particular image—even if that image isn’t labeled—because other similar leopard images on the web are labeled with that subspecies’s name. +And our “Similar colors” refinement doesn’t just return images based on the overall color of an image. If it did, lots of images would simply be classified as “white.” If you’re looking for [tulips] and you refine results to “white,” you really want images in which the tulips themselves are white—not the surrounding image. It takes some heavy-duty algorithmic wizardry and processing power for a search engine to understand what the items of interest are in all the images out there. +Those are just a few of the technologies we’ve built to make Google Images more useful. Meanwhile, the quantity and variety of images on the web has ballooned since 2001, and images have become one of the most popular types of content people search for. So over the next few days we’re rolling out an update to Google Images to match the scope and beauty of this fast-growing visual web, and to bring to the surface some of the powerful technology behind Images. +Here’s what’s new in this refreshed design of Google Images: +Dense tiled layout designed to make it easy to look at lots of images at once. We want to get the app out of the way so you can find what you’re really looking for. +Instant scrolling between pages, without letting you get lost in the images. You can now get up to 1,000 images, all in one scrolling page. And we’ll show small, unobtrusive page numbers so you don’t lose track of where you are. +Larger thumbnail previews on the results page, designed for modern browsers and high-res screens. +A hover pane that appears when you mouse over a given thumbnail image, giving you a larger preview, more info about the image and other image-specific features such as “Similar images.” +Once you click on an image, you’re taken to a new landing page that displays a large image in context, with the website it’s hosted on visible right behind it. Click anywhere outside the image, and you’re right in the original page where you can learn more about the source and context. +Optimized keyboard navigation for faster scrolling through many pages, taking advantage of standard web keyboard shortcuts such as Page Up / Page Down. It’s all about getting you to the info you need quickly, so you can get on with actually building that treehouse or buying those flowers. +Apple's not really ready to say it's sorry about the iPhone 4 antenna design, but it is willing to give all you darn squeaky wheels free cases for your trouble. Since Apple can't build its own Bumpers fast enough, it will give you a few options and let you decide, then send it your way for free as long as you purchased the phone before September 30th. Not good enough for you? Well, if you already bought a bumper from Apple you'll get a refund, and you can also return your phone for a full refund within 30 days as long as it's unharmed. +This solution comes at the end of 22 days of Apple engineers "working their butts off," according to Steve, with "physics" ultimately being pinned as the main culprit. Apple claims you can replicate the left-handed "death grip" bar-dropping problem on the BlackBerry Bold 9700, HTC Droid Eris, and Samsung Omnia II, and that "phones aren't perfect." Steve also claims that only 0.55% of people who bought the iPhone 4 have called into AppleCare to complain about the antenna, and the phone has a 1.7% return rate at AT&T, compared to 6% with the 3GS, though he would cop to a slight increase in dropped calls over the iPhone 3GS. For this Steve has what he confesses to be a pet theory: that 3GS users were using the case they had from the 3G, and therefore weren't met with the horrible reality of a naked, call dropping handset. Hence the free case solution, which will probably satisfy some, infuriate others, and never even blip onto the radar of many of the massive horde of consumers that's devoured this product in unprecedented numbers. +Update: Our own Richard Lai just waltzed down to the Regent Street Apple Store in London with his iPhone Bumper receipt in hand. A few minutes later he left with cold, hard cash, and kept the Bumper to boot. Seems as if the refund effort is a go, at least over in the UK. +Update 2: We've heard from several tipsters saying Apple no longer does Bumper refunds at its stores; customers will now have to make an online claim instead. Looks like we got super lucky. +If you have ever received an instant message, text message, or any text-based chat message that seemed to be written in a foreign language, this Webopedia Quick Reference will help you decipher the text chat lingo by providing the definitions to more than 1,300 chat, text message, and Twitter abbreviations. +With the popularity and rise in real-time text-based communications, such as Facebook, Twitter, instant messaging, e-mail, Internet and online gaming services, chat rooms, discussion boards and mobile phone text messaging (SMS), came the emergence of a new language tailored to the immediacy and compactness of these new communication media. +While it does seem incredible that there are so many chat abbreviations, remember that different chat abbreviations are used by different groups of people when communicating online. Some of the following chat abbreviations may be familiar to you, while others may be foreign because they are used by a group of people with different online interests and hobbies than your own. For example, people playing online games are likely to use chat abbreviations that are different than those used by someone running a financial blog updating their Twitter status. +Twitter is a free microblog, or social messaging tool that lets people stay connected through brief text message updates up to 140 characters in length. Twitter is based on you answering the question "What are you doing?" You then post thoughts, observations and goings-on during the day in answer to that question. Your update is posted on your Twitter profile page through SMS text messaging, the Twitter Web site, instant messaging, RSS, e-mail or through other social applications and sites, such as Facebook. +As with any new social medium, there is an entire vocabulary that users of the Twitter service adopt. Many of the new lingo Twitter-based terms and phrases are used to describe the collection of people who use the service, while other terms are used in reference to describe specific functions and features of the service itself. Also, there are a number of "chat terms," which are basically shorthand abbreviations that users often include in their tweets. Lastly, our guide also provides descriptions to a number of Twitter tools and applications that you can use to enhance your Twitter experience. +Here are definitions to more than 100 Twitter-related abbreviations, words, phrases, and tools that are associated with the Twitter microblogging service. If you know of a Twitter slang term or application name that is not included in our Twitter Dictionary, please let us know. diff --git a/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java b/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java new file mode 100644 index 000000000..234559bb7 --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/ImeLoggerTests.java @@ -0,0 +1,59 @@ +package com.android.inputmethod.latin; + +import android.test.ServiceTestCase; + +public class ImeLoggerTests extends ServiceTestCase { + + private static final String WORD_SEPARATORS + = ".\u0009\u0020,;:!?\n()[]*&@{}<>;_+=|\\u0022"; + + public ImeLoggerTests() { + super(LatinIME.class); + } + static LatinImeLogger sLogger; + @Override + protected void setUp() { + try { + super.setUp(); + } catch (Exception e) { + e.printStackTrace(); + } + setupService(); + // startService(null); // can't be started because VoiceInput can't be found. + final LatinIME context = getService(); + context.mWordSeparators = WORD_SEPARATORS; + LatinImeLogger.init(context); + sLogger = LatinImeLogger.sLatinImeLogger; + } + /*********************** Tests *********************/ + public void testRingBuffer() { + for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) { + LatinImeLogger.logOnDelete(); + } + assertEquals("", sLogger.mRingCharBuffer.getLastString()); + LatinImeLogger.logOnInputChar('t'); + LatinImeLogger.logOnInputChar('g'); + LatinImeLogger.logOnInputChar('i'); + LatinImeLogger.logOnInputChar('s'); + LatinImeLogger.logOnInputChar(' '); + LatinImeLogger.logOnAutoSuggestion("tgis", "this"); + LatinImeLogger.logOnInputChar(' '); + LatinImeLogger.logOnDelete(); + assertEquals("", sLogger.mRingCharBuffer.getLastString()); + LatinImeLogger.logOnDelete(); + assertEquals("tgis", sLogger.mRingCharBuffer.getLastString()); + assertEquals("tgis", LatinImeLogger.sLastAutoSuggestBefore); + LatinImeLogger.logOnAutoSuggestionCanceled(); + assertEquals("", LatinImeLogger.sLastAutoSuggestBefore); + LatinImeLogger.logOnDelete(); + assertEquals("tgi", sLogger.mRingCharBuffer.getLastString()); + for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) { + LatinImeLogger.logOnDelete(); + } + assertEquals("", sLogger.mRingCharBuffer.getLastString()); + for (int i = 0; i < sLogger.mRingCharBuffer.BUFSIZE * 2; ++i) { + LatinImeLogger.logOnInputChar('a'); + } + assertEquals(sLogger.mRingCharBuffer.BUFSIZE, sLogger.mRingCharBuffer.length); + } +} diff --git a/tests/src/com/android/inputmethod/latin/SuggestHelper.java b/tests/src/com/android/inputmethod/latin/SuggestHelper.java new file mode 100644 index 000000000..759bfa18a --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/SuggestHelper.java @@ -0,0 +1,268 @@ +/* + * 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.text.TextUtils; +import android.util.Log; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.UserBigramDictionary; +import com.android.inputmethod.latin.WordComposer; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.Channels; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +public class SuggestHelper { + private Suggest mSuggest; + private UserBigramDictionary mUserBigram; + private final String TAG; + + /** Uses main dictionary only **/ + public SuggestHelper(String tag, Context context, int[] resId) { + TAG = tag; + InputStream[] is = null; + try { + // merging separated dictionary into one if dictionary is separated + int total = 0; + is = new InputStream[resId.length]; + for (int i = 0; i < resId.length; i++) { + is[i] = context.getResources().openRawResource(resId[i]); + total += is[i].available(); + } + + ByteBuffer byteBuffer = + ByteBuffer.allocateDirect(total).order(ByteOrder.nativeOrder()); + int got = 0; + for (int i = 0; i < resId.length; i++) { + got += Channels.newChannel(is[i]).read(byteBuffer); + } + if (got != total) { + Log.w(TAG, "Read " + got + " bytes, expected " + total); + } else { + mSuggest = new Suggest(context, byteBuffer); + Log.i(TAG, "Created mSuggest " + total + " bytes"); + } + } catch (IOException e) { + Log.w(TAG, "No available memory for binary dictionary"); + } finally { + try { + if (is != null) { + for (int i = 0; i < is.length; i++) { + is[i].close(); + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to close input stream"); + } + } + mSuggest.setAutoTextEnabled(false); + mSuggest.setCorrectionMode(Suggest.CORRECTION_FULL_BIGRAM); + } + + /** Uses both main dictionary and user-bigram dictionary **/ + public SuggestHelper(String tag, Context context, int[] resId, int userBigramMax, + int userBigramDelete) { + this(tag, context, resId); + mUserBigram = new UserBigramDictionary(context, null, Locale.US.toString(), + Suggest.DIC_USER); + mUserBigram.setDatabaseMax(userBigramMax); + mUserBigram.setDatabaseDelete(userBigramDelete); + mSuggest.setUserBigramDictionary(mUserBigram); + } + + void changeUserBigramLocale(Context context, Locale locale) { + if (mUserBigram != null) { + flushUserBigrams(); + mUserBigram.close(); + mUserBigram = new UserBigramDictionary(context, null, locale.toString(), + Suggest.DIC_USER); + mSuggest.setUserBigramDictionary(mUserBigram); + } + } + + private WordComposer createWordComposer(CharSequence s) { + WordComposer word = new WordComposer(); + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + int[] codes; + // If it's not a lowercase letter, don't find adjacent letters + if (c < 'a' || c > 'z') { + codes = new int[] { c }; + } else { + codes = adjacents[c - 'a']; + } + word.add(c, codes); + } + return word; + } + + private void showList(String title, List suggestions) { + Log.i(TAG, title); + for (int i = 0; i < suggestions.size(); i++) { + Log.i(title, suggestions.get(i) + ", "); + } + } + + private boolean isDefaultSuggestion(List suggestions, CharSequence word) { + // Check if either the word is what you typed or the first alternative + return suggestions.size() > 0 && + (/*TextUtils.equals(suggestions.get(0), word) || */ + (suggestions.size() > 1 && TextUtils.equals(suggestions.get(1), word))); + } + + boolean isDefaultSuggestion(CharSequence typed, CharSequence expected) { + WordComposer word = createWordComposer(typed); + List suggestions = mSuggest.getSuggestions(null, word, false, null); + return isDefaultSuggestion(suggestions, expected); + } + + boolean isDefaultCorrection(CharSequence typed, CharSequence expected) { + WordComposer word = createWordComposer(typed); + List suggestions = mSuggest.getSuggestions(null, word, false, null); + return isDefaultSuggestion(suggestions, expected) && mSuggest.hasMinimalCorrection(); + } + + boolean isASuggestion(CharSequence typed, CharSequence expected) { + WordComposer word = createWordComposer(typed); + List suggestions = mSuggest.getSuggestions(null, word, false, null); + for (int i = 1; i < suggestions.size(); i++) { + if (TextUtils.equals(suggestions.get(i), expected)) return true; + } + return false; + } + + private void getBigramSuggestions(CharSequence previous, CharSequence typed) { + if (!TextUtils.isEmpty(previous) && (typed.length() > 1)) { + WordComposer firstChar = createWordComposer(Character.toString(typed.charAt(0))); + mSuggest.getSuggestions(null, firstChar, false, previous); + } + } + + boolean isDefaultNextSuggestion(CharSequence previous, CharSequence typed, + CharSequence expected) { + WordComposer word = createWordComposer(typed); + getBigramSuggestions(previous, typed); + List suggestions = mSuggest.getSuggestions(null, word, false, previous); + return isDefaultSuggestion(suggestions, expected); + } + + boolean isDefaultNextCorrection(CharSequence previous, CharSequence typed, + CharSequence expected) { + WordComposer word = createWordComposer(typed); + getBigramSuggestions(previous, typed); + List suggestions = mSuggest.getSuggestions(null, word, false, previous); + return isDefaultSuggestion(suggestions, expected) && mSuggest.hasMinimalCorrection(); + } + + boolean isASuggestion(CharSequence previous, CharSequence typed, + CharSequence expected) { + WordComposer word = createWordComposer(typed); + getBigramSuggestions(previous, typed); + List suggestions = mSuggest.getSuggestions(null, word, false, previous); + for (int i = 1; i < suggestions.size(); i++) { + if (TextUtils.equals(suggestions.get(i), expected)) return true; + } + return false; + } + + boolean isValid(CharSequence typed) { + return mSuggest.isValidWord(typed); + } + + boolean isUserBigramSuggestion(CharSequence previous, char typed, + CharSequence expected) { + WordComposer word = createWordComposer(Character.toString(typed)); + + if (mUserBigram == null) return false; + + flushUserBigrams(); + if (!TextUtils.isEmpty(previous) && !TextUtils.isEmpty(Character.toString(typed))) { + WordComposer firstChar = createWordComposer(Character.toString(typed)); + mSuggest.getSuggestions(null, firstChar, false, previous); + boolean reloading = mUserBigram.reloadDictionaryIfRequired(); + if (reloading) mUserBigram.waitForDictionaryLoading(); + mUserBigram.getBigrams(firstChar, previous, mSuggest, null); + } + + List suggestions = mSuggest.mBigramSuggestions; + for (int i = 0; i < suggestions.size(); i++) { + if (TextUtils.equals(suggestions.get(i), expected)) return true; + } + + return false; + } + + void addToUserBigram(String sentence) { + StringTokenizer st = new StringTokenizer(sentence); + String previous = null; + while (st.hasMoreTokens()) { + String current = st.nextToken(); + if (previous != null) { + addToUserBigram(new String[] {previous, current}); + } + previous = current; + } + } + + void addToUserBigram(String[] pair) { + if (mUserBigram != null && pair.length == 2) { + mUserBigram.addBigrams(pair[0], pair[1]); + } + } + + void flushUserBigrams() { + if (mUserBigram != null) { + mUserBigram.flushPendingWrites(); + mUserBigram.waitUntilUpdateDBDone(); + } + } + + final int[][] adjacents = { + {'a','s','w','q',-1}, + {'b','h','v','n','g','j',-1}, + {'c','v','f','x','g',}, + {'d','f','r','e','s','x',-1}, + {'e','w','r','s','d',-1}, + {'f','g','d','c','t','r',-1}, + {'g','h','f','y','t','v',-1}, + {'h','j','u','g','b','y',-1}, + {'i','o','u','k',-1}, + {'j','k','i','h','u','n',-1}, + {'k','l','o','j','i','m',-1}, + {'l','k','o','p',-1}, + {'m','k','n','l',-1}, + {'n','m','j','k','b',-1}, + {'o','p','i','l',-1}, + {'p','o',-1}, + {'q','w',-1}, + {'r','t','e','f',-1}, + {'s','d','e','w','a','z',-1}, + {'t','y','r',-1}, + {'u','y','i','h','j',-1}, + {'v','b','g','c','h',-1}, + {'w','e','q',-1}, + {'x','c','d','z','f',-1}, + {'y','u','t','h','g',-1}, + {'z','s','x','a','d',-1}, + }; +} diff --git a/tests/src/com/android/inputmethod/latin/SuggestPerformanceTests.java b/tests/src/com/android/inputmethod/latin/SuggestPerformanceTests.java new file mode 100644 index 000000000..7eb66d502 --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/SuggestPerformanceTests.java @@ -0,0 +1,126 @@ +/* + * 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.test.AndroidTestCase; +import android.util.Log; +import com.android.inputmethod.latin.tests.R; +import java.io.InputStreamReader; +import java.io.InputStream; +import java.io.BufferedReader; +import java.util.StringTokenizer; + +public class SuggestPerformanceTests extends AndroidTestCase { + private static final String TAG = "SuggestPerformanceTests"; + + private String mTestText; + private SuggestHelper sh; + + @Override + protected void setUp() { + // TODO Figure out a way to directly using the dictionary rather than copying it over + + // For testing with real dictionary, TEMPORARILY COPY main dictionary into test directory. + // DO NOT SUBMIT real dictionary under test directory. + //int[] resId = new int[] { R.raw.main0, R.raw.main1, R.raw.main2 }; + + int[] resId = new int[] { R.raw.test }; + + sh = new SuggestHelper(TAG, getTestContext(), resId); + loadString(); + } + + private void loadString() { + try { + InputStream is = getTestContext().getResources().openRawResource(R.raw.testtext); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = reader.readLine(); + while (line != null) { + sb.append(line + " "); + line = reader.readLine(); + } + mTestText = sb.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /************************** Helper functions ************************/ + private int lookForSuggestion(String prevWord, String currentWord) { + for (int i = 1; i < currentWord.length(); i++) { + if (i == 1) { + if (sh.isDefaultNextSuggestion(prevWord, currentWord.substring(0, i), + currentWord)) { + return i; + } + } else { + if (sh.isDefaultNextCorrection(prevWord, currentWord.substring(0, i), + currentWord)) { + return i; + } + } + } + return currentWord.length(); + } + + private double runText(boolean withBigrams) { + StringTokenizer st = new StringTokenizer(mTestText); + String prevWord = null; + int typeCount = 0; + int characterCount = 0; // without space + int wordCount = 0; + while (st.hasMoreTokens()) { + String currentWord = st.nextToken(); + boolean endCheck = false; + if (currentWord.matches("[\\w]*[\\.|?|!|*|@|&|/|:|;]")) { + currentWord = currentWord.substring(0, currentWord.length() - 1); + endCheck = true; + } + if (withBigrams && prevWord != null) { + typeCount += lookForSuggestion(prevWord, currentWord); + } else { + typeCount += lookForSuggestion(null, currentWord); + } + characterCount += currentWord.length(); + if (!endCheck) prevWord = currentWord; + wordCount++; + } + + double result = (double) (characterCount - typeCount) / characterCount * 100; + if (withBigrams) { + Log.i(TAG, "with bigrams -> " + result + " % saved!"); + } else { + Log.i(TAG, "without bigrams -> " + result + " % saved!"); + } + Log.i(TAG, "\ttotal number of words: " + wordCount); + Log.i(TAG, "\ttotal number of characters: " + mTestText.length()); + Log.i(TAG, "\ttotal number of characters without space: " + characterCount); + Log.i(TAG, "\ttotal number of characters typed: " + typeCount); + return result; + } + + + /************************** Performance Tests ************************/ + /** + * Compare the Suggest with and without bigram + * Check the log for detail + */ + public void testSuggestPerformance() { + assertTrue(runText(false) <= runText(true)); + } +} diff --git a/tests/src/com/android/inputmethod/latin/SuggestTests.java b/tests/src/com/android/inputmethod/latin/SuggestTests.java new file mode 100644 index 000000000..8463ed316 --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/SuggestTests.java @@ -0,0 +1,172 @@ +/* + * 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.test.AndroidTestCase; +import com.android.inputmethod.latin.tests.R; + +public class SuggestTests extends AndroidTestCase { + private static final String TAG = "SuggestTests"; + + private SuggestHelper sh; + + @Override + protected void setUp() { + int[] resId = new int[] { R.raw.test }; + sh = new SuggestHelper(TAG, getTestContext(), resId); + } + + /************************** Tests ************************/ + + /** + * Tests for simple completions of one character. + */ + public void testCompletion1char() { + assertTrue(sh.isDefaultSuggestion("peopl", "people")); + assertTrue(sh.isDefaultSuggestion("abou", "about")); + assertTrue(sh.isDefaultSuggestion("thei", "their")); + } + + /** + * Tests for simple completions of two characters. + */ + public void testCompletion2char() { + assertTrue(sh.isDefaultSuggestion("peop", "people")); + assertTrue(sh.isDefaultSuggestion("calli", "calling")); + assertTrue(sh.isDefaultSuggestion("busine", "business")); + } + + /** + * Tests for proximity errors. + */ + public void testProximityPositive() { + assertTrue(sh.isDefaultSuggestion("peiple", "people")); + assertTrue(sh.isDefaultSuggestion("peoole", "people")); + assertTrue(sh.isDefaultSuggestion("pwpple", "people")); + } + + /** + * Tests for proximity errors - negative, when the error key is not near. + */ + public void testProximityNegative() { + assertFalse(sh.isDefaultSuggestion("arout", "about")); + assertFalse(sh.isDefaultSuggestion("ire", "are")); + } + + /** + * Tests for checking if apostrophes are added automatically. + */ + public void testApostropheInsertion() { + assertTrue(sh.isDefaultSuggestion("im", "I'm")); + assertTrue(sh.isDefaultSuggestion("dont", "don't")); + } + + /** + * Test to make sure apostrophed word is not suggested for an apostrophed word. + */ + public void testApostrophe() { + assertFalse(sh.isDefaultSuggestion("don't", "don't")); + } + + /** + * Tests for suggestion of capitalized version of a word. + */ + public void testCapitalization() { + assertTrue(sh.isDefaultSuggestion("i'm", "I'm")); + assertTrue(sh.isDefaultSuggestion("sunday", "Sunday")); + assertTrue(sh.isDefaultSuggestion("sundat", "Sunday")); + } + + /** + * Tests to see if more than one completion is provided for certain prefixes. + */ + public void testMultipleCompletions() { + assertTrue(sh.isASuggestion("com", "come")); + assertTrue(sh.isASuggestion("com", "company")); + assertTrue(sh.isASuggestion("th", "the")); + assertTrue(sh.isASuggestion("th", "that")); + assertTrue(sh.isASuggestion("th", "this")); + assertTrue(sh.isASuggestion("th", "they")); + } + + /** + * Does the suggestion engine recognize zero frequency words as valid words. + */ + public void testZeroFrequencyAccepted() { + assertTrue(sh.isValid("yikes")); + assertFalse(sh.isValid("yike")); + } + + /** + * Tests to make sure that zero frequency words are not suggested as completions. + */ + public void testZeroFrequencySuggestionsNegative() { + assertFalse(sh.isASuggestion("yike", "yikes")); + assertFalse(sh.isASuggestion("what", "whatcha")); + } + + /** + * Tests to ensure that words with large edit distances are not suggested, in some cases + * and not considered corrections, in some cases. + */ + public void testTooLargeEditDistance() { + assertFalse(sh.isASuggestion("sniyr", "about")); + assertFalse(sh.isDefaultCorrection("rjw", "the")); + } + + /** + * Make sure sh.isValid is case-sensitive. + */ + public void testValidityCaseSensitivity() { + assertTrue(sh.isValid("Sunday")); + assertFalse(sh.isValid("sunday")); + } + + /** + * Are accented forms of words suggested as corrections? + */ + public void testAccents() { + // nio + assertTrue(sh.isDefaultCorrection("nino", "ni\u00F1o")); + // nio + assertTrue(sh.isDefaultCorrection("nimo", "ni\u00F1o")); + // Mara + assertTrue(sh.isDefaultCorrection("maria", "Mar\u00EDa")); + } + + /** + * Make sure bigrams are showing when first character is typed + * and don't show any when there aren't any + */ + public void testBigramsAtFirstChar() { + assertTrue(sh.isDefaultNextSuggestion("about", "p", "part")); + assertTrue(sh.isDefaultNextSuggestion("I'm", "a", "about")); + assertTrue(sh.isDefaultNextSuggestion("about", "b", "business")); + assertTrue(sh.isASuggestion("about", "b", "being")); + assertFalse(sh.isDefaultNextSuggestion("about", "p", "business")); + } + + /** + * Make sure bigrams score affects the original score + */ + public void testBigramsScoreEffect() { + assertTrue(sh.isDefaultCorrection("pa", "page")); + assertTrue(sh.isDefaultNextCorrection("about", "pa", "part")); + assertTrue(sh.isDefaultCorrection("sa", "said")); + assertTrue(sh.isDefaultNextCorrection("from", "sa", "same")); + } +} diff --git a/tests/src/com/android/inputmethod/latin/UserBigramTests.java b/tests/src/com/android/inputmethod/latin/UserBigramTests.java new file mode 100644 index 000000000..cbf7bd8e1 --- /dev/null +++ b/tests/src/com/android/inputmethod/latin/UserBigramTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.AndroidTestCase; +import com.android.inputmethod.latin.tests.R; +import java.util.Locale; + +public class UserBigramTests extends AndroidTestCase { + private static final String TAG = "UserBigramTests"; + + private static final int SUGGESTION_STARTS = 6; + private static final int MAX_DATA = 20; + private static final int DELETE_DATA = 10; + + private SuggestHelper sh; + + @Override + protected void setUp() { + int[] resId = new int[] { R.raw.test }; + sh = new SuggestHelper(TAG, getTestContext(), resId, MAX_DATA, DELETE_DATA); + } + + /************************** Tests ************************/ + + /** + * Test suggestion started at right time + */ + public void testUserBigram() { + for (int i = 0; i < SUGGESTION_STARTS; i++) sh.addToUserBigram(pair1); + for (int i = 0; i < (SUGGESTION_STARTS - 1); i++) sh.addToUserBigram(pair2); + + assertTrue(sh.isUserBigramSuggestion("user", 'b', "bigram")); + assertFalse(sh.isUserBigramSuggestion("android", 'p', "platform")); + } + + /** + * Test loading correct (locale) bigrams + */ + public void testOpenAndClose() { + for (int i = 0; i < SUGGESTION_STARTS; i++) sh.addToUserBigram(pair1); + assertTrue(sh.isUserBigramSuggestion("user", 'b', "bigram")); + + // change to fr_FR + sh.changeUserBigramLocale(getTestContext(), Locale.FRANCE); + for (int i = 0; i < SUGGESTION_STARTS; i++) sh.addToUserBigram(pair3); + assertTrue(sh.isUserBigramSuggestion("locale", 'f', "france")); + assertFalse(sh.isUserBigramSuggestion("user", 'b', "bigram")); + + // change back to en_US + sh.changeUserBigramLocale(getTestContext(), Locale.US); + assertFalse(sh.isUserBigramSuggestion("locale", 'f', "france")); + assertTrue(sh.isUserBigramSuggestion("user", 'b', "bigram")); + } + + /** + * Test data gets pruned when it is over maximum + */ + public void testPruningData() { + for (int i = 0; i < SUGGESTION_STARTS; i++) sh.addToUserBigram(sentence0); + sh.flushUserBigrams(); + assertTrue(sh.isUserBigramSuggestion("Hello", 'w', "world")); + + sh.addToUserBigram(sentence1); + sh.addToUserBigram(sentence2); + assertTrue(sh.isUserBigramSuggestion("Hello", 'w', "world")); + + // pruning should happen + sh.addToUserBigram(sentence3); + sh.addToUserBigram(sentence4); + + // trying to reopen database to check pruning happened in database + sh.changeUserBigramLocale(getTestContext(), Locale.US); + assertFalse(sh.isUserBigramSuggestion("Hello", 'w', "world")); + } + + final String[] pair1 = new String[] {"user", "bigram"}; + final String[] pair2 = new String[] {"android","platform"}; + final String[] pair3 = new String[] {"locale", "france"}; + final String sentence0 = "Hello world"; + final String sentence1 = "This is a test for user input based bigram"; + final String sentence2 = "It learns phrases that contain both dictionary and nondictionary " + + "words"; + final String sentence3 = "This should give better suggestions than the previous version"; + final String sentence4 = "Android stock keyboard is improving"; +}